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:
DecDuck
2024-11-10 22:25:54 +11:00
parent 4983b25702
commit 6a38ea306b
10 changed files with 352 additions and 511 deletions
+1 -1
View File
@@ -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
View File
@@ -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>
-1
View File
@@ -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
View File
@@ -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(())
} }
+133 -86
View File
@@ -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();
});
}
})
} }
} }
+33 -108
View File
@@ -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(&current_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
} }
+128 -113
View File
@@ -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(&copy_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 -2
View File
@@ -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;
-69
View File
@@ -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)
}
}
+3 -6
View File
@@ -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| {