mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2026-07-26 01:34:46 +10:00
feat(downloads): reduce scope of download agent
due to a miscommunication, the scope of the download agent has grown too much. this commit reduces that scopes, and intends for a lot of the heavy lifting to be done by the soon-to-be-implemented download manager.
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@ import {
|
|||||||
RectangleGroupIcon,
|
RectangleGroupIcon,
|
||||||
} from "@heroicons/vue/16/solid";
|
} from "@heroicons/vue/16/solid";
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue";
|
||||||
import type { NavigationItem } from "~/components/types";
|
import type { NavigationItem } from "~/types";
|
||||||
|
|
||||||
const navigation: Array<NavigationItem & { icon: Component }> = [
|
const navigation: Array<NavigationItem & { icon: Component }> = [
|
||||||
{
|
{
|
||||||
|
|||||||
+18
-118
@@ -1,139 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
|
||||||
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
|
||||||
@click="queueGameWrapper"
|
|
||||||
>
|
|
||||||
Queue Game Download
|
|
||||||
</button>
|
|
||||||
<input placeholder="GAME ID" v-model="gameId" />
|
<input placeholder="GAME ID" v-model="gameId" />
|
||||||
<input placeholder="VERSION NAME" v-model="versionName" />
|
<input placeholder="VERSION NAME" v-model="versionName" />
|
||||||
<input placeholder="STATUS" v-model="status" />
|
|
||||||
<input placeholder="NEW ROOT DIR" v-model="newRootDir" />
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
||||||
@click="startGameDownloadsWrapper"
|
@click="startGameDownload"
|
||||||
>
|
>
|
||||||
Start Game Downloads
|
Download game
|
||||||
|
<span v-if="progress != 0"> ({{ Math.floor(progress * 1000) / 10 }}%) </span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
|
||||||
@click="cancelGameDownloadWrapper"
|
|
||||||
>
|
|
||||||
Cancel game download
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
|
||||||
@click="getGameDownloadProgressWrapper"
|
|
||||||
>
|
|
||||||
Get game download progress
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
|
||||||
@click="setGameDownloadStatusWrapper"
|
|
||||||
>
|
|
||||||
Set game download progress
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-full rounded-md p-4 bg-blue-600 text-white"
|
|
||||||
@click="changeRootDirectoryWrapper"
|
|
||||||
>
|
|
||||||
Change root download location
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
const gameId = ref("");
|
const gameId = ref("");
|
||||||
const versionName = ref("");
|
const versionName = ref("");
|
||||||
const status = ref("");
|
const progress = ref(0);
|
||||||
const newRootDir = ref("");
|
|
||||||
|
|
||||||
|
async function startGameDownload() {
|
||||||
async function queueGame() {
|
await invoke("download_game", {
|
||||||
await invoke("queue_game_download", {
|
|
||||||
gameId: gameId.value,
|
gameId: gameId.value,
|
||||||
gameVersion: versionName.value,
|
gameVersion: versionName.value,
|
||||||
maxThreads: 12,
|
|
||||||
});
|
});
|
||||||
console.log("Requested game from FE");
|
|
||||||
}
|
|
||||||
function queueGameWrapper() {
|
|
||||||
console.log("Wrapper started");
|
|
||||||
queueGame()
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function startGameDownloads() {
|
|
||||||
console.log("Downloading Games");
|
|
||||||
await invoke("start_game_downloads", { maxThreads: 4 });
|
|
||||||
console.log("Finished downloading games");
|
|
||||||
}
|
|
||||||
function startGameDownloadsWrapper() {
|
|
||||||
startGameDownloads()
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function cancelGameDownloadWrapper() {
|
|
||||||
console.log("Triggered game cancel wrapper");
|
|
||||||
setGameDownloadStatus("Cancelled")
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async function getGameDownloadProgress() {
|
|
||||||
console.log("Getting game download status");
|
|
||||||
await invoke("get_game_download_progress", { gameId: gameId.value });
|
|
||||||
}
|
|
||||||
function getGameDownloadProgressWrapper() {
|
|
||||||
getGameDownloadProgress()
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/* status can be any of the following values:
|
|
||||||
Uninitialised,
|
|
||||||
Queued,
|
|
||||||
Paused,
|
|
||||||
Manifest,
|
|
||||||
Downloading,
|
|
||||||
Finished,
|
|
||||||
Stalled,
|
|
||||||
Failed,
|
|
||||||
Cancelled,
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function setGameDownloadStatus(status: string) {
|
setInterval(() => {
|
||||||
console.log("Setting game download status");
|
(async () => {
|
||||||
await invoke("set_download_state", { gameId: gameId.value, status: status });
|
const currentProgress = await invoke<number>(
|
||||||
}
|
"get_game_download_progress",
|
||||||
function setGameDownloadStatusWrapper() {
|
{
|
||||||
console.log("Called setGameDownloadWrapper");
|
gameId: gameId.value,
|
||||||
setGameDownloadStatus(status.value)
|
}
|
||||||
.then(() => {})
|
);
|
||||||
.catch((e) => {
|
console.log(currentProgress);
|
||||||
console.log(e)
|
progress.value = currentProgress;
|
||||||
})
|
})();
|
||||||
}
|
}, 100);
|
||||||
async function changeRootDirectory() {
|
|
||||||
console.log("Changing root directory");
|
|
||||||
await invoke("change_root_directory", { newDir: newRootDir.value });
|
|
||||||
}
|
|
||||||
function changeRootDirectoryWrapper() {
|
|
||||||
changeRootDirectory()
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
|||||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = client.post(endpoint).json(&body).send()?;
|
let response = client.post(endpoint).json(&body).send()?;
|
||||||
info!("server responded with {}", response.status());
|
|
||||||
let response_struct = response.json::<HandshakeResponse>()?;
|
let response_struct = response.json::<HandshakeResponse>()?;
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
+35
-7
@@ -1,7 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
borrow::BorrowMut,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
fmt::format,
|
||||||
fs::{self, create_dir_all},
|
fs::{self, create_dir_all},
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
sync::{LazyLock, Mutex},
|
sync::{LazyLock, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -9,8 +11,11 @@ use directories::BaseDirs;
|
|||||||
use log::info;
|
use log::info;
|
||||||
use rustbreak::{deser::Bincode, PathDatabase};
|
use rustbreak::{deser::Bincode, PathDatabase};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::fs::metadata;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::DB;
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone, Deserialize)]
|
#[derive(serde::Serialize, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DatabaseAuth {
|
pub struct DatabaseAuth {
|
||||||
@@ -32,7 +37,7 @@ pub enum DatabaseGameStatus {
|
|||||||
#[derive(Serialize, Clone, Deserialize)]
|
#[derive(Serialize, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DatabaseGames {
|
pub struct DatabaseGames {
|
||||||
pub games_base_dir: String,
|
pub install_dirs: Vec<String>,
|
||||||
pub games_statuses: HashMap<String, DatabaseGameStatus>,
|
pub games_statuses: HashMap<String, DatabaseGameStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
auth: None,
|
auth: None,
|
||||||
base_url: "".to_string(),
|
base_url: "".to_string(),
|
||||||
games: DatabaseGames {
|
games: DatabaseGames {
|
||||||
games_base_dir: games_base_dir.to_str().unwrap().to_string(),
|
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
|
||||||
games_statuses: HashMap::new(),
|
games_statuses: HashMap::new(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -91,8 +96,31 @@ impl DatabaseImpls for DatabaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn change_root_directory(new_dir: String) {
|
pub fn add_new_download_dir(new_dir: String) -> Result<(), String> {
|
||||||
info!("Changed root directory to {}", new_dir);
|
// Check the new directory is all good
|
||||||
let mut lock = DATA_ROOT_DIR.lock().unwrap();
|
let new_dir_path = Path::new(&new_dir);
|
||||||
*lock = new_dir.into();
|
if new_dir_path.exists() {
|
||||||
|
let metadata = new_dir_path
|
||||||
|
.metadata()
|
||||||
|
.map_err(|e| format!("Unable to access file or directory: {}", e.to_string()))?;
|
||||||
|
if metadata.is_dir() {
|
||||||
|
return Err("Invalid path: not a directory".to_string());
|
||||||
|
}
|
||||||
|
let dir_contents = new_dir_path
|
||||||
|
.read_dir()
|
||||||
|
.map_err(|e| format!("Unable to check directory contents: {}", e.to_string()))?;
|
||||||
|
if dir_contents.count() == 0 {
|
||||||
|
return Err("Path is not empty".to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
create_dir_all(new_dir_path)
|
||||||
|
.map_err(|e| format!("Unable to create directories to path: {}", e.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add it to the dictionary
|
||||||
|
let mut lock = DB.borrow_data_mut().unwrap();
|
||||||
|
lock.games.install_dirs.push(new_dir);
|
||||||
|
drop(lock);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,121 @@
|
|||||||
use crate::auth::generate_authorization_header;
|
use crate::auth::generate_authorization_header;
|
||||||
use crate::db::{DatabaseImpls, DATA_ROOT_DIR};
|
use crate::db::DatabaseImpls;
|
||||||
use crate::downloads::download_logic;
|
|
||||||
use crate::downloads::manifest::{DropDownloadContext, DropManifest};
|
use crate::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||||
use crate::downloads::progress::ProgressChecker;
|
use crate::remote::RemoteAccessError;
|
||||||
use crate::DB;
|
use crate::DB;
|
||||||
use atomic_counter::RelaxedCounter;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use rustix::fs::{fallocate, FallocateFlags};
|
use rayon::ThreadPoolBuilder;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use rustix::fs::{fallocate, FallocateFlags};
|
||||||
|
|
||||||
|
use super::download_logic::download_game_chunk;
|
||||||
|
|
||||||
pub struct GameDownloadAgent {
|
pub struct GameDownloadAgent {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub status: Arc<RwLock<GameDownloadState>>,
|
pub control_flag: Arc<RwLock<DownloadThreadControlFlag>>,
|
||||||
|
pub target_download_dir: usize,
|
||||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||||
pub progress: ProgressChecker<DropDownloadContext>,
|
// pub progress: ProgressChecker<DropDownloadContext>,
|
||||||
pub manifest: Mutex<Option<DropManifest>>,
|
pub manifest: Mutex<Option<DropManifest>>,
|
||||||
|
pub progress: ProgressObject,
|
||||||
}
|
}
|
||||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||||
pub enum GameDownloadState {
|
pub enum DownloadThreadControlFlag {
|
||||||
Uninitialised,
|
Go,
|
||||||
Queued,
|
Stop,
|
||||||
Paused,
|
|
||||||
Manifest,
|
|
||||||
Downloading,
|
|
||||||
Finished,
|
|
||||||
Stalled,
|
|
||||||
Failed,
|
|
||||||
Cancelled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug)]
|
||||||
pub enum GameDownloadError {
|
pub enum GameDownloadError {
|
||||||
ManifestDownload,
|
CommunicationError(RemoteAccessError),
|
||||||
FailedContextGeneration,
|
ChecksumError,
|
||||||
Status(u16),
|
SetupError(String),
|
||||||
System(SystemError),
|
LockError,
|
||||||
}
|
}
|
||||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
|
||||||
pub enum SystemError {
|
impl Display for GameDownloadError {
|
||||||
MutexLockFailed,
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
GameDownloadError::CommunicationError(error) => write!(f, "{}", error),
|
||||||
|
GameDownloadError::SetupError(error) => write!(f, "{}", error),
|
||||||
|
GameDownloadError::LockError => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
|
||||||
|
GameDownloadError::ChecksumError => write!(f, "Checksum failed to validate for download"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DOWNLOAD_MAX_THREADS: usize = 4;
|
||||||
|
|
||||||
|
pub struct ProgressObject {
|
||||||
|
pub max: u64,
|
||||||
|
pub current: Arc<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameDownloadAgent {
|
impl GameDownloadAgent {
|
||||||
pub fn new(id: String, version: String) -> Self {
|
pub fn new(id: String, version: String, target_download_dir: usize) -> Self {
|
||||||
let status = Arc::new(RwLock::new(GameDownloadState::Uninitialised));
|
// Don't run by default
|
||||||
|
let status = Arc::new(RwLock::new(DownloadThreadControlFlag::Stop));
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
version,
|
version,
|
||||||
status: status.clone(),
|
control_flag: status.clone(),
|
||||||
manifest: Mutex::new(None),
|
manifest: Mutex::new(None),
|
||||||
progress: ProgressChecker::new(
|
target_download_dir,
|
||||||
Box::new(download_logic::download_game_chunk),
|
|
||||||
Arc::new(RelaxedCounter::new(0)),
|
|
||||||
status,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
contexts: Mutex::new(Vec::new()),
|
contexts: Mutex::new(Vec::new()),
|
||||||
|
progress: ProgressObject {
|
||||||
|
max: 0,
|
||||||
|
current: Arc::new(AtomicU64::new(0)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn queue(&self) -> Result<(), GameDownloadError> {
|
pub fn set_control_flag(&self, flag: DownloadThreadControlFlag) {
|
||||||
self.change_state(GameDownloadState::Queued);
|
let mut lock = self.control_flag.write().unwrap();
|
||||||
if self.manifest.lock().unwrap().is_none() {
|
*lock = flag;
|
||||||
return Ok(());
|
}
|
||||||
}
|
pub fn get_control_flag(&self) -> DownloadThreadControlFlag {
|
||||||
self.ensure_manifest_exists()
|
let lock = self.control_flag.read().unwrap();
|
||||||
|
lock.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn begin_download(&self, max_threads: usize) -> Result<(), GameDownloadError> {
|
// Blocking
|
||||||
self.change_state(GameDownloadState::Downloading);
|
// Requires mutable self
|
||||||
// TODO we're coping the whole context thing
|
pub fn setup_download(&mut self) -> Result<(), GameDownloadError> {
|
||||||
// It's not necessary, I just can't figure out to make the borrow checker happy
|
self.ensure_manifest_exists()?;
|
||||||
{
|
|
||||||
let lock = self.contexts.lock().unwrap().to_vec();
|
self.generate_contexts()?;
|
||||||
self.progress.run_context_parallel(lock, max_threads);
|
|
||||||
}
|
self.set_control_flag(DownloadThreadControlFlag::Go);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> {
|
// Blocking
|
||||||
|
pub fn download(&mut self) -> Result<(), GameDownloadError> {
|
||||||
|
self.setup_download()?;
|
||||||
|
self.run();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_manifest_exists(&mut self) -> Result<(), GameDownloadError> {
|
||||||
if self.manifest.lock().unwrap().is_some() {
|
if self.manifest.lock().unwrap().is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.download_manifest()
|
// Explicitly propagate error
|
||||||
|
Ok(self.download_manifest()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_manifest(&self) -> Result<(), GameDownloadError> {
|
fn download_manifest(&mut self) -> Result<(), GameDownloadError> {
|
||||||
let base_url = DB.fetch_base_url();
|
let base_url = DB.fetch_base_url();
|
||||||
let manifest_url = base_url
|
let manifest_url = base_url
|
||||||
.join(
|
.join(
|
||||||
@@ -104,8 +129,6 @@ impl GameDownloadAgent {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
info!("Generating & sending client");
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
let response = client
|
let response = client
|
||||||
.get(manifest_url.to_string())
|
.get(manifest_url.to_string())
|
||||||
@@ -114,8 +137,14 @@ impl GameDownloadAgent {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if response.status() != 200 {
|
if response.status() != 200 {
|
||||||
info!("Error status: {}", response.status());
|
return Err(GameDownloadError::CommunicationError(
|
||||||
return Err(GameDownloadError::Status(response.status().as_u16()));
|
format!(
|
||||||
|
"Failed to download game manifest: {} {}",
|
||||||
|
response.status(),
|
||||||
|
response.text().unwrap()
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest_download = response.json::<DropManifest>().unwrap();
|
let manifest_download = response.json::<DropManifest>().unwrap();
|
||||||
@@ -125,42 +154,33 @@ impl GameDownloadAgent {
|
|||||||
return chunk.lengths.iter().sum::<usize>();
|
return chunk.lengths.iter().sum::<usize>();
|
||||||
})
|
})
|
||||||
.sum::<usize>();
|
.sum::<usize>();
|
||||||
self.progress.set_capacity(length);
|
self.progress.max = length.try_into().unwrap();
|
||||||
|
|
||||||
if let Ok(mut manifest) = self.manifest.lock() {
|
if let Ok(mut manifest) = self.manifest.lock() {
|
||||||
*manifest = Some(manifest_download)
|
*manifest = Some(manifest_download);
|
||||||
} else {
|
return Ok(());
|
||||||
return Err(GameDownloadError::System(SystemError::MutexLockFailed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
return Err(GameDownloadError::LockError);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn change_state(&self, state: GameDownloadState) {
|
pub fn generate_contexts(&self) -> Result<(), GameDownloadError> {
|
||||||
let mut lock = self.status.write().unwrap();
|
let db_lock = DB.borrow_data().unwrap();
|
||||||
*lock = state;
|
let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone();
|
||||||
}
|
drop(db_lock);
|
||||||
pub fn get_state(&self) -> GameDownloadState {
|
|
||||||
let lock = self.status.read().unwrap();
|
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||||
lock.clone()
|
let version = self.version.clone();
|
||||||
}
|
let game_id = self.id.clone();
|
||||||
|
|
||||||
|
let data_base_dir_path = Path::new(&data_base_dir);
|
||||||
|
|
||||||
pub fn generate_job_contexts(
|
|
||||||
&self,
|
|
||||||
manifest: &DropManifest,
|
|
||||||
version: String,
|
|
||||||
game_id: String,
|
|
||||||
) -> Result<(), GameDownloadError> {
|
|
||||||
let mut contexts = Vec::new();
|
let mut contexts = Vec::new();
|
||||||
let base_path = DATA_ROOT_DIR
|
let base_path = data_base_dir_path.join(game_id.clone()).clone();
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.join("games")
|
|
||||||
.join(game_id.clone())
|
|
||||||
.clone();
|
|
||||||
create_dir_all(base_path.clone()).unwrap();
|
create_dir_all(base_path.clone()).unwrap();
|
||||||
info!("Generating contexts");
|
|
||||||
for (raw_path, chunk) in manifest {
|
for (raw_path, chunk) in manifest {
|
||||||
let path = base_path.join(Path::new(raw_path));
|
let path = base_path.join(Path::new(&raw_path));
|
||||||
|
|
||||||
let container = path.parent().unwrap();
|
let container = path.parent().unwrap();
|
||||||
create_dir_all(container).unwrap();
|
create_dir_all(container).unwrap();
|
||||||
@@ -181,17 +201,44 @@ impl GameDownloadAgent {
|
|||||||
running_offset += *length as u64;
|
running_offset += *length as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
if running_offset > 0 {
|
if running_offset > 0 {
|
||||||
fallocate(file, FallocateFlags::empty(), 0, running_offset).unwrap();
|
fallocate(file, FallocateFlags::empty(), 0, running_offset).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!("Finished generating");
|
|
||||||
if let Ok(mut context_lock) = self.contexts.lock() {
|
if let Ok(mut context_lock) = self.contexts.lock() {
|
||||||
*context_lock = contexts;
|
*context_lock = contexts;
|
||||||
} else {
|
return Ok(());
|
||||||
return Err(GameDownloadError::FailedContextGeneration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
return Err(GameDownloadError::SetupError(
|
||||||
|
"Failed to generate download contexts".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self) {
|
||||||
|
let pool = ThreadPoolBuilder::new()
|
||||||
|
.num_threads(DOWNLOAD_MAX_THREADS)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
pool.scope(move |scope| {
|
||||||
|
let contexts = self.contexts.lock().unwrap();
|
||||||
|
|
||||||
|
for context in contexts.iter() {
|
||||||
|
let context = context.clone();
|
||||||
|
let control_flag = self.control_flag.clone(); // Clone arcs
|
||||||
|
let progress = self.progress.current.clone(); // Clone arcs
|
||||||
|
info!(
|
||||||
|
"starting download for file {} {}",
|
||||||
|
context.file_name, context.index
|
||||||
|
);
|
||||||
|
|
||||||
|
scope.spawn(move |_| {
|
||||||
|
download_game_chunk(context, control_flag, progress).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,137 +1,62 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use rayon::spawn;
|
||||||
|
|
||||||
use crate::{downloads::download_agent::GameDownloadAgent, AppState};
|
use crate::{downloads::download_agent::GameDownloadAgent, AppState};
|
||||||
|
|
||||||
use super::download_agent::{GameDownloadError, GameDownloadState};
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn queue_game_download(
|
pub fn download_game(
|
||||||
game_id: String,
|
game_id: String,
|
||||||
game_version: String,
|
game_version: String,
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
) -> Result<(), GameDownloadError> {
|
|
||||||
info!("Queuing Game Download");
|
|
||||||
let download_agent = Arc::new(GameDownloadAgent::new(
|
|
||||||
game_id.clone(),
|
|
||||||
game_version.clone(),
|
|
||||||
));
|
|
||||||
download_agent.queue().await?;
|
|
||||||
|
|
||||||
let mut queue = state.lock().unwrap();
|
|
||||||
queue.game_downloads.insert(game_id, download_agent);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn start_game_downloads(
|
|
||||||
max_threads: usize,
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
) -> Result<(), GameDownloadError> {
|
|
||||||
info!("Downloading Games");
|
|
||||||
let lock = state.lock().unwrap();
|
|
||||||
let mut game_downloads = lock.game_downloads.clone();
|
|
||||||
drop(lock);
|
|
||||||
thread::spawn(move || loop {
|
|
||||||
let mut current_id = String::new();
|
|
||||||
let mut download_agent = None;
|
|
||||||
{
|
|
||||||
for (id, agent) in &game_downloads {
|
|
||||||
if agent.get_state() == GameDownloadState::Queued {
|
|
||||||
download_agent = Some(agent.clone());
|
|
||||||
current_id = id.clone();
|
|
||||||
info!("Got queued game to download");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if download_agent.is_none() {
|
|
||||||
info!("No more games left to download");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
info!("Downloading game");
|
|
||||||
{
|
|
||||||
start_game_download(max_threads, download_agent.unwrap()).unwrap();
|
|
||||||
game_downloads.remove_entry(¤t_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
info!("Spawned download");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_game_download(
|
|
||||||
max_threads: usize,
|
|
||||||
download_agent: Arc<GameDownloadAgent>,
|
|
||||||
) -> Result<(), GameDownloadError> {
|
|
||||||
info!("Triggered Game Download");
|
|
||||||
|
|
||||||
download_agent.ensure_manifest_exists()?;
|
|
||||||
|
|
||||||
let local_manifest = {
|
|
||||||
let manifest = download_agent.manifest.lock().unwrap();
|
|
||||||
(*manifest).clone().unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
download_agent
|
|
||||||
.generate_job_contexts(
|
|
||||||
&local_manifest,
|
|
||||||
download_agent.version.clone(),
|
|
||||||
download_agent.id.clone(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
download_agent.begin_download(max_threads).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn cancel_specific_game_download(
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
game_id: String,
|
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
info!("called stop_specific_game_download");
|
info!("beginning game download...");
|
||||||
get_game_download(state, game_id).change_state(GameDownloadState::Cancelled);
|
|
||||||
|
|
||||||
//TODO: Drop the game download instance
|
let mut download_agent = GameDownloadAgent::new(game_id.clone(), game_version.clone(), 0);
|
||||||
|
// Setup download requires mutable
|
||||||
|
download_agent.setup_download().unwrap();
|
||||||
|
|
||||||
info!("Stopping callback");
|
let mut lock: std::sync::MutexGuard<'_, AppState> = state.lock().unwrap();
|
||||||
|
let download_agent_ref = Arc::new(download_agent);
|
||||||
|
lock.game_downloads
|
||||||
|
.insert(game_id, download_agent_ref.clone());
|
||||||
|
|
||||||
|
// Run it in another thread
|
||||||
|
spawn(move || {
|
||||||
|
// Run doesn't require mutable
|
||||||
|
download_agent_ref.clone().run();
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_game_download_progress(
|
pub fn get_game_download_progress(
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
) -> Result<f64, String> {
|
) -> Result<f64, String> {
|
||||||
let progress = get_game_download(state, game_id)
|
let da = use_download_agent(state, game_id)?;
|
||||||
.progress
|
|
||||||
.get_progress_percentage();
|
let progress = &da.progress;
|
||||||
info!("{}", progress);
|
let current: f64 = progress
|
||||||
Ok(progress)
|
.current
|
||||||
|
.fetch_add(0, std::sync::atomic::Ordering::Relaxed) as f64;
|
||||||
|
let max = progress.max as f64;
|
||||||
|
|
||||||
|
let current_progress = current / max;
|
||||||
|
|
||||||
|
Ok(current_progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
fn use_download_agent(
|
||||||
pub async fn set_download_state(
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
state: tauri::State<'_, Mutex<AppState>>,
|
||||||
game_id: String,
|
game_id: String,
|
||||||
status: GameDownloadState,
|
) -> Result<Arc<GameDownloadAgent>, String> {
|
||||||
) -> Result<(), String> {
|
|
||||||
info!("Setting game state");
|
|
||||||
get_game_download(state, game_id).change_state(status);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_game_download(
|
|
||||||
state: tauri::State<'_, Mutex<AppState>>,
|
|
||||||
game_id: String,
|
|
||||||
) -> Arc<GameDownloadAgent> {
|
|
||||||
let lock = state.lock().unwrap();
|
let lock = state.lock().unwrap();
|
||||||
let download_agent = lock.game_downloads.get(&game_id).unwrap();
|
let download_agent = lock.game_downloads.get(&game_id).ok_or("Invalid game ID")?;
|
||||||
download_agent.clone()
|
Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,136 @@
|
|||||||
|
use crate::auth::generate_authorization_header;
|
||||||
use crate::db::DatabaseImpls;
|
use crate::db::DatabaseImpls;
|
||||||
use crate::downloads::manifest::DropDownloadContext;
|
use crate::downloads::manifest::DropDownloadContext;
|
||||||
|
use crate::remote::RemoteAccessError;
|
||||||
use crate::DB;
|
use crate::DB;
|
||||||
use crate::{auth::generate_authorization_header, GAME_PAUSE_CHECK_INTERVAL};
|
|
||||||
use atomic_counter::{AtomicCounter, RelaxedCounter};
|
use atomic_counter::{AtomicCounter, RelaxedCounter};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use md5::{Context, Digest};
|
use md5::{Context, Digest};
|
||||||
|
use reqwest::blocking::Response;
|
||||||
|
use serde::de::Error;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::RwLock;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{File, OpenOptions},
|
fs::{File, OpenOptions},
|
||||||
io::{self, BufWriter, Error, ErrorKind, Seek, SeekFrom, Write},
|
io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
sync::Arc,
|
||||||
thread::sleep,
|
|
||||||
};
|
};
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
|
|
||||||
use super::download_agent::GameDownloadState;
|
use super::download_agent::{DownloadThreadControlFlag, GameDownloadError};
|
||||||
|
|
||||||
pub struct DropFileWriter {
|
pub struct DropWriter<W: Write> {
|
||||||
file: File,
|
|
||||||
hasher: Context,
|
hasher: Context,
|
||||||
progress: Arc<RelaxedCounter>,
|
destination: W,
|
||||||
status: Arc<RwLock<GameDownloadState>>,
|
|
||||||
}
|
}
|
||||||
impl DropFileWriter {
|
impl DropWriter<File> {
|
||||||
fn new(
|
fn new(path: PathBuf) -> Self {
|
||||||
path: PathBuf,
|
|
||||||
status: Arc<RwLock<GameDownloadState>>,
|
|
||||||
progress: Arc<RelaxedCounter>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
file: OpenOptions::new().write(true).open(path).unwrap(),
|
destination: OpenOptions::new().write(true).open(path).unwrap(),
|
||||||
hasher: Context::new(),
|
hasher: Context::new(),
|
||||||
progress,
|
|
||||||
status,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut self) -> io::Result<Digest> {
|
fn finish(mut self) -> io::Result<Digest> {
|
||||||
self.flush().unwrap();
|
self.flush().unwrap();
|
||||||
Ok(self.hasher.compute())
|
Ok(self.hasher.compute())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_state(&mut self) -> Option<Result<usize, Error>> {
|
|
||||||
match self.status.read().unwrap().clone() {
|
|
||||||
GameDownloadState::Uninitialised => todo!(),
|
|
||||||
GameDownloadState::Queued => {
|
|
||||||
return Some(Err(Error::new(
|
|
||||||
ErrorKind::NotConnected,
|
|
||||||
"Download has not yet been started",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
GameDownloadState::Manifest => {
|
|
||||||
return Some(Err(Error::new(
|
|
||||||
ErrorKind::NotFound,
|
|
||||||
"Manifest still not finished downloading",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
GameDownloadState::Downloading => {}
|
|
||||||
GameDownloadState::Finished => {
|
|
||||||
return Some(Err(Error::new(
|
|
||||||
ErrorKind::AlreadyExists,
|
|
||||||
"Download already finished",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
GameDownloadState::Stalled => {
|
|
||||||
return Some(Err(Error::new(ErrorKind::Interrupted, "Download Stalled")))
|
|
||||||
}
|
|
||||||
GameDownloadState::Failed => {
|
|
||||||
return Some(Err(Error::new(ErrorKind::BrokenPipe, "Download Failed")))
|
|
||||||
}
|
|
||||||
GameDownloadState::Cancelled => {
|
|
||||||
return Some(Err(Error::new(
|
|
||||||
ErrorKind::ConnectionAborted,
|
|
||||||
"Interrupt command recieved",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
GameDownloadState::Paused => {
|
|
||||||
info!("Game download paused");
|
|
||||||
sleep(GAME_PAUSE_CHECK_INTERVAL);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// TODO: Implement error handling
|
// Write automatically pushes to file and hasher
|
||||||
impl Write for DropFileWriter {
|
impl Write for DropWriter<File> {
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
// TODO: Tidy up these error messages / types because these ones don't really seem to fit
|
self.hasher.write_all(buf).map_err(|e| {
|
||||||
if let Some(value) = self.manage_state() {
|
io::Error::new(
|
||||||
return value;
|
ErrorKind::Other,
|
||||||
}
|
format!("Unable to write to hasher: {}", e),
|
||||||
let len = buf.len();
|
)
|
||||||
self.progress.add(len);
|
})?;
|
||||||
|
self.destination.write(buf)
|
||||||
//info!("Writing data to writer");
|
|
||||||
self.hasher.write_all(buf).unwrap();
|
|
||||||
self.file.write(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
self.hasher.flush()?;
|
self.hasher.flush()?;
|
||||||
self.file.flush()
|
self.destination.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Seek for DropFileWriter {
|
// Seek moves around destination output
|
||||||
|
impl Seek for DropWriter<File> {
|
||||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
self.file.seek(pos)
|
self.destination.seek(pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DropDownloadPipeline<R: Read, W: Write> {
|
||||||
|
pub source: R,
|
||||||
|
pub destination: DropWriter<W>,
|
||||||
|
pub control_flag: Arc<RwLock<DownloadThreadControlFlag>>,
|
||||||
|
pub progress: Arc<AtomicU64>,
|
||||||
|
pub size: usize,
|
||||||
|
}
|
||||||
|
impl DropDownloadPipeline<Response, File> {
|
||||||
|
fn new(
|
||||||
|
source: Response,
|
||||||
|
destination: DropWriter<File>,
|
||||||
|
control_flag: Arc<RwLock<DownloadThreadControlFlag>>,
|
||||||
|
progress: Arc<AtomicU64>,
|
||||||
|
size: usize,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
control_flag,
|
||||||
|
progress,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy(&mut self) -> Result<bool, io::Error> {
|
||||||
|
let copy_buf_size = 512;
|
||||||
|
let mut copy_buf = vec![0; copy_buf_size];
|
||||||
|
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination);
|
||||||
|
|
||||||
|
let mut current_size = 0;
|
||||||
|
loop {
|
||||||
|
if *self.control_flag.read().unwrap() == DownloadThreadControlFlag::Stop {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_read = self.source.read(&mut copy_buf)?;
|
||||||
|
current_size += bytes_read;
|
||||||
|
|
||||||
|
buf_writer.write(©_buf[0..bytes_read])?;
|
||||||
|
self.progress.fetch_add(
|
||||||
|
bytes_read.try_into().unwrap(),
|
||||||
|
std::sync::atomic::Ordering::Relaxed,
|
||||||
|
);
|
||||||
|
|
||||||
|
if current_size == self.size {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Result<Digest, io::Error> {
|
||||||
|
let checksum = self.destination.finish()?;
|
||||||
|
return Ok(checksum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn download_game_chunk(
|
pub fn download_game_chunk(
|
||||||
ctx: DropDownloadContext,
|
ctx: DropDownloadContext,
|
||||||
status: Arc<RwLock<GameDownloadState>>,
|
control_flag: Arc<RwLock<DownloadThreadControlFlag>>,
|
||||||
progress: Arc<RelaxedCounter>,
|
progress: Arc<AtomicU64>,
|
||||||
) {
|
) -> Result<bool, GameDownloadError> {
|
||||||
if *status.read().unwrap() == GameDownloadState::Cancelled {
|
// If we're paused
|
||||||
info!("Callback stopped download at start");
|
if *control_flag.read().unwrap() == DownloadThreadControlFlag::Stop {
|
||||||
return;
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_url = DB.fetch_base_url();
|
let base_url = DB.fetch_base_url();
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
@@ -133,47 +147,48 @@ pub fn download_game_chunk(
|
|||||||
|
|
||||||
let header = generate_authorization_header();
|
let header = generate_authorization_header();
|
||||||
|
|
||||||
let mut response = match client.get(chunk_url).header("Authorization", header).send() {
|
let response = client
|
||||||
Ok(response) => response,
|
.get(chunk_url)
|
||||||
Err(e) => {
|
.header("Authorization", header)
|
||||||
info!("{}", e);
|
.send()
|
||||||
return;
|
.map_err(|e| GameDownloadError::CommunicationError(RemoteAccessError::FetchError(e)))?;
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut file: DropFileWriter = DropFileWriter::new(ctx.path, status, progress);
|
let mut destination = DropWriter::new(ctx.path);
|
||||||
|
|
||||||
if ctx.offset != 0 {
|
if ctx.offset != 0 {
|
||||||
file.seek(SeekFrom::Start(ctx.offset))
|
destination
|
||||||
|
.seek(SeekFrom::Start(ctx.offset))
|
||||||
.expect("Failed to seek to file offset");
|
.expect("Failed to seek to file offset");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writing everything to disk directly is probably slightly faster in terms of disk
|
let content_length = response.content_length();
|
||||||
// speed because it balances out the writes, but this is better than the performance
|
if content_length.is_none() {
|
||||||
// loss from constantly reading the callbacks
|
return Err(GameDownloadError::CommunicationError(
|
||||||
|
RemoteAccessError::GenericErrror(
|
||||||
let mut writer = BufWriter::with_capacity(1024 * 1024, file);
|
"Invalid download endpoint, missing Content-Length header.".to_owned(),
|
||||||
|
),
|
||||||
match io::copy(&mut response, &mut writer) {
|
));
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
info!("Copy errored with error {}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
writer.flush().unwrap();
|
|
||||||
let file = match writer.into_inner() {
|
let mut pipeline = DropDownloadPipeline::new(
|
||||||
Ok(file) => file,
|
response,
|
||||||
Err(_) => {
|
destination,
|
||||||
error!("Failed to acquire writer from BufWriter");
|
control_flag,
|
||||||
return;
|
progress,
|
||||||
}
|
content_length.unwrap().try_into().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let completed = pipeline.copy().unwrap();
|
||||||
|
if !completed {
|
||||||
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = hex::encode(file.finish().unwrap().0);
|
let checksum = pipeline.finish().unwrap();
|
||||||
|
|
||||||
|
let res = hex::encode(checksum.0);
|
||||||
if res != ctx.checksum {
|
if res != ctx.checksum {
|
||||||
info!(
|
return Err(GameDownloadError::ChecksumError);
|
||||||
"Checksum failed. Original: {}, Calculated: {} for {}",
|
|
||||||
ctx.checksum, res, ctx.file_name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod download_agent;
|
pub mod download_agent;
|
||||||
pub mod download_commands;
|
pub mod download_commands;
|
||||||
mod download_logic;
|
mod download_logic;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
pub mod progress;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
use atomic_counter::{AtomicCounter, RelaxedCounter};
|
|
||||||
use log::info;
|
|
||||||
use rayon::ThreadPoolBuilder;
|
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
|
||||||
|
|
||||||
use super::download_agent::GameDownloadState;
|
|
||||||
|
|
||||||
pub struct ProgressChecker<T>
|
|
||||||
where
|
|
||||||
T: 'static + Send + Sync,
|
|
||||||
{
|
|
||||||
counter: Arc<RelaxedCounter>,
|
|
||||||
f: Arc<
|
|
||||||
Box<dyn Fn(T, Arc<RwLock<GameDownloadState>>, Arc<RelaxedCounter>) + Send + Sync + 'static>,
|
|
||||||
>,
|
|
||||||
status: Arc<RwLock<GameDownloadState>>,
|
|
||||||
capacity: Mutex<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ProgressChecker<T>
|
|
||||||
where
|
|
||||||
T: Send + Sync,
|
|
||||||
{
|
|
||||||
pub fn new(
|
|
||||||
f: Box<
|
|
||||||
dyn Fn(T, Arc<RwLock<GameDownloadState>>, Arc<RelaxedCounter>) + Send + Sync + 'static,
|
|
||||||
>,
|
|
||||||
counter: Arc<RelaxedCounter>,
|
|
||||||
status: Arc<RwLock<GameDownloadState>>,
|
|
||||||
capacity: usize,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
f: f.into(),
|
|
||||||
counter,
|
|
||||||
status,
|
|
||||||
capacity: capacity.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn run_context_parallel(&self, contexts: Vec<T>, max_threads: usize) {
|
|
||||||
let threads = ThreadPoolBuilder::new()
|
|
||||||
.num_threads(max_threads)
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
threads.scope(|s| {
|
|
||||||
for context in contexts {
|
|
||||||
let status = self.status.clone();
|
|
||||||
let counter = self.counter.clone();
|
|
||||||
let f = self.f.clone();
|
|
||||||
s.spawn(move |_| {
|
|
||||||
info!("Running thread");
|
|
||||||
f(context, status, counter)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
info!("Concluded scope");
|
|
||||||
}
|
|
||||||
pub fn set_capacity(&self, capacity: usize) {
|
|
||||||
let mut lock = self.capacity.lock().unwrap();
|
|
||||||
*lock = capacity;
|
|
||||||
}
|
|
||||||
pub fn get_progress(&self) -> usize {
|
|
||||||
self.counter.get()
|
|
||||||
}
|
|
||||||
// I strongly dislike type casting in my own code, so I've shovelled it into here
|
|
||||||
pub fn get_progress_percentage(&self) -> f64 {
|
|
||||||
(self.get_progress() as f64) / (*self.capacity.lock().unwrap() as f64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ mod tests;
|
|||||||
use crate::db::DatabaseImpls;
|
use crate::db::DatabaseImpls;
|
||||||
use crate::downloads::download_agent::GameDownloadAgent;
|
use crate::downloads::download_agent::GameDownloadAgent;
|
||||||
use auth::{auth_initiate, generate_authorization_header, recieve_handshake};
|
use auth::{auth_initiate, generate_authorization_header, recieve_handshake};
|
||||||
use db::{change_root_directory, DatabaseInterface, DATA_ROOT_DIR};
|
use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR};
|
||||||
use downloads::download_commands::*;
|
use downloads::download_commands::*;
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use http::{header::*, response::Builder as ResponseBuilder};
|
use http::{header::*, response::Builder as ResponseBuilder};
|
||||||
@@ -119,13 +119,10 @@ pub fn run() {
|
|||||||
// Library
|
// Library
|
||||||
fetch_library,
|
fetch_library,
|
||||||
fetch_game,
|
fetch_game,
|
||||||
change_root_directory,
|
add_new_download_dir,
|
||||||
// Downloads
|
// Downloads
|
||||||
queue_game_download,
|
download_game,
|
||||||
start_game_downloads,
|
|
||||||
cancel_specific_game_download,
|
|
||||||
get_game_download_progress,
|
get_game_download_progress,
|
||||||
set_download_state
|
|
||||||
])
|
])
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
|||||||
Reference in New Issue
Block a user