mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-14 08:41:21 +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:
@ -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 client = reqwest::blocking::Client::new();
|
||||
let response = client.post(endpoint).json(&body).send()?;
|
||||
info!("server responded with {}", response.status());
|
||||
let response_struct = response.json::<HandshakeResponse>()?;
|
||||
|
||||
{
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
use std::{
|
||||
borrow::BorrowMut,
|
||||
collections::HashMap,
|
||||
fmt::format,
|
||||
fs::{self, create_dir_all},
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
sync::{LazyLock, Mutex},
|
||||
};
|
||||
|
||||
@ -9,8 +11,11 @@ use directories::BaseDirs;
|
||||
use log::info;
|
||||
use rustbreak::{deser::Bincode, PathDatabase};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs::metadata;
|
||||
use url::Url;
|
||||
|
||||
use crate::DB;
|
||||
|
||||
#[derive(serde::Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DatabaseAuth {
|
||||
@ -32,7 +37,7 @@ pub enum DatabaseGameStatus {
|
||||
#[derive(Serialize, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DatabaseGames {
|
||||
pub games_base_dir: String,
|
||||
pub install_dirs: Vec<String>,
|
||||
pub games_statuses: HashMap<String, DatabaseGameStatus>,
|
||||
}
|
||||
|
||||
@ -67,7 +72,7 @@ impl DatabaseImpls for DatabaseInterface {
|
||||
auth: None,
|
||||
base_url: "".to_string(),
|
||||
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(),
|
||||
},
|
||||
};
|
||||
@ -91,8 +96,31 @@ impl DatabaseImpls for DatabaseInterface {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn change_root_directory(new_dir: String) {
|
||||
info!("Changed root directory to {}", new_dir);
|
||||
let mut lock = DATA_ROOT_DIR.lock().unwrap();
|
||||
*lock = new_dir.into();
|
||||
pub fn add_new_download_dir(new_dir: String) -> Result<(), String> {
|
||||
// Check the new directory is all good
|
||||
let new_dir_path = Path::new(&new_dir);
|
||||
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::db::{DatabaseImpls, DATA_ROOT_DIR};
|
||||
use crate::downloads::download_logic;
|
||||
use crate::db::DatabaseImpls;
|
||||
use crate::downloads::manifest::{DropDownloadContext, DropManifest};
|
||||
use crate::downloads::progress::ProgressChecker;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use atomic_counter::RelaxedCounter;
|
||||
use log::info;
|
||||
use rustix::fs::{fallocate, FallocateFlags};
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use rustix::fs::{fallocate, FallocateFlags};
|
||||
|
||||
use super::download_logic::download_game_chunk;
|
||||
|
||||
pub struct GameDownloadAgent {
|
||||
pub id: String,
|
||||
pub version: String,
|
||||
pub status: Arc<RwLock<GameDownloadState>>,
|
||||
pub control_flag: Arc<RwLock<DownloadThreadControlFlag>>,
|
||||
pub target_download_dir: usize,
|
||||
contexts: Mutex<Vec<DropDownloadContext>>,
|
||||
pub progress: ProgressChecker<DropDownloadContext>,
|
||||
// pub progress: ProgressChecker<DropDownloadContext>,
|
||||
pub manifest: Mutex<Option<DropManifest>>,
|
||||
pub progress: ProgressObject,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
pub enum GameDownloadState {
|
||||
Uninitialised,
|
||||
Queued,
|
||||
Paused,
|
||||
Manifest,
|
||||
Downloading,
|
||||
Finished,
|
||||
Stalled,
|
||||
Failed,
|
||||
Cancelled,
|
||||
pub enum DownloadThreadControlFlag {
|
||||
Go,
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum GameDownloadError {
|
||||
ManifestDownload,
|
||||
FailedContextGeneration,
|
||||
Status(u16),
|
||||
System(SystemError),
|
||||
CommunicationError(RemoteAccessError),
|
||||
ChecksumError,
|
||||
SetupError(String),
|
||||
LockError,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum SystemError {
|
||||
MutexLockFailed,
|
||||
|
||||
impl Display for GameDownloadError {
|
||||
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 {
|
||||
pub fn new(id: String, version: String) -> Self {
|
||||
let status = Arc::new(RwLock::new(GameDownloadState::Uninitialised));
|
||||
pub fn new(id: String, version: String, target_download_dir: usize) -> Self {
|
||||
// Don't run by default
|
||||
let status = Arc::new(RwLock::new(DownloadThreadControlFlag::Stop));
|
||||
Self {
|
||||
id,
|
||||
version,
|
||||
status: status.clone(),
|
||||
control_flag: status.clone(),
|
||||
manifest: Mutex::new(None),
|
||||
progress: ProgressChecker::new(
|
||||
Box::new(download_logic::download_game_chunk),
|
||||
Arc::new(RelaxedCounter::new(0)),
|
||||
status,
|
||||
0,
|
||||
),
|
||||
target_download_dir,
|
||||
contexts: Mutex::new(Vec::new()),
|
||||
progress: ProgressObject {
|
||||
max: 0,
|
||||
current: Arc::new(AtomicU64::new(0)),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub async fn queue(&self) -> Result<(), GameDownloadError> {
|
||||
self.change_state(GameDownloadState::Queued);
|
||||
if self.manifest.lock().unwrap().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
self.ensure_manifest_exists()
|
||||
pub fn set_control_flag(&self, flag: DownloadThreadControlFlag) {
|
||||
let mut lock = self.control_flag.write().unwrap();
|
||||
*lock = flag;
|
||||
}
|
||||
pub fn get_control_flag(&self) -> DownloadThreadControlFlag {
|
||||
let lock = self.control_flag.read().unwrap();
|
||||
lock.clone()
|
||||
}
|
||||
|
||||
pub fn begin_download(&self, max_threads: usize) -> Result<(), GameDownloadError> {
|
||||
self.change_state(GameDownloadState::Downloading);
|
||||
// TODO we're coping the whole context thing
|
||||
// It's not necessary, I just can't figure out to make the borrow checker happy
|
||||
{
|
||||
let lock = self.contexts.lock().unwrap().to_vec();
|
||||
self.progress.run_context_parallel(lock, max_threads);
|
||||
}
|
||||
// Blocking
|
||||
// Requires mutable self
|
||||
pub fn setup_download(&mut self) -> Result<(), GameDownloadError> {
|
||||
self.ensure_manifest_exists()?;
|
||||
|
||||
self.generate_contexts()?;
|
||||
|
||||
self.set_control_flag(DownloadThreadControlFlag::Go);
|
||||
|
||||
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() {
|
||||
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 manifest_url = base_url
|
||||
.join(
|
||||
@ -104,8 +129,6 @@ impl GameDownloadAgent {
|
||||
.unwrap();
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
info!("Generating & sending client");
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(manifest_url.to_string())
|
||||
@ -114,8 +137,14 @@ impl GameDownloadAgent {
|
||||
.unwrap();
|
||||
|
||||
if response.status() != 200 {
|
||||
info!("Error status: {}", response.status());
|
||||
return Err(GameDownloadError::Status(response.status().as_u16()));
|
||||
return Err(GameDownloadError::CommunicationError(
|
||||
format!(
|
||||
"Failed to download game manifest: {} {}",
|
||||
response.status(),
|
||||
response.text().unwrap()
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
let manifest_download = response.json::<DropManifest>().unwrap();
|
||||
@ -125,42 +154,33 @@ impl GameDownloadAgent {
|
||||
return chunk.lengths.iter().sum::<usize>();
|
||||
})
|
||||
.sum::<usize>();
|
||||
self.progress.set_capacity(length);
|
||||
self.progress.max = length.try_into().unwrap();
|
||||
|
||||
if let Ok(mut manifest) = self.manifest.lock() {
|
||||
*manifest = Some(manifest_download)
|
||||
} else {
|
||||
return Err(GameDownloadError::System(SystemError::MutexLockFailed));
|
||||
*manifest = Some(manifest_download);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
return Err(GameDownloadError::LockError);
|
||||
}
|
||||
|
||||
pub fn change_state(&self, state: GameDownloadState) {
|
||||
let mut lock = self.status.write().unwrap();
|
||||
*lock = state;
|
||||
}
|
||||
pub fn get_state(&self) -> GameDownloadState {
|
||||
let lock = self.status.read().unwrap();
|
||||
lock.clone()
|
||||
}
|
||||
pub fn generate_contexts(&self) -> Result<(), GameDownloadError> {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
let manifest = self.manifest.lock().unwrap().clone().unwrap();
|
||||
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 base_path = DATA_ROOT_DIR
|
||||
.lock()
|
||||
.unwrap()
|
||||
.join("games")
|
||||
.join(game_id.clone())
|
||||
.clone();
|
||||
let base_path = data_base_dir_path.join(game_id.clone()).clone();
|
||||
create_dir_all(base_path.clone()).unwrap();
|
||||
info!("Generating contexts");
|
||||
|
||||
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();
|
||||
create_dir_all(container).unwrap();
|
||||
@ -181,17 +201,44 @@ impl GameDownloadAgent {
|
||||
running_offset += *length as u64;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if running_offset > 0 {
|
||||
fallocate(file, FallocateFlags::empty(), 0, running_offset).unwrap();
|
||||
}
|
||||
}
|
||||
info!("Finished generating");
|
||||
|
||||
if let Ok(mut context_lock) = self.contexts.lock() {
|
||||
*context_lock = contexts;
|
||||
} else {
|
||||
return Err(GameDownloadError::FailedContextGeneration);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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::{
|
||||
borrow::Borrow,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use log::info;
|
||||
use rayon::spawn;
|
||||
|
||||
use crate::{downloads::download_agent::GameDownloadAgent, AppState};
|
||||
|
||||
use super::download_agent::{GameDownloadError, GameDownloadState};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn queue_game_download(
|
||||
pub fn download_game(
|
||||
game_id: String,
|
||||
game_version: String,
|
||||
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> {
|
||||
info!("called stop_specific_game_download");
|
||||
get_game_download(state, game_id).change_state(GameDownloadState::Cancelled);
|
||||
info!("beginning game download...");
|
||||
|
||||
//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(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_game_download_progress(
|
||||
pub fn get_game_download_progress(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
game_id: String,
|
||||
) -> Result<f64, String> {
|
||||
let progress = get_game_download(state, game_id)
|
||||
.progress
|
||||
.get_progress_percentage();
|
||||
info!("{}", progress);
|
||||
Ok(progress)
|
||||
let da = use_download_agent(state, game_id)?;
|
||||
|
||||
let progress = &da.progress;
|
||||
let current: f64 = 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]
|
||||
pub async fn set_download_state(
|
||||
fn use_download_agent(
|
||||
state: tauri::State<'_, Mutex<AppState>>,
|
||||
game_id: String,
|
||||
status: GameDownloadState,
|
||||
) -> 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> {
|
||||
) -> Result<Arc<GameDownloadAgent>, String> {
|
||||
let lock = state.lock().unwrap();
|
||||
let download_agent = lock.game_downloads.get(&game_id).unwrap();
|
||||
download_agent.clone()
|
||||
let download_agent = lock.game_downloads.get(&game_id).ok_or("Invalid game ID")?;
|
||||
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::downloads::manifest::DropDownloadContext;
|
||||
use crate::remote::RemoteAccessError;
|
||||
use crate::DB;
|
||||
use crate::{auth::generate_authorization_header, GAME_PAUSE_CHECK_INTERVAL};
|
||||
use atomic_counter::{AtomicCounter, RelaxedCounter};
|
||||
use log::{error, info};
|
||||
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::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, BufWriter, Error, ErrorKind, Seek, SeekFrom, Write},
|
||||
io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
thread::sleep,
|
||||
sync::Arc,
|
||||
};
|
||||
use urlencoding::encode;
|
||||
|
||||
use super::download_agent::GameDownloadState;
|
||||
use super::download_agent::{DownloadThreadControlFlag, GameDownloadError};
|
||||
|
||||
pub struct DropFileWriter {
|
||||
file: File,
|
||||
pub struct DropWriter<W: Write> {
|
||||
hasher: Context,
|
||||
progress: Arc<RelaxedCounter>,
|
||||
status: Arc<RwLock<GameDownloadState>>,
|
||||
destination: W,
|
||||
}
|
||||
impl DropFileWriter {
|
||||
fn new(
|
||||
path: PathBuf,
|
||||
status: Arc<RwLock<GameDownloadState>>,
|
||||
progress: Arc<RelaxedCounter>,
|
||||
) -> Self {
|
||||
impl DropWriter<File> {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self {
|
||||
file: OpenOptions::new().write(true).open(path).unwrap(),
|
||||
destination: OpenOptions::new().write(true).open(path).unwrap(),
|
||||
hasher: Context::new(),
|
||||
progress,
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> io::Result<Digest> {
|
||||
self.flush().unwrap();
|
||||
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
|
||||
impl Write for DropFileWriter {
|
||||
// Write automatically pushes to file and hasher
|
||||
impl Write for DropWriter<File> {
|
||||
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
|
||||
if let Some(value) = self.manage_state() {
|
||||
return value;
|
||||
}
|
||||
let len = buf.len();
|
||||
self.progress.add(len);
|
||||
|
||||
//info!("Writing data to writer");
|
||||
self.hasher.write_all(buf).unwrap();
|
||||
self.file.write(buf)
|
||||
self.hasher.write_all(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Unable to write to hasher: {}", e),
|
||||
)
|
||||
})?;
|
||||
self.destination.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
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> {
|
||||
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(
|
||||
ctx: DropDownloadContext,
|
||||
status: Arc<RwLock<GameDownloadState>>,
|
||||
progress: Arc<RelaxedCounter>,
|
||||
) {
|
||||
if *status.read().unwrap() == GameDownloadState::Cancelled {
|
||||
info!("Callback stopped download at start");
|
||||
return;
|
||||
control_flag: Arc<RwLock<DownloadThreadControlFlag>>,
|
||||
progress: Arc<AtomicU64>,
|
||||
) -> Result<bool, GameDownloadError> {
|
||||
// If we're paused
|
||||
if *control_flag.read().unwrap() == DownloadThreadControlFlag::Stop {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
@ -133,47 +147,48 @@ pub fn download_game_chunk(
|
||||
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let mut response = match client.get(chunk_url).header("Authorization", header).send() {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
info!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let response = client
|
||||
.get(chunk_url)
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.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 {
|
||||
file.seek(SeekFrom::Start(ctx.offset))
|
||||
destination
|
||||
.seek(SeekFrom::Start(ctx.offset))
|
||||
.expect("Failed to seek to file offset");
|
||||
}
|
||||
|
||||
// Writing everything to disk directly is probably slightly faster in terms of disk
|
||||
// speed because it balances out the writes, but this is better than the performance
|
||||
// loss from constantly reading the callbacks
|
||||
|
||||
let mut writer = BufWriter::with_capacity(1024 * 1024, file);
|
||||
|
||||
match io::copy(&mut response, &mut writer) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
info!("Copy errored with error {}", e)
|
||||
}
|
||||
let content_length = response.content_length();
|
||||
if content_length.is_none() {
|
||||
return Err(GameDownloadError::CommunicationError(
|
||||
RemoteAccessError::GenericErrror(
|
||||
"Invalid download endpoint, missing Content-Length header.".to_owned(),
|
||||
),
|
||||
));
|
||||
}
|
||||
writer.flush().unwrap();
|
||||
let file = match writer.into_inner() {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
error!("Failed to acquire writer from BufWriter");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut pipeline = DropDownloadPipeline::new(
|
||||
response,
|
||||
destination,
|
||||
control_flag,
|
||||
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 {
|
||||
info!(
|
||||
"Checksum failed. Original: {}, Calculated: {} for {}",
|
||||
ctx.checksum, res, ctx.file_name
|
||||
);
|
||||
return Err(GameDownloadError::ChecksumError);
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
pub mod download_agent;
|
||||
pub mod download_commands;
|
||||
mod download_logic;
|
||||
mod manifest;
|
||||
pub mod progress;
|
||||
mod manifest;
|
||||
@ -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::downloads::download_agent::GameDownloadAgent;
|
||||
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 env_logger::Env;
|
||||
use http::{header::*, response::Builder as ResponseBuilder};
|
||||
@ -119,13 +119,10 @@ pub fn run() {
|
||||
// Library
|
||||
fetch_library,
|
||||
fetch_game,
|
||||
change_root_directory,
|
||||
add_new_download_dir,
|
||||
// Downloads
|
||||
queue_game_download,
|
||||
start_game_downloads,
|
||||
cancel_specific_game_download,
|
||||
download_game,
|
||||
get_game_download_progress,
|
||||
set_download_state
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.setup(|app| {
|
||||
|
||||
Reference in New Issue
Block a user