diff --git a/pages/store/index.vue b/pages/store/index.vue index a236007..486b796 100644 --- a/pages/store/index.vue +++ b/pages/store/index.vue @@ -1,25 +1,38 @@ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d4d4a6a..622e924 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1039,6 +1039,7 @@ dependencies = [ "tauri-plugin-single-instance", "tokio", "url", + "urlencoding", "uuid", "versions", "webbrowser", @@ -4815,6 +4816,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 46dd52f..edbcbbb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ env_logger = "0.11.5" http = "1.1.0" tokio = { version = "1.40.0", features = ["rt", "tokio-macros"] } versions = { version = "6.3.2", features = ["serde"] } +urlencoding = "2.1.3" [dependencies.uuid] version = "1.10.0" diff --git a/src-tauri/src/downloads/download_files.rs b/src-tauri/src/downloads/download_files.rs deleted file mode 100644 index 97e3aea..0000000 --- a/src-tauri/src/downloads/download_files.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::fs::File; -use std::io::{Seek, SeekFrom, Write}; -use std::os::unix::fs::MetadataExt; -use std::sync::{Arc, Mutex}; -use log::info; -use uuid::Bytes; -use crate::auth::generate_authorization_header; -use crate::DB; -use crate::db::DatabaseImpls; -use crate::downloads::manifest::DropDownloadContext; - -const CHUNK_SIZE: u64 = 1024 * 1024 * 64; -pub fn download_game_chunk(ctx: DropDownloadContext) { - let base_url = DB.fetch_base_url(); - - - let client = reqwest::blocking::Client::new(); - let chunk_url = base_url.join( - &format!( - "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}", - ctx.game_id, - ctx.version, - ctx.file_name, - ctx.index - )).unwrap(); - - let header = generate_authorization_header(); - - let response = client - .get(chunk_url) - .header("Authorization", header) - .send() - .unwrap(); - let response_data = response.bytes().unwrap(); - - write_to_file(ctx.file, ctx.index as u64, response_data.to_vec()); -} - -fn write_to_file(file: Arc>, index: u64, data: Vec) { - let mut lock = file.lock().unwrap(); - - if index != 0 { - lock.seek(SeekFrom::Start(index * CHUNK_SIZE)).expect("Failed to seek to file offset"); - } - - lock.write_all(&data).unwrap(); -} \ No newline at end of file diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs new file mode 100644 index 0000000..bbd1c64 --- /dev/null +++ b/src-tauri/src/downloads/download_logic.rs @@ -0,0 +1,40 @@ +use crate::auth::generate_authorization_header; +use crate::db::DatabaseImpls; +use crate::downloads::manifest::DropDownloadContext; +use crate::DB; +use log::info; +use urlencoding::encode; +use std::io::{BufWriter, Seek, SeekFrom, Write}; + +pub fn download_game_chunk(ctx: DropDownloadContext) { + let base_url = DB.fetch_base_url(); + + let client = reqwest::blocking::Client::new(); + let chunk_url = base_url + .join(&format!( + "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}", + // Encode the parts we don't trust + ctx.game_id, encode(&ctx.version), encode(&ctx.file_name), ctx.index + )) + .unwrap(); + + let header = generate_authorization_header(); + + let mut response = client + .get(chunk_url) + .header("Authorization", header) + .send() + .unwrap(); + + let mut file_lock = ctx.file.lock().unwrap(); + + if ctx.offset != 0 { + file_lock + .seek(SeekFrom::Start(ctx.offset)) + .expect("Failed to seek to file offset"); + } + + let mut stream = BufWriter::with_capacity(1024, file_lock.try_clone().unwrap()); + + response.copy_to(&mut stream).unwrap(); +} diff --git a/src-tauri/src/downloads/game_download.rs b/src-tauri/src/downloads/download_manager.rs similarity index 67% rename from src-tauri/src/downloads/game_download.rs rename to src-tauri/src/downloads/download_manager.rs index 331c34d..be0be95 100644 --- a/src-tauri/src/downloads/game_download.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -1,19 +1,19 @@ -use std::fs::File; -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::sync::atomic::AtomicUsize; -use log::info; -use serde::{Deserialize, Serialize}; -use crate::{AppState, DB}; use crate::auth::generate_authorization_header; use crate::db::{DatabaseImpls, DATA_ROOT_DIR}; -use crate::downloads::download_files; +use crate::downloads::download_logic; use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::downloads::progress::ProgressChecker; +use crate::{AppState, DB}; +use log::info; +use serde::{Deserialize, Serialize}; +use std::fs::{create_dir_all, File}; +use std::path::Path; +use std::sync::atomic::AtomicUsize; +use std::sync::{Arc, Mutex}; #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct GameDownload { +pub struct GameDownloadManager { id: String, version: String, progress: Arc, @@ -40,14 +40,14 @@ pub enum GameDownloadError { } #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] pub enum SystemError { - MutexLockFailed + MutexLockFailed, } #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Ord, PartialOrd)] #[serde(rename_all = "camelCase")] pub struct GameChunkCtx { chunk_id: usize, } -impl GameDownload { +impl GameDownloadManager { pub fn new(id: String, version: String) -> Self { Self { id, @@ -64,11 +64,16 @@ impl GameDownload { } self.ensure_manifest_exists().await } - pub async fn download(&self, max_threads: usize, contexts: Vec) -> Result<(), GameDownloadError> { + pub fn begin_download( + &self, + max_threads: usize, + contexts: Vec, + ) -> Result<(), GameDownloadError> { let progress = Arc::new(AtomicUsize::new(0)); self.change_state(GameDownloadState::Downloading); - let progress = ProgressChecker::new(Box::new(download_files::download_game_chunk), progress); - progress.run_contexts_parallel_async(contexts, max_threads).await; + let progress = + ProgressChecker::new(Box::new(download_logic::download_game_chunk), progress); + progress.run_contexts_parallel(contexts, max_threads); Ok(()) } pub async fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> { @@ -85,10 +90,9 @@ impl GameDownload { .join( format!( "/api/v1/client/metadata/manifest?id={}&version={}", - self.id, - self.version + self.id, self.version ) - .as_str() + .as_str(), ) .unwrap(); @@ -109,10 +113,11 @@ impl GameDownload { } let manifest_download = response.json::().await.unwrap(); - info!("Manifest: {:?}", manifest_download); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download) - } else { return Err(GameDownloadError::System(SystemError::MutexLockFailed)); } + } else { + return Err(GameDownloadError::System(SystemError::MutexLockFailed)); + } Ok(()) } @@ -122,26 +127,38 @@ impl GameDownload { *lock = state; } } -pub fn to_contexts(manifest: &DropManifest, version: String, game_id: String) -> Vec { +pub fn generate_job_contexts( + manifest: &DropManifest, + version: String, + game_id: String, +) -> Vec { let mut contexts = Vec::new(); - let base_path = DATA_ROOT_DIR.clone(); - for key in manifest { - let path = base_path.join(Path::new(key.0)); - let file = Arc::new(Mutex::new(File::create(path).unwrap())); - for i in 0..key.1.ids.len() { - contexts.push(DropDownloadContext { - file_chunk: Arc::new(key.1.clone()), + let base_path = DATA_ROOT_DIR.join("games").join(game_id.clone()).clone(); + create_dir_all(base_path.clone()).unwrap(); + for (raw_path, chunk) in manifest { + let path = base_path.join(Path::new(raw_path)); - file_name: key.0.clone(), + let container = path.parent().unwrap(); + create_dir_all(container).unwrap(); + + let file = Arc::new(Mutex::new(File::create(path).unwrap())); + let mut running_offset = 0; + + for i in 0..chunk.ids.len() { + if i == 1 { + info!("woah a chunk bigger than 1") + } + contexts.push(DropDownloadContext { + file_name: raw_path.to_string(), version: version.to_string(), + offset: running_offset, index: i, game_id: game_id.to_string(), file: file.clone(), }); - + running_offset += chunk.lengths[i] as u64; } } - info!("Contexts: {:?}", contexts); contexts } @@ -154,40 +171,21 @@ pub async fn start_game_download( ) -> Result<(), GameDownloadError> { info!("Triggered Game Download"); - let download = Arc::new(GameDownload::new(game_id.clone(), game_version.clone())); + let download_manager = Arc::new(GameDownloadManager::new( + game_id.clone(), + game_version.clone(), + )); - download.ensure_manifest_exists().await?; + download_manager.ensure_manifest_exists().await?; let local_manifest = { - let manifest = download.manifest.lock().unwrap(); + let manifest = download_manager.manifest.lock().unwrap(); (*manifest).clone().unwrap() }; - let contexts = to_contexts(&local_manifest, game_version.clone(), game_id); + let contexts = generate_job_contexts(&local_manifest, game_version.clone(), game_id); - let _ = download.download(max_threads, contexts).await; + let _ = download_manager.begin_download(max_threads, contexts); Ok(()) - - /* - let Some(unlocked) = manifest else { return Err(GameDownloadError::ManifestDoesNotExist) }; - let lock = unlocked.lock().unwrap(); - - let chunks = lock.parse_to_chunks(); - - /* - let manifest = match d.manifest { - Some(lock) => { - let lock = lock.lock().unwrap(); - lock.parse_to_chunks() - }, - None => { return Err(GameDownloadError::ManifestDoesNotExist) } - }; - */ - - app_state.game_downloads.push(download.clone()); - download.download(max_threads, chunks).await - - */ } - diff --git a/src-tauri/src/downloads/downloads.rs b/src-tauri/src/downloads/downloads.rs deleted file mode 100644 index 8850ac6..0000000 --- a/src-tauri/src/downloads/downloads.rs +++ /dev/null @@ -1,11 +0,0 @@ -/* GENERAL OUTLINE -When downloading any game, the following details must be provided to the server: - - Game ID - - User token - - TBC - -The steps to then download a game are as follows: - 1. User requests - */ - - diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/downloads/manifest.rs index a646179..7feb45c 100644 --- a/src-tauri/src/downloads/manifest.rs +++ b/src-tauri/src/downloads/manifest.rs @@ -14,10 +14,10 @@ pub struct DropChunk { #[derive(Debug, Clone)] pub struct DropDownloadContext { - pub file_chunk: Arc, pub file_name: String, pub version: String, pub index: usize, + pub offset: u64, pub game_id: String, pub file: Arc> } \ No newline at end of file diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 907902b..a3129e7 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,5 +1,4 @@ -mod downloads; mod manifest; pub mod progress; -pub mod game_download; -mod download_files; \ No newline at end of file +pub mod download_manager; +mod download_logic; \ No newline at end of file diff --git a/src-tauri/src/downloads/progress.rs b/src-tauri/src/downloads/progress.rs index e8176f9..6194a90 100644 --- a/src-tauri/src/downloads/progress.rs +++ b/src-tauri/src/downloads/progress.rs @@ -30,19 +30,6 @@ where T: Send + Sync self.counter.fetch_add(1, Ordering::Relaxed); } } - pub async fn run_contexts_parallel_async(&self, contexts: Vec, max_threads: usize) { - let threads = ThreadPoolBuilder::new() - // If max_threads == 0, then the limit will be determined - // by Rayon's internal RAYON_NUM_THREADS - .num_threads(max_threads) - .build() - .unwrap(); - - for context in contexts { - let f = self.f.clone(); - threads.spawn(move || f(context)); - } - } pub fn run_contexts_parallel(&self, contexts: Vec, max_threads: usize) { let threads = ThreadPoolBuilder::new() // If max_threads == 0, then the limit will be determined diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b4043c9..a793c0f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,7 +22,7 @@ use std::{ use std::sync::Arc; use tauri_plugin_deep_link::DeepLinkExt; use crate::db::DatabaseImpls; -use crate::downloads::game_download::{start_game_download, GameDownload}; +use crate::downloads::download_manager::{start_game_download, GameDownloadManager}; #[derive(Clone, Copy, Serialize)] pub enum AppStatus { @@ -47,7 +47,7 @@ pub struct AppState { status: AppStatus, user: Option, games: HashMap, - game_downloads: Vec> + game_downloads: Vec> } #[tauri::command]