From 16365713cf64f32ab65ab6edebad78cf8cd4663e Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 9 Aug 2025 15:50:21 +1000 Subject: [PATCH 01/13] v2 download API and fixes (#112) * fix: potential download fixes * fix: show installed games not on remote * fix: more download_logic error handling * partial: move to async * feat: interactivity improvements * feat: v2 download API * fix: download seek offsets * fix: clippy * fix: apply clippy suggestion * fix: performance improvements starting up download * fix: finished bucket file * fix: ui tweaks and fixes * fix: revert version to 0.3.2 * fix: clippy --- main/components/LibrarySearch.vue | 44 ++- main/composables/game.ts | 1 + main/package.json | 2 +- main/pages/library/[id]/index.vue | 30 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/build.rs | 1 + src-tauri/src/database/models.rs | 1 + .../download_manager_builder.rs | 19 +- .../download_manager/util/progress_object.rs | 6 +- .../src/error/application_download_error.rs | 4 +- src-tauri/src/games/collections/commands.rs | 95 +++-- src-tauri/src/games/commands.rs | 20 +- src-tauri/src/games/downloads/commands.rs | 69 ++-- .../src/games/downloads/download_agent.rs | 333 +++++++++++------- .../src/games/downloads/download_logic.rs | 210 ++++++----- src-tauri/src/games/downloads/drop_data.rs | 8 - src-tauri/src/games/downloads/manifest.rs | 83 ++++- src-tauri/src/games/downloads/validate.rs | 18 +- src-tauri/src/games/library.rs | 177 +++++----- src-tauri/src/lib.rs | 200 ++++++----- src-tauri/src/remote/auth.rs | 54 +-- src-tauri/src/remote/cache.rs | 17 +- src-tauri/src/remote/commands.rs | 34 +- src-tauri/src/remote/requests.rs | 26 +- src-tauri/src/remote/server_proto.rs | 4 +- src-tauri/tauri.conf.json | 2 +- 27 files changed, 859 insertions(+), 603 deletions(-) diff --git a/main/components/LibrarySearch.vue b/main/components/LibrarySearch.vue index cee9103..17a44e9 100644 --- a/main/components/LibrarySearch.vue +++ b/main/components/LibrarySearch.vue @@ -1,5 +1,5 @@ @@ -80,12 +104,12 @@ import { listen } from "@tauri-apps/api/event"; // Style information const gameStatusTextStyle: { [key in GameStatusEnum]: string } = { [GameStatusEnum.Installed]: "text-green-500", - [GameStatusEnum.Downloading]: "text-blue-500", + [GameStatusEnum.Downloading]: "text-zinc-400", [GameStatusEnum.Validating]: "text-blue-300", [GameStatusEnum.Running]: "text-green-500", [GameStatusEnum.Remote]: "text-zinc-500", - [GameStatusEnum.Queued]: "text-blue-500", - [GameStatusEnum.Updating]: "text-blue-500", + [GameStatusEnum.Queued]: "text-zinc-400", + [GameStatusEnum.Updating]: "text-zinc-400", [GameStatusEnum.Uninstalling]: "text-zinc-100", [GameStatusEnum.SetupRequired]: "text-yellow-500", [GameStatusEnum.PartiallyInstalled]: "text-gray-400", @@ -107,6 +131,7 @@ const router = useRouter(); const searchQuery = ref(""); +const loading = ref(false); const games: { [key: string]: { game: Game; status: Ref }; } = {}; @@ -115,7 +140,10 @@ const icons: { [key: string]: string } = {}; const rawGames: Ref = ref([]); async function calculateGames(clearAll = false) { - if (clearAll) rawGames.value = []; + if (clearAll) { + rawGames.value = []; + loading.value = true; + } // If we update immediately, the navigation gets re-rendered before we // add all the necessary state, and it freaks tf out const newGames = await invoke("fetch_library"); @@ -127,10 +155,11 @@ async function calculateGames(clearAll = false) { if (icons[game.id]) continue; icons[game.id] = await useObject(game.mIconObjectId); } + loading.value = false; rawGames.value = newGames; } -await calculateGames(); +calculateGames(true); const navigation = computed(() => rawGames.value.map((game) => { @@ -138,8 +167,7 @@ const navigation = computed(() => const isInstalled = computed( () => - status.value.type == GameStatusEnum.Installed || - status.value.type == GameStatusEnum.SetupRequired + status.value.type != GameStatusEnum.Remote ); const item = { diff --git a/main/composables/game.ts b/main/composables/game.ts index e57eb8c..71319bd 100644 --- a/main/composables/game.ts +++ b/main/composables/game.ts @@ -43,6 +43,7 @@ export const useGame = async (gameId: string) => { gameStatusRegistry[gameId] = ref(parseStatus(data.status)); listen(`update_game/${gameId}`, (event) => { + console.log(event); const payload: { status: SerializedGameStatus; version?: GameVersion; diff --git a/main/package.json b/main/package.json index f620973..2334242 100644 --- a/main/package.json +++ b/main/package.json @@ -1,7 +1,7 @@ { "name": "view", "private": true, - "version": "0.3.1", + "version": "0.3.2", "type": "module", "scripts": { "build": "nuxt generate", diff --git a/main/pages/library/[id]/index.vue b/main/pages/library/[id]/index.vue index c55b992..5fc30fc 100644 --- a/main/pages/library/[id]/index.vue +++ b/main/pages/library/[id]/index.vue @@ -243,7 +243,10 @@ -
+
+
+
+ + Loading... +
+
, // Guaranteed to exist if the game also exists in the app state map pub game_statuses: HashMap, + pub game_versions: HashMap>, pub installed_game_version: HashMap, diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs index 7d8aeed..518550b 100644 --- a/src-tauri/src/download_manager/download_manager_builder.rs +++ b/src-tauri/src/download_manager/download_manager_builder.rs @@ -128,7 +128,7 @@ impl DownloadManagerBuilder { drop(download_thread_lock); } - fn stop_and_wait_current_download(&self) { + fn stop_and_wait_current_download(&self) -> bool { self.set_status(DownloadManagerStatus::Paused); if let Some(current_flag) = &self.active_control_flag { current_flag.set(DownloadThreadControlFlag::Stop); @@ -136,8 +136,10 @@ impl DownloadManagerBuilder { let mut download_thread_lock = self.current_download_thread.lock().unwrap(); if let Some(current_download_thread) = download_thread_lock.take() { - current_download_thread.join().unwrap(); - } + return current_download_thread.join().is_ok(); + }; + + true } fn manage_queue(mut self) -> Result<(), ()> { @@ -254,12 +256,16 @@ impl DownloadManagerBuilder { } }; - // If the download gets cancelled + // If the download gets canceled // immediately return, on_cancelled gets called for us earlier if !download_result { return; } + if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop { + return; + } + let validate_result = match download_agent.validate(&app_handle) { Ok(v) => v, Err(e) => { @@ -274,6 +280,10 @@ impl DownloadManagerBuilder { } }; + if download_agent.control_flag().get() == DownloadThreadControlFlag::Stop { + return; + } + if validate_result { download_agent.on_complete(&app_handle); sender @@ -315,6 +325,7 @@ impl DownloadManagerBuilder { self.stop_and_wait_current_download(); self.remove_and_cleanup_front_download(¤t_agent.metadata()); + self.push_ui_queue_update(); } self.set_status(DownloadManagerStatus::Error); } diff --git a/src-tauri/src/download_manager/util/progress_object.rs b/src-tauri/src/download_manager/util/progress_object.rs index 2f0bb5d..c2c0c2f 100644 --- a/src-tauri/src/download_manager/util/progress_object.rs +++ b/src-tauri/src/download_manager/util/progress_object.rs @@ -1,8 +1,8 @@ use std::{ sync::{ + Arc, Mutex, atomic::{AtomicUsize, Ordering}, mpsc::Sender, - Arc, Mutex, }, time::{Duration, Instant}, }; @@ -23,7 +23,7 @@ pub struct ProgressObject { //last_update: Arc>, last_update_time: Arc, bytes_last_update: Arc, - rolling: RollingProgressWindow<250>, + rolling: RollingProgressWindow<1>, } #[derive(Clone)] @@ -128,7 +128,7 @@ pub fn calculate_update(progress: &ProgressObject) { .bytes_last_update .swap(current_bytes_downloaded, Ordering::Acquire); - let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update; + let bytes_since_last_update = current_bytes_downloaded.saturating_sub(bytes_at_last_update); let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1); diff --git a/src-tauri/src/error/application_download_error.rs b/src-tauri/src/error/application_download_error.rs index a6294ea..72eb55d 100644 --- a/src-tauri/src/error/application_download_error.rs +++ b/src-tauri/src/error/application_download_error.rs @@ -11,6 +11,7 @@ use super::remote_access_error::RemoteAccessError; // TODO: Rename / separate from downloads #[derive(Debug, SerializeDisplay)] pub enum ApplicationDownloadError { + NotInitialized, Communication(RemoteAccessError), DiskFull(u64, u64), Checksum, @@ -22,6 +23,7 @@ pub enum ApplicationDownloadError { impl Display for ApplicationDownloadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { + ApplicationDownloadError::NotInitialized => write!(f, "Download not initalized, did something go wrong?"), ApplicationDownloadError::DiskFull(required, available) => write!( f, "Game requires {}, {} remaining left on disk.", @@ -39,7 +41,7 @@ impl Display for ApplicationDownloadError { ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"), ApplicationDownloadError::DownloadError => write!( f, - "download failed. See Download Manager status for specific error" + "Download failed. See Download Manager status for specific error" ), } } diff --git a/src-tauri/src/games/collections/commands.rs b/src-tauri/src/games/collections/commands.rs index bff8928..a09d441 100644 --- a/src-tauri/src/games/collections/commands.rs +++ b/src-tauri/src/games/collections/commands.rs @@ -1,109 +1,96 @@ use serde_json::json; -use url::Url; use crate::{ - DB, - database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, - remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC}, + remote::{ + auth::generate_authorization_header, + requests::{generate_url, make_authenticated_get}, + utils::DROP_CLIENT_ASYNC, + }, }; use super::collection::{Collection, Collections}; #[tauri::command] -pub fn fetch_collections() -> Result { - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| { - r.header("Authorization", generate_authorization_header()) - })? - .send()?; +pub async fn fetch_collections() -> Result { + let response = + make_authenticated_get(generate_url(&["/api/v1/client/collection"], &[])?).await?; - Ok(response.json()?) + Ok(response.json().await?) } #[tauri::command] -pub fn fetch_collection(collection_id: String) -> Result { - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request( - &client, +pub async fn fetch_collection(collection_id: String) -> Result { + let response = make_authenticated_get(generate_url( &["/api/v1/client/collection/", &collection_id], &[], - |r| r.header("Authorization", generate_authorization_header()), - )? - .send()?; + )?) + .await?; - Ok(response.json()?) + Ok(response.json().await?) } #[tauri::command] -pub fn create_collection(name: String) -> Result { - let client = DROP_CLIENT_SYNC.clone(); - let base_url = DB.fetch_base_url(); - - let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?; +pub async fn create_collection(name: String) -> Result { + let client = DROP_CLIENT_ASYNC.clone(); + let url = generate_url(&["/api/v1/client/collection"], &[])?; let response = client - .post(base_url) + .post(url) .header("Authorization", generate_authorization_header()) .json(&json!({"name": name})) - .send()?; + .send() + .await?; - Ok(response.json()?) + Ok(response.json().await?) } #[tauri::command] -pub fn add_game_to_collection( +pub async fn add_game_to_collection( collection_id: String, game_id: String, ) -> Result<(), RemoteAccessError> { - let client = DROP_CLIENT_SYNC.clone(); - let url = Url::parse(&format!( - "{}api/v1/client/collection/{}/entry/", - DB.fetch_base_url(), - collection_id - ))?; + let client = DROP_CLIENT_ASYNC.clone(); + + let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?; client .post(url) .header("Authorization", generate_authorization_header()) .json(&json!({"id": game_id})) - .send()?; + .send() + .await?; Ok(()) } #[tauri::command] -pub fn delete_collection(collection_id: String) -> Result { - let client = DROP_CLIENT_SYNC.clone(); - let base_url = Url::parse(&format!( - "{}api/v1/client/collection/{}", - DB.fetch_base_url(), - collection_id - ))?; +pub async fn delete_collection(collection_id: String) -> Result { + let client = DROP_CLIENT_ASYNC.clone(); + + let url = generate_url(&["/api/v1/client/collection", &collection_id], &[])?; let response = client - .delete(base_url) + .delete(url) .header("Authorization", generate_authorization_header()) - .send()?; + .send() + .await?; - Ok(response.json()?) + Ok(response.json().await?) } #[tauri::command] -pub fn delete_game_in_collection( +pub async fn delete_game_in_collection( collection_id: String, game_id: String, ) -> Result<(), RemoteAccessError> { - let client = DROP_CLIENT_SYNC.clone(); - let base_url = Url::parse(&format!( - "{}api/v1/client/collection/{}/entry", - DB.fetch_base_url(), - collection_id - ))?; + let client = DROP_CLIENT_ASYNC.clone(); + + let url = generate_url(&["/api/v1/client/collection", &collection_id, "entry"], &[])?; client - .delete(base_url) + .delete(url) .header("Authorization", generate_authorization_header()) .json(&json!({"id": game_id})) - .send()?; + .send().await?; Ok(()) } diff --git a/src-tauri/src/games/commands.rs b/src-tauri/src/games/commands.rs index c26e4c6..b99084a 100644 --- a/src-tauri/src/games/commands.rs +++ b/src-tauri/src/games/commands.rs @@ -18,28 +18,28 @@ use crate::{ use super::{ library::{ - FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic, + FetchGameStruct, Game, fetch_game_logic, fetch_game_version_options_logic, fetch_library_logic, }, state::{GameStatusManager, GameStatusWithTransient}, }; #[tauri::command] -pub fn fetch_library( - state: tauri::State<'_, Mutex>, +pub async fn fetch_library( + state: tauri::State<'_, Mutex>>, ) -> Result, RemoteAccessError> { offline!( state, fetch_library_logic, fetch_library_logic_offline, state - ) + ).await } #[tauri::command] -pub fn fetch_game( +pub async fn fetch_game( game_id: String, - state: tauri::State<'_, Mutex>, + state: tauri::State<'_, Mutex>>, ) -> Result { offline!( state, @@ -47,7 +47,7 @@ pub fn fetch_game( fetch_game_logic_offline, game_id, state - ) + ).await } #[tauri::command] @@ -68,9 +68,9 @@ pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), Libr } #[tauri::command] -pub fn fetch_game_verion_options( +pub async fn fetch_game_version_options( game_id: String, - state: tauri::State<'_, Mutex>, + state: tauri::State<'_, Mutex>>, ) -> Result, RemoteAccessError> { - fetch_game_verion_options_logic(game_id, state) + fetch_game_version_options_logic(game_id, state).await } diff --git a/src-tauri/src/games/downloads/commands.rs b/src-tauri/src/games/downloads/commands.rs index 0fa6df9..83aee8e 100644 --- a/src-tauri/src/games/downloads/commands.rs +++ b/src-tauri/src/games/downloads/commands.rs @@ -3,44 +3,47 @@ use std::{ sync::{Arc, Mutex}, }; + use crate::{ - database::{db::borrow_db_checked, models::data::GameDownloadStatus}, - download_manager:: - downloadable::Downloadable - , - error::application_download_error::ApplicationDownloadError, AppState, + database::{ + db::borrow_db_checked, + models::data::GameDownloadStatus, + }, + download_manager::downloadable::Downloadable, + error::application_download_error::ApplicationDownloadError, }; use super::download_agent::GameDownloadAgent; #[tauri::command] -pub fn download_game( +pub async fn download_game( game_id: String, game_version: String, install_dir: usize, - state: tauri::State<'_, Mutex>, + state: tauri::State<'_, Mutex>>, ) -> Result<(), ApplicationDownloadError> { - let sender = state.lock().unwrap().download_manager.get_sender(); - let game_download_agent = GameDownloadAgent::new_from_index( - game_id, - game_version, - install_dir, - sender, - )?; - let game_download_agent = Arc::new(Box::new(game_download_agent) as Box); + let sender = { state.lock().unwrap().download_manager.get_sender().clone() }; + + let game_download_agent = + GameDownloadAgent::new_from_index(game_id.clone(), game_version.clone(), install_dir, sender).await?; + + let game_download_agent = + Arc::new(Box::new(game_download_agent) as Box); state - .lock() - .unwrap() - .download_manager - .queue_download(game_download_agent).unwrap(); + .lock() + .unwrap() + .download_manager + .queue_download(game_download_agent.clone()) + .unwrap(); + Ok(()) } #[tauri::command] -pub fn resume_download( +pub async fn resume_download( game_id: String, - state: tauri::State<'_, Mutex>, + state: tauri::State<'_, Mutex>>, ) -> Result<(), ApplicationDownloadError> { let s = borrow_db_checked() .applications @@ -62,17 +65,21 @@ pub fn resume_download( let sender = state.lock().unwrap().download_manager.get_sender(); let parent_dir: PathBuf = install_dir.into(); - let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new( - game_id, - version_name.clone(), - parent_dir.parent().unwrap().to_path_buf(), - sender, - )?) as Box); + let game_download_agent = Arc::new(Box::new( + GameDownloadAgent::new( + game_id, + version_name.clone(), + parent_dir.parent().unwrap().to_path_buf(), + sender, + ) + .await?, + ) as Box); state - .lock() - .unwrap() - .download_manager - .queue_download(game_download_agent).unwrap(); + .lock() + .unwrap() + .download_manager + .queue_download(game_download_agent) + .unwrap(); Ok(()) } diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs index 7946048..2098490 100644 --- a/src-tauri/src/games/downloads/download_agent.rs +++ b/src-tauri/src/games/downloads/download_agent.rs @@ -11,13 +11,15 @@ use crate::download_manager::util::download_thread_control_flag::{ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject}; use crate::error::application_download_error::ApplicationDownloadError; use crate::error::remote_access_error::RemoteAccessError; -use crate::games::downloads::manifest::{DropDownloadContext, DropManifest}; +use crate::games::downloads::manifest::{ + DownloadBucket, DownloadContext, DownloadDrop, DropManifest, DropValidateContext, ManifestBody, +}; use crate::games::downloads::validate::validate_game_chunk; use crate::games::library::{on_game_complete, push_game_update, set_partially_installed}; use crate::games::state::GameStatusManager; use crate::process::utils::get_disk_available; -use crate::remote::requests::make_request; -use crate::remote::utils::DROP_CLIENT_SYNC; +use crate::remote::requests::generate_url; +use crate::remote::utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}; use log::{debug, error, info, warn}; use rayon::ThreadPoolBuilder; use std::collections::HashMap; @@ -31,16 +33,18 @@ use tauri::{AppHandle, Emitter}; #[cfg(target_os = "linux")] use rustix::fs::{FallocateFlags, fallocate}; -use super::download_logic::download_game_chunk; +use super::download_logic::download_game_bucket; use super::drop_data::DropData; static RETRY_COUNT: usize = 3; +const TARGET_BUCKET_SIZE: usize = 63 * 1000 * 1000; + pub struct GameDownloadAgent { pub id: String, pub version: String, pub control_flag: DownloadThreadControl, - contexts: Mutex>, + buckets: Mutex>, context_map: Mutex>, pub manifest: Mutex>, pub progress: Arc, @@ -50,19 +54,21 @@ pub struct GameDownloadAgent { } impl GameDownloadAgent { - pub fn new_from_index( + pub async fn new_from_index( id: String, version: String, target_download_dir: usize, sender: Sender, ) -> Result { - let db_lock = borrow_db_checked(); - let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); - drop(db_lock); + let base_dir = { + let db_lock = borrow_db_checked(); - Self::new(id, version, base_dir, sender) + db_lock.applications.install_dirs[target_download_dir].clone() + }; + + Self::new(id, version, base_dir, sender).await } - pub fn new( + pub async fn new( id: String, version: String, base_dir: PathBuf, @@ -82,7 +88,7 @@ impl GameDownloadAgent { version, control_flag, manifest: Mutex::new(None), - contexts: Mutex::new(Vec::new()), + buckets: Mutex::new(Vec::new()), context_map: Mutex::new(HashMap::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, @@ -90,7 +96,7 @@ impl GameDownloadAgent { status: Mutex::new(DownloadStatus::Queued), }; - result.ensure_manifest_exists()?; + result.ensure_manifest_exists().await?; let required_space = result .manifest @@ -100,8 +106,7 @@ impl GameDownloadAgent { .unwrap() .values() .map(|e| e.lengths.iter().sum::()) - .sum::() - as u64; + .sum::() as u64; let available_space = get_disk_available(data_base_dir_path)? as u64; @@ -117,26 +122,25 @@ impl GameDownloadAgent { // Blocking pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> { - self.ensure_manifest_exists()?; + let mut db_lock = borrow_db_mut_checked(); + let status = ApplicationTransientStatus::Downloading { + version_name: self.version.clone(), + }; + db_lock + .applications + .transient_statuses + .insert(self.metadata(), status.clone()); + // Don't use GameStatusManager because this game isn't installed + push_game_update(app_handle, &self.metadata().id, None, (None, Some(status))); - self.ensure_contexts()?; + if !self.check_manifest_exists() { + return Err(ApplicationDownloadError::NotInitialized); + } + + self.ensure_buckets()?; self.control_flag.set(DownloadThreadControlFlag::Go); - let mut db_lock = borrow_db_mut_checked(); - db_lock.applications.transient_statuses.insert( - self.metadata(), - ApplicationTransientStatus::Downloading { - version_name: self.version.clone(), - }, - ); - push_game_update( - app_handle, - &self.metadata().id, - None, - GameStatusManager::fetch_state(&self.metadata().id, &db_lock), - ); - Ok(()) } @@ -147,9 +151,7 @@ impl GameDownloadAgent { info!("beginning download for {}...", self.metadata().id); - let res = self - .run() - .map_err(|()| ApplicationDownloadError::DownloadError); + let res = self.run().map_err(ApplicationDownloadError::Communication); debug!( "{} took {}ms to download", @@ -159,37 +161,43 @@ impl GameDownloadAgent { res } - pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> { + pub fn check_manifest_exists(&self) -> bool { + self.manifest.lock().unwrap().is_some() + } + + pub async fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> { if self.manifest.lock().unwrap().is_some() { return Ok(()); } - self.download_manifest() + self.download_manifest().await } - fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { - let header = generate_authorization_header(); - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request( - &client, + async fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { + let client = DROP_CLIENT_ASYNC.clone(); + let url = generate_url( &["/api/v1/client/game/manifest"], &[("id", &self.id), ("version", &self.version)], - |f| f.header("Authorization", header), ) - .map_err(ApplicationDownloadError::Communication)? - .send() - .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; + .map_err(ApplicationDownloadError::Communication)?; + + let response = client + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .await + .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; if response.status() != 200 { return Err(ApplicationDownloadError::Communication( RemoteAccessError::ManifestDownloadFailed( response.status(), - response.text().unwrap(), + response.text().await.unwrap(), ), )); } - let manifest_download: DropManifest = response.json().unwrap(); + let manifest_download: DropManifest = response.json().await.unwrap(); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); @@ -201,20 +209,23 @@ impl GameDownloadAgent { // Sets it up for both download and validate fn setup_progress(&self) { - let contexts = self.contexts.lock().unwrap(); + let buckets = self.buckets.lock().unwrap(); - let length = contexts.len(); + let chunk_count = buckets.iter().map(|e| e.drops.len()).sum(); - let chunk_count = contexts.iter().map(|chunk| chunk.length).sum(); + let total_length = buckets + .iter() + .map(|bucket| bucket.drops.iter().map(|e| e.length).sum::()) + .sum(); - self.progress.set_max(chunk_count); - self.progress.set_size(length); + self.progress.set_max(total_length); + self.progress.set_size(chunk_count); self.progress.reset(); } - pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> { - if self.contexts.lock().unwrap().is_empty() { - self.generate_contexts()?; + pub fn ensure_buckets(&self) -> Result<(), ApplicationDownloadError> { + if self.buckets.lock().unwrap().is_empty() { + self.generate_buckets()?; } *self.context_map.lock().unwrap() = self.dropdata.get_contexts(); @@ -222,14 +233,22 @@ impl GameDownloadAgent { Ok(()) } - pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> { + pub fn generate_buckets(&self) -> Result<(), ApplicationDownloadError> { let manifest = self.manifest.lock().unwrap().clone().unwrap(); let game_id = self.id.clone(); - let mut contexts = Vec::new(); let base_path = Path::new(&self.dropdata.base_path); create_dir_all(base_path).unwrap(); + let mut buckets = Vec::new(); + + let mut current_bucket = DownloadBucket { + game_id: game_id.clone(), + version: self.version.clone(), + drops: Vec::new(), + }; + let mut current_bucket_size = 0; + for (raw_path, chunk) in manifest { let path = base_path.join(Path::new(&raw_path)); @@ -244,42 +263,79 @@ impl GameDownloadAgent { .truncate(false) .open(path.clone()) .unwrap(); - let mut running_offset = 0; + let mut file_running_offset = 0; for (index, length) in chunk.lengths.iter().enumerate() { - contexts.push(DropDownloadContext { - file_name: raw_path.to_string(), - version: chunk.version_name.to_string(), - offset: running_offset, - index, - game_id: game_id.to_string(), - path: path.clone(), - checksum: chunk.checksums[index].clone(), + let drop = DownloadDrop { + filename: raw_path.to_string(), + start: file_running_offset, length: *length, + checksum: chunk.checksums[index].clone(), permissions: chunk.permissions, - }); - running_offset += *length as u64; + path: path.clone(), + index, + }; + file_running_offset += *length; + + if *length >= TARGET_BUCKET_SIZE { + // They get their own bucket + + buckets.push(DownloadBucket { + game_id: game_id.clone(), + version: self.version.clone(), + drops: vec![drop], + }); + + continue; + } + + if current_bucket_size + *length >= TARGET_BUCKET_SIZE + && !current_bucket.drops.is_empty() + { + // Move current bucket into list and make a new one + buckets.push(current_bucket); + current_bucket = DownloadBucket { + game_id: game_id.clone(), + version: self.version.clone(), + drops: Vec::new(), + }; + current_bucket_size = 0; + } + + current_bucket.drops.push(drop); + current_bucket_size += *length; } #[cfg(target_os = "linux")] - if running_offset > 0 && !already_exists { - let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); + if file_running_offset > 0 && !already_exists { + let _ = fallocate(file, FallocateFlags::empty(), 0, file_running_offset as u64); } } - let existing_contexts = self.dropdata.get_completed_contexts(); + + if !current_bucket.drops.is_empty() { + buckets.push(current_bucket); + } + + info!("buckets: {}", buckets.len()); + + let existing_contexts = self.dropdata.get_contexts(); self.dropdata.set_contexts( - &contexts + &buckets .iter() - .map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum))) + .flat_map(|x| x.drops.iter().map(|v| v.checksum.clone())) + .map(|x| { + let contains = existing_contexts.get(&x).unwrap_or(&false); + (x, *contains) + }) .collect::>(), ); - *self.contexts.lock().unwrap() = contexts; + *self.buckets.lock().unwrap() = buckets; Ok(()) } - fn run(&self) -> Result { + fn run(&self) -> Result { self.setup_progress(); let max_download_threads = borrow_db_checked().settings.max_download_threads; @@ -295,78 +351,81 @@ impl GameDownloadAgent { let completed_contexts = Arc::new(boxcar::Vec::new()); let completed_indexes_loop_arc = completed_contexts.clone(); - let contexts = self.contexts.lock().unwrap(); + let download_context = DROP_CLIENT_SYNC + .post(generate_url(&["/api/v2/client/context"], &[]).unwrap()) + .json(&ManifestBody { + game: self.id.clone(), + version: self.version.clone(), + }) + .header("Authorization", generate_authorization_header()) + .send()?; + + if download_context.status() != 200 { + return Err(RemoteAccessError::InvalidResponse(download_context.json()?)); + } + + let download_context = &download_context.json::()?; + + info!("download context: {}", download_context.context); + + let buckets = self.buckets.lock().unwrap(); pool.scope(|scope| { - let client = &DROP_CLIENT_SYNC.clone(); let context_map = self.context_map.lock().unwrap(); - for (index, context) in contexts.iter().enumerate() { - let client = client.clone(); - let completed_indexes = completed_indexes_loop_arc.clone(); + for (index, bucket) in buckets.iter().enumerate() { + let mut bucket = (*bucket).clone(); + let completed_contexts = completed_indexes_loop_arc.clone(); let progress = self.progress.get(index); let progress_handle = ProgressHandle::new(progress, self.progress.clone()); // If we've done this one already, skip it // Note to future DecDuck, DropData gets loaded into context_map - if let Some(v) = context_map.get(&context.checksum) - && *v - { - progress_handle.skip(context.length); + let todo_drops = bucket + .drops + .into_iter() + .filter(|e| { + let todo = !*context_map.get(&e.checksum).unwrap_or(&false); + if !todo { + progress_handle.skip(e.length); + } + todo + }) + .collect::>(); + + if todo_drops.is_empty() { continue; - } + }; + + bucket.drops = todo_drops; let sender = self.sender.clone(); - let request = match make_request( - &client, - &["/api/v1/client/chunk"], - &[ - ("id", &context.game_id), - ("version", &context.version), - ("name", &context.file_name), - ("chunk", &context.index.to_string()), - ], - |r| r, - ) { - Ok(request) => request, - Err(e) => { - sender - .send(DownloadManagerSignal::Error( - ApplicationDownloadError::Communication(e), - )) - .unwrap(); - continue; - } - }; - scope.spawn(move |_| { // 3 attempts for i in 0..RETRY_COUNT { let loop_progress_handle = progress_handle.clone(); - match download_game_chunk( - context, + match download_game_bucket( + &bucket, + download_context, &self.control_flag, loop_progress_handle, - request.try_clone().unwrap(), ) { Ok(true) => { - completed_indexes.push(context.checksum.clone()); + for drop in bucket.drops { + completed_contexts.push(drop.checksum); + } return; } Ok(false) => return, Err(e) => { warn!("game download agent error: {e}"); - let retry = match &e { - ApplicationDownloadError::Communication( - _remote_access_error, - ) => true, - ApplicationDownloadError::Checksum => true, - ApplicationDownloadError::Lock => true, - ApplicationDownloadError::IoError(_error_kind) => false, - ApplicationDownloadError::DownloadError => false, - ApplicationDownloadError::DiskFull(_, _) => false, - }; + let retry = matches!( + &e, + ApplicationDownloadError::Communication(_) + | ApplicationDownloadError::Checksum + | ApplicationDownloadError::Lock + ); if i == RETRY_COUNT - 1 || !retry { warn!("retry logic failed, not re-attempting."); @@ -390,14 +449,14 @@ impl GameDownloadAgent { context_map_lock.values().filter(|x| **x).count() }; + let context_map_lock = self.context_map.lock().unwrap(); - let contexts = contexts + let contexts = buckets .iter() + .flat_map(|x| x.drops.iter().map(|e| e.checksum.clone())) .map(|x| { - ( - x.checksum.clone(), - context_map_lock.get(&x.checksum).copied().unwrap_or(false), - ) + let completed = context_map_lock.get(&x).unwrap_or(&false); + (x, *completed) }) .collect::>(); drop(context_map_lock); @@ -408,10 +467,11 @@ impl GameDownloadAgent { // If there are any contexts left which are false if !contexts.iter().all(|x| x.1) { info!( - "download agent for {} exited without completing ({}/{})", + "download agent for {} exited without completing ({}/{}) ({} buckets)", self.id.clone(), completed_lock_len, contexts.len(), + buckets.len() ); return Ok(false); } @@ -442,13 +502,15 @@ impl GameDownloadAgent { pub fn validate(&self, app_handle: &AppHandle) -> Result { self.setup_validate(app_handle); - let contexts = self.contexts.lock().unwrap(); + let buckets = self.buckets.lock().unwrap(); + let contexts: Vec = buckets + .clone() + .into_iter() + .flat_map(|e| -> Vec { e.into() }) + .collect(); let max_download_threads = borrow_db_checked().settings.max_download_threads; - debug!( - "validating game: {} with {} threads", - self.dropdata.game_id, max_download_threads - ); + info!("{} validation contexts", contexts.len()); let pool = ThreadPoolBuilder::new() .num_threads(max_download_threads) .build() @@ -549,6 +611,13 @@ impl Downloadable for GameDownloadAgent { .applications .transient_statuses .remove(&self.metadata()); + + push_game_update( + app_handle, + &self.id, + None, + GameStatusManager::fetch_state(&self.id, &handle), + ); } fn on_complete(&self, app_handle: &tauri::AppHandle) { diff --git a/src-tauri/src/games/downloads/download_logic.rs b/src-tauri/src/games/downloads/download_logic.rs index 4b35693..72579cc 100644 --- a/src-tauri/src/games/downloads/download_logic.rs +++ b/src-tauri/src/games/downloads/download_logic.rs @@ -5,13 +5,15 @@ use crate::download_manager::util::progress_object::ProgressHandle; use crate::error::application_download_error::ApplicationDownloadError; use crate::error::drop_server_error::DropServerError; use crate::error::remote_access_error::RemoteAccessError; -use crate::games::downloads::manifest::DropDownloadContext; +use crate::games::downloads::manifest::{ChunkBody, DownloadBucket, DownloadContext, DownloadDrop}; use crate::remote::auth::generate_authorization_header; -use log::{debug, warn}; +use crate::remote::requests::generate_url; +use crate::remote::utils::DROP_CLIENT_SYNC; +use log::{info, warn}; use md5::{Context, Digest}; -use reqwest::blocking::{RequestBuilder, Response}; +use reqwest::blocking::Response; -use std::fs::{set_permissions, Permissions}; +use std::fs::{Permissions, set_permissions}; use std::io::Read; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; @@ -21,21 +23,29 @@ use std::{ path::PathBuf, }; +static MAX_PACKET_LENGTH: usize = 4096 * 4; + pub struct DropWriter { hasher: Context, - destination: W, + destination: BufWriter, + progress: ProgressHandle, } impl DropWriter { - fn new(path: PathBuf) -> Self { - let destination = OpenOptions::new().write(true).create(true).truncate(false).open(&path).unwrap(); - Self { - destination, + fn new(path: PathBuf, progress: ProgressHandle) -> Result { + let destination = OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .open(&path)?; + Ok(Self { + destination: BufWriter::with_capacity(1024 * 1024, destination), hasher: Context::new(), - } + progress, + }) } fn finish(mut self) -> io::Result { - self.flush().unwrap(); + self.flush()?; Ok(self.hasher.compute()) } } @@ -45,7 +55,10 @@ impl Write for DropWriter { self.hasher .write_all(buf) .map_err(|e| io::Error::other(format!("Unable to write to hasher: {e}")))?; - self.destination.write(buf) + let bytes_written = self.destination.write(buf)?; + self.progress.add(bytes_written); + + Ok(bytes_written) } fn flush(&mut self) -> io::Result<()> { @@ -62,91 +75,103 @@ impl Seek for DropWriter { pub struct DropDownloadPipeline<'a, R: Read, W: Write> { pub source: R, - pub destination: DropWriter, + pub drops: Vec, + pub destination: Vec>, pub control_flag: &'a DownloadThreadControl, - pub progress: ProgressHandle, - pub size: usize, } + impl<'a> DropDownloadPipeline<'a, Response, File> { fn new( source: Response, - destination: DropWriter, + drops: Vec, control_flag: &'a DownloadThreadControl, progress: ProgressHandle, - size: usize, - ) -> Self { - Self { + ) -> Result { + Ok(Self { source, - destination, + destination: drops + .iter() + .map(|drop| DropWriter::new(drop.path.clone(), progress.clone())) + .try_collect()?, + drops, control_flag, - progress, - size, - } + }) } fn copy(&mut self) -> Result { - 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 copy_buffer = [0u8; MAX_PACKET_LENGTH]; + for (index, drop) in self.drops.iter().enumerate() { + let destination = self + .destination + .get_mut(index) + .ok_or(io::Error::other("no destination")) + .unwrap(); + let mut remaining = drop.length; + if drop.start != 0 { + destination.seek(SeekFrom::Start(drop.start.try_into().unwrap()))?; + } + loop { + let size = MAX_PACKET_LENGTH.min(remaining); + self.source.read_exact(&mut copy_buffer[0..size])?; + remaining -= size; + + destination.write_all(©_buffer[0..size])?; + + if remaining == 0 { + break; + }; + } - let mut current_size = 0; - loop { if self.control_flag.get() == DownloadThreadControlFlag::Stop { - buf_writer.flush()?; return Ok(false); } - - let mut bytes_read = self.source.read(&mut copy_buf)?; - current_size += bytes_read; - - if current_size > self.size { - let over = current_size - self.size; - warn!("server sent too many bytes... {over} over"); - bytes_read -= over; - current_size = self.size; - } - - buf_writer.write_all(©_buf[0..bytes_read])?; - self.progress.add(bytes_read); - - if current_size >= self.size { - debug!( - "finished with final size of {} vs {}", - current_size, self.size - ); - break; - } } - buf_writer.flush()?; Ok(true) } - fn finish(self) -> Result { - let checksum = self.destination.finish()?; - Ok(checksum) + fn finish(self) -> Result, io::Error> { + let checksums = self + .destination + .into_iter() + .map(|e| e.finish()) + .try_collect()?; + Ok(checksums) } } -pub fn download_game_chunk( - ctx: &DropDownloadContext, +pub fn download_game_bucket( + bucket: &DownloadBucket, + ctx: &DownloadContext, control_flag: &DownloadThreadControl, progress: ProgressHandle, - request: RequestBuilder, ) -> Result { // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { progress.set(0); return Ok(false); } - let response = request - .header("Authorization", generate_authorization_header()) + + let header = generate_authorization_header(); + + let url = generate_url(&["/api/v2/client/chunk"], &[]) + .map_err(ApplicationDownloadError::Communication)?; + + let body = ChunkBody::create(ctx, &bucket.drops); + + let response = DROP_CLIENT_SYNC + .post(url) + .json(&body) + .header("Authorization", header) .send() .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; if response.status() != 200 { - debug!("chunk request got status code: {}", response.status()); - let raw_res = response.text().unwrap(); + info!("chunk request got status code: {}", response.status()); + let raw_res = response.text().map_err(|e| { + ApplicationDownloadError::Communication(RemoteAccessError::FetchError(e.into())) + })?; + info!("{}", raw_res); if let Ok(err) = serde_json::from_str::(&raw_res) { return Err(ApplicationDownloadError::Communication( RemoteAccessError::InvalidResponse(err), @@ -157,30 +182,35 @@ pub fn download_game_chunk( )); } - let mut destination = DropWriter::new(ctx.path.clone()); + let lengths = response + .headers() + .get("Content-Lengths") + .ok_or(ApplicationDownloadError::Communication( + RemoteAccessError::UnparseableResponse("missing Content-Lengths header".to_owned()), + ))? + .to_str() + .unwrap(); - if ctx.offset != 0 { - destination - .seek(SeekFrom::Start(ctx.offset)) - .expect("Failed to seek to file offset"); - } - let content_length = response.content_length(); - if content_length.is_none() { - warn!("recieved 0 length content from server"); - return Err(ApplicationDownloadError::Communication( - RemoteAccessError::InvalidResponse(response.json().unwrap()), - )); - } - let length = content_length.unwrap().try_into().unwrap(); - - if length != ctx.length { - return Err(ApplicationDownloadError::DownloadError); + for (i, raw_length) in lengths.split(",").enumerate() { + let length = raw_length.parse::().unwrap_or(0); + let Some(drop) = bucket.drops.get(i) else { + warn!("invalid number of Content-Lengths recieved: {}, {}", i, lengths); + return Err(ApplicationDownloadError::DownloadError); + }; + if drop.length != length { + warn!( + "for {}, expected {}, got {} ({})", + drop.filename, drop.length, raw_length, length + ); + return Err(ApplicationDownloadError::DownloadError); + } } let mut pipeline = - DropDownloadPipeline::new(response, destination, control_flag, progress, length); + DropDownloadPipeline::new(response, bucket.drops.clone(), control_flag, progress) + .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; let completed = pipeline .copy() @@ -192,23 +222,23 @@ pub fn download_game_chunk( // If we complete the file, set the permissions (if on Linux) #[cfg(unix)] { - let permissions = Permissions::from_mode(ctx.permissions); - set_permissions(ctx.path.clone(), permissions).unwrap(); + for drop in bucket.drops.iter() { + let permissions = Permissions::from_mode(drop.permissions); + set_permissions(drop.path.clone(), permissions) + .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; + } } - let checksum = pipeline + let checksums = pipeline .finish() .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; - let res = hex::encode(checksum.0); - if res != ctx.checksum { - return Err(ApplicationDownloadError::Checksum); + for (index, drop) in bucket.drops.iter().enumerate() { + let res = hex::encode(**checksums.get(index).unwrap()); + if res != drop.checksum { + return Err(ApplicationDownloadError::Checksum); + } } - debug!( - "Successfully finished download #{}, copied {} bytes", - ctx.checksum, length - ); - Ok(true) } diff --git a/src-tauri/src/games/downloads/drop_data.rs b/src-tauri/src/games/downloads/drop_data.rs index 38ee575..52ad3fb 100644 --- a/src-tauri/src/games/downloads/drop_data.rs +++ b/src-tauri/src/games/downloads/drop_data.rs @@ -76,14 +76,6 @@ impl DropData { pub fn set_context(&self, context: String, state: bool) { self.contexts.lock().unwrap().entry(context).insert_entry(state); } - pub fn get_completed_contexts(&self) -> Vec { - self.contexts - .lock() - .unwrap() - .iter() - .filter_map(|x| if *x.1 { Some(x.0.clone()) } else { None }) - .collect() - } pub fn get_contexts(&self) -> HashMap { self.contexts.lock().unwrap().clone() } diff --git a/src-tauri/src/games/downloads/manifest.rs b/src-tauri/src/games/downloads/manifest.rs index 7c5b3a9..7b8c41a 100644 --- a/src-tauri/src/games/downloads/manifest.rs +++ b/src-tauri/src/games/downloads/manifest.rs @@ -2,6 +2,65 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; +#[derive(Debug, Clone)] +// Drops go in buckets +pub struct DownloadDrop { + pub index: usize, + pub filename: String, + pub path: PathBuf, + pub start: usize, + pub length: usize, + pub checksum: String, + pub permissions: u32, +} + +#[derive(Debug, Clone)] +pub struct DownloadBucket { + pub game_id: String, + pub version: String, + pub drops: Vec, +} + +#[derive(Deserialize)] +pub struct DownloadContext { + pub context: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChunkBodyFile { + filename: String, + chunk_index: usize, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChunkBody { + pub context: String, + pub files: Vec, +} + +#[derive(Serialize)] +pub struct ManifestBody { + pub game: String, + pub version: String, +} + +impl ChunkBody { + pub fn create(context: &DownloadContext, drops: &[DownloadDrop]) -> ChunkBody { + Self { + context: context.context.clone(), + files: drops + .iter() + .map(|e| ChunkBodyFile { + filename: e.filename.clone(), + chunk_index: e.index, + }) + .collect(), + } + } +} + pub type DropManifest = HashMap; #[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] #[serde(rename_all = "camelCase")] @@ -14,14 +73,26 @@ pub struct DropChunk { } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct DropDownloadContext { - pub file_name: String, - pub version: String, +pub struct DropValidateContext { pub index: usize, - pub offset: u64, - pub game_id: String, + pub offset: usize, pub path: PathBuf, pub checksum: String, pub length: usize, - pub permissions: u32, +} + +impl From for Vec { + fn from(value: DownloadBucket) -> Self { + value + .drops + .into_iter() + .map(|e| DropValidateContext { + index: e.index, + offset: e.start, + path: e.path, + checksum: e.checksum, + length: e.length, + }) + .collect() + } } diff --git a/src-tauri/src/games/downloads/validate.rs b/src-tauri/src/games/downloads/validate.rs index 90e9e41..1d11cb8 100644 --- a/src-tauri/src/games/downloads/validate.rs +++ b/src-tauri/src/games/downloads/validate.rs @@ -7,24 +7,22 @@ use log::debug; use md5::Context; use crate::{ - download_manager:: - util::{ - download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, - progress_object::ProgressHandle, - } - , + download_manager::util::{ + download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, + progress_object::ProgressHandle, + }, error::application_download_error::ApplicationDownloadError, - games::downloads::manifest::DropDownloadContext, + games::downloads::manifest::DropValidateContext, }; pub fn validate_game_chunk( - ctx: &DropDownloadContext, + ctx: &DropValidateContext, control_flag: &DownloadThreadControl, progress: ProgressHandle, ) -> Result { debug!( "Starting chunk validation {}, {}, {} #{}", - ctx.file_name, ctx.index, ctx.offset, ctx.checksum + ctx.path.display(), ctx.index, ctx.offset, ctx.checksum ); // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { @@ -38,7 +36,7 @@ pub fn validate_game_chunk( if ctx.offset != 0 { source - .seek(SeekFrom::Start(ctx.offset)) + .seek(SeekFrom::Start(ctx.offset.try_into().unwrap())) .expect("Failed to seek to file offset"); } diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs index 4c5bad5..a42abda 100644 --- a/src-tauri/src/games/library.rs +++ b/src-tauri/src/games/library.rs @@ -20,7 +20,8 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::remote::auth::generate_authorization_header; use crate::remote::cache::cache_object_db; use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db}; -use crate::remote::requests::make_request; +use crate::remote::requests::generate_url; +use crate::remote::utils::DROP_CLIENT_ASYNC; use crate::remote::utils::DROP_CLIENT_SYNC; use bitcode::{Decode, Encode}; @@ -76,24 +77,24 @@ pub struct StatsUpdateEvent { pub time: usize, } -pub fn fetch_library_logic( - state: tauri::State<'_, Mutex>, +pub async fn fetch_library_logic( + state: tauri::State<'_, Mutex>>, ) -> Result, RemoteAccessError> { - let header = generate_authorization_header(); - - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| { - f.header("Authorization", header) - })? - .send()?; + let client = DROP_CLIENT_ASYNC.clone(); + let response = generate_url(&["/api/v1/client/user/library"], &[])?; + let response = client + .get(response) + .header("Authorization", generate_authorization_header()) + .send() + .await?; if response.status() != 200 { - let err = response.json().unwrap(); + let err = response.json().await.unwrap(); warn!("{err:?}"); return Err(RemoteAccessError::InvalidResponse(err)); } - let mut games: Vec = response.json()?; + let mut games: Vec = response.json().await?; let mut handle = state.lock().unwrap(); @@ -135,73 +136,89 @@ pub fn fetch_library_logic( Ok(games) } -pub fn fetch_library_logic_offline( - _state: tauri::State<'_, Mutex>, +pub async fn fetch_library_logic_offline( + _state: tauri::State<'_, Mutex>>, ) -> Result, RemoteAccessError> { let mut games: Vec = get_cached_object("library")?; let db_handle = borrow_db_checked(); games.retain(|game| { - db_handle - .applications - .installed_game_version - .contains_key(&game.id) + matches!( + &db_handle + .applications + .game_statuses + .get(&game.id) + .unwrap_or(&GameDownloadStatus::Remote {}), + GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. } + ) }); Ok(games) } -pub fn fetch_game_logic( +pub async fn fetch_game_logic( id: String, - state: tauri::State<'_, Mutex>, + state: tauri::State<'_, Mutex>>, ) -> Result { - let mut state_handle = state.lock().unwrap(); + let version = { + let state_handle = state.lock().unwrap(); - let db_lock = borrow_db_checked(); + let db_lock = borrow_db_checked(); - let metadata_option = db_lock.applications.installed_game_version.get(&id); - let version = match metadata_option { - None => None, - Some(metadata) => db_lock - .applications - .game_versions - .get(&metadata.id) - .map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap()) - .cloned(), - }; - - let game = state_handle.games.get(&id); - if let Some(game) = game { - let status = GameStatusManager::fetch_state(&id, &db_lock); - - let data = FetchGameStruct { - game: game.clone(), - status, - version, + let metadata_option = db_lock.applications.installed_game_version.get(&id); + let version = match metadata_option { + None => None, + Some(metadata) => db_lock + .applications + .game_versions + .get(&metadata.id) + .map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap()) + .cloned(), }; - cache_object_db(&id, game, &db_lock)?; + let game = state_handle.games.get(&id); + if let Some(game) = game { + let status = GameStatusManager::fetch_state(&id, &db_lock); - return Ok(data); - } - drop(db_lock); + let data = FetchGameStruct { + game: game.clone(), + status, + version, + }; - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| { - r.header("Authorization", generate_authorization_header()) - })? - .send()?; + cache_object_db(&id, game, &db_lock)?; + + return Ok(data); + } + + version + }; + + let client = DROP_CLIENT_ASYNC.clone(); + let response = generate_url(&["/api/v1/client/game/", &id], &[])?; + let response = client + .get(response) + .header("Authorization", generate_authorization_header()) + .send() + .await?; if response.status() == 404 { + let offline_fetch = fetch_game_logic_offline(id.clone(), state).await; + if let Ok(fetch_data) = offline_fetch { + return Ok(fetch_data); + } + return Err(RemoteAccessError::GameNotFound(id)); } if response.status() != 200 { - let err = response.json().unwrap(); + let err = response.json().await.unwrap(); warn!("{err:?}"); return Err(RemoteAccessError::InvalidResponse(err)); } - let game: Game = response.json()?; + let game: Game = response.json().await?; + + let mut state_handle = state.lock().unwrap(); state_handle.games.insert(id.clone(), game.clone()); let mut db_handle = borrow_db_mut_checked(); @@ -227,24 +244,20 @@ pub fn fetch_game_logic( Ok(data) } -pub fn fetch_game_logic_offline( +pub async fn fetch_game_logic_offline( id: String, - _state: tauri::State<'_, Mutex>, + _state: tauri::State<'_, Mutex>>, ) -> Result { let db_handle = borrow_db_checked(); let metadata_option = db_handle.applications.installed_game_version.get(&id); let version = match metadata_option { None => None, - Some(metadata) => Some( - db_handle - .applications - .game_versions - .get(&metadata.id) - .unwrap() - .get(metadata.version.as_ref().unwrap()) - .unwrap() - .clone(), - ), + Some(metadata) => db_handle + .applications + .game_versions + .get(&metadata.id) + .map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap()) + .cloned(), }; let status = GameStatusManager::fetch_state(&id, &db_handle); @@ -259,27 +272,26 @@ pub fn fetch_game_logic_offline( }) } -pub fn fetch_game_verion_options_logic( +pub async fn fetch_game_version_options_logic( game_id: String, - state: tauri::State<'_, Mutex>, + state: tauri::State<'_, Mutex>>, ) -> Result, RemoteAccessError> { - let client = DROP_CLIENT_SYNC.clone(); + let client = DROP_CLIENT_ASYNC.clone(); - let response = make_request( - &client, - &["/api/v1/client/game/versions"], - &[("id", &game_id)], - |r| r.header("Authorization", generate_authorization_header()), - )? - .send()?; + let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?; + let response = client + .get(response) + .header("Authorization", generate_authorization_header()) + .send() + .await?; if response.status() != 200 { - let err = response.json().unwrap(); + let err = response.json().await.unwrap(); warn!("{err:?}"); return Err(RemoteAccessError::InvalidResponse(err)); } - let data: Vec = response.json()?; + let data: Vec = response.json().await?; let state_lock = state.lock().unwrap(); let process_manager_lock = state_lock.process_manager.lock().unwrap(); @@ -440,19 +452,18 @@ pub fn on_game_complete( return Err(RemoteAccessError::GameNotFound(meta.id.clone())); } - let header = generate_authorization_header(); - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request( - &client, + let response = generate_url( &["/api/v1/client/game/version"], &[ ("id", &meta.id), ("version", meta.version.as_ref().unwrap()), ], - |f| f.header("Authorization", header), - )? - .send()?; + )?; + let response = client + .get(response) + .header("Authorization", generate_authorization_header()) + .send()?; let game_version: GameVersion = response.json()?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0f02bf8..6209391 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,8 @@ +#![deny(unused_must_use)] #![feature(fn_traits)] #![feature(duration_constructors)] +#![feature(duration_millis_float)] +#![feature(iterator_try_collect)] #![deny(clippy::all)] mod database; @@ -38,7 +41,7 @@ use games::collections::commands::{ fetch_collection, fetch_collections, }; use games::commands::{ - fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, + fetch_game, fetch_game_status, fetch_game_version_options, fetch_library, uninstall_game, }; use games::downloads::commands::download_game; use games::library::{Game, update_game_configuration}; @@ -134,7 +137,7 @@ pub struct AppState<'a> { compat_info: Option, } -fn setup(handle: AppHandle) -> AppState<'static> { +async fn setup(handle: AppHandle) -> AppState<'static> { let logfile = FileAppender::builder() .encoder(Box::new(PatternEncoder::new( "{d} | {l} | {f}:{L} - {m}{n}", @@ -189,7 +192,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { debug!("database is set up"); // TODO: Account for possible failure - let (app_status, user) = auth::setup(); + let (app_status, user) = auth::setup().await; let db_handle = borrow_db_checked(); let mut missing_games = Vec::new(); @@ -316,7 +319,7 @@ pub fn run() { delete_download_dir, fetch_download_dir_stats, fetch_game_status, - fetch_game_verion_options, + fetch_game_version_options, update_game_configuration, // Collections fetch_collections, @@ -348,92 +351,99 @@ pub fn run() { )) .setup(|app| { let handle = app.handle().clone(); - let state = setup(handle); - debug!("initialized drop client"); - app.manage(Mutex::new(state)); - { - use tauri_plugin_deep_link::DeepLinkExt; - let _ = app.deep_link().register_all(); - debug!("registered all pre-defined deep links"); - } + tauri::async_runtime::block_on(async move { + let state = setup(handle).await; + info!("initialized drop client"); + app.manage(Mutex::new(state)); - let handle = app.handle().clone(); + { + use tauri_plugin_deep_link::DeepLinkExt; + let _ = app.deep_link().register_all(); + debug!("registered all pre-defined deep links"); + } - let _main_window = tauri::WebviewWindowBuilder::new( - &handle, - "main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it - tauri::WebviewUrl::App("main".into()), - ) - .title("Drop Desktop App") - .min_inner_size(1000.0, 500.0) - .inner_size(1536.0, 864.0) - .decorations(false) - .shadow(false) - .data_directory(DATA_ROOT_DIR.join(".webview")) - .build() - .unwrap(); + let handle = app.handle().clone(); - app.deep_link().on_open_url(move |event| { - debug!("handling drop:// url"); - let binding = event.urls(); - let url = binding.first().unwrap(); - if url.host_str().unwrap() == "handshake" { - recieve_handshake(handle.clone(), url.path().to_string()); + let _main_window = tauri::WebviewWindowBuilder::new( + &handle, + "main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it + tauri::WebviewUrl::App("main".into()), + ) + .title("Drop Desktop App") + .min_inner_size(1000.0, 500.0) + .inner_size(1536.0, 864.0) + .decorations(false) + .shadow(false) + .data_directory(DATA_ROOT_DIR.join(".webview")) + .build() + .unwrap(); + + app.deep_link().on_open_url(move |event| { + debug!("handling drop:// url"); + let binding = event.urls(); + let url = binding.first().unwrap(); + if url.host_str().unwrap() == "handshake" { + tauri::async_runtime::spawn(recieve_handshake( + handle.clone(), + url.path().to_string(), + )); + } + }); + + let menu = Menu::with_items( + app, + &[ + &MenuItem::with_id(app, "open", "Open", true, None::<&str>).unwrap(), + &PredefinedMenuItem::separator(app).unwrap(), + /* + &MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?, + &MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?, + &PredefinedMenuItem::separator(app)?, + */ + &MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap(), + ], + ) + .unwrap(); + + run_on_tray(|| { + TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "open" => { + app.webview_windows().get("main").unwrap().show().unwrap(); + } + "quit" => { + cleanup_and_exit(app, &app.state()); + } + + _ => { + warn!("menu event not handled: {:?}", event.id); + } + }) + .build(app) + .expect("error while setting up tray menu"); + }); + + { + let mut db_handle = borrow_db_mut_checked(); + if let Some(original) = db_handle.prev_database.take() { + warn!( + "Database corrupted. Original file at {}", + original.canonicalize().unwrap().to_string_lossy() + ); + app.dialog() + .message( + "Database corrupted. A copy has been saved at: ".to_string() + + original.to_str().unwrap(), + ) + .title("Database corrupted") + .show(|_| {}); + } } }); - let menu = Menu::with_items( - app, - &[ - &MenuItem::with_id(app, "open", "Open", true, None::<&str>)?, - &PredefinedMenuItem::separator(app)?, - /* - &MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?, - &MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?, - &PredefinedMenuItem::separator(app)?, - */ - &MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?, - ], - )?; - - run_on_tray(|| { - TrayIconBuilder::new() - .icon(app.default_window_icon().unwrap().clone()) - .menu(&menu) - .on_menu_event(|app, event| match event.id.as_ref() { - "open" => { - app.webview_windows().get("main").unwrap().show().unwrap(); - } - "quit" => { - cleanup_and_exit(app, &app.state()); - } - - _ => { - warn!("menu event not handled: {:?}", event.id); - } - }) - .build(app) - .expect("error while setting up tray menu"); - }); - - { - let mut db_handle = borrow_db_mut_checked(); - if let Some(original) = db_handle.prev_database.take() { - warn!( - "Database corrupted. Original file at {}", - original.canonicalize().unwrap().to_string_lossy() - ); - app.dialog() - .message( - "Database corrupted. A copy has been saved at: ".to_string() - + original.to_str().unwrap(), - ) - .title("Database corrupted") - .show(|_| {}); - } - } - Ok(()) }) .register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| { @@ -441,15 +451,21 @@ pub fn run() { fetch_object(request, responder).await; }); }) - .register_asynchronous_uri_scheme_protocol("server", move |ctx, request, responder| { - let state: tauri::State<'_, Mutex> = ctx.app_handle().state(); - offline!( - state, - handle_server_proto, - handle_server_proto_offline, - request, - responder - ); + .register_asynchronous_uri_scheme_protocol("server", |ctx, request, responder| { + tauri::async_runtime::block_on(async move { + let state = ctx + .app_handle() + .state::>>(); + + offline!( + state, + handle_server_proto, + handle_server_proto_offline, + request, + responder + ) + .await; + }); }) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { diff --git a/src-tauri/src/remote/auth.rs b/src-tauri/src/remote/auth.rs index e66b361..58c9936 100644 --- a/src-tauri/src/remote/auth.rs +++ b/src-tauri/src/remote/auth.rs @@ -12,12 +12,12 @@ use crate::{ database::{ db::{borrow_db_checked, borrow_db_mut_checked}, models::data::DatabaseAuth, - }, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::utils::DROP_CLIENT_SYNC, AppState, AppStatus, User + }, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::{requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}}, AppState, AppStatus, User }; use super::{ cache::{cache_object, get_cached_object}, - requests::make_request, + requests::generate_url, }; #[derive(Serialize)] @@ -61,16 +61,10 @@ pub fn generate_authorization_header() -> String { format!("Nonce {} {} {}", certs.client_id, nonce, signature) } -pub fn fetch_user() -> Result { - let header = generate_authorization_header(); - - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request(&client, &["/api/v1/client/user"], &[], |f| { - f.header("Authorization", header) - })? - .send()?; +pub async fn fetch_user() -> Result { + let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?; if response.status() != 200 { - let err: DropServerError = response.json()?; + let err: DropServerError = response.json().await?; warn!("{err:?}"); if err.status_message == "Nonce expired" { @@ -80,10 +74,13 @@ pub fn fetch_user() -> Result { return Err(RemoteAccessError::InvalidResponse(err)); } - response.json::().map_err(std::convert::Into::into) + response + .json::() + .await + .map_err(std::convert::Into::into) } -fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> { +async fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> { let path_chunks: Vec<&str> = path.split('/').collect(); if path_chunks.len() != 3 { app.emit("auth/failed", ()).unwrap(); @@ -105,13 +102,13 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc }; let endpoint = base_url.join("/api/v1/client/auth/handshake")?; - let client = DROP_CLIENT_SYNC.clone(); - let response = client.post(endpoint).json(&body).send()?; + let client = DROP_CLIENT_ASYNC.clone(); + let response = client.post(endpoint).json(&body).send().await?; debug!("handshake responsded with {}", response.status().as_u16()); if !response.status().is_success() { - return Err(RemoteAccessError::InvalidResponse(response.json()?)); + return Err(RemoteAccessError::InvalidResponse(response.json().await?)); } - let response_struct: HandshakeResponse = response.json()?; + let response_struct: HandshakeResponse = response.json().await?; { let mut handle = borrow_db_mut_checked(); @@ -129,9 +126,10 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc .post(base_url.join("/api/v1/client/user/webtoken").unwrap()) .header("Authorization", header) .send() + .await .unwrap(); - token.text().unwrap() + token.text().await.unwrap() }; let mut handle = borrow_db_mut_checked(); @@ -141,11 +139,11 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc Ok(()) } -pub fn recieve_handshake(app: AppHandle, path: String) { +pub async fn recieve_handshake(app: AppHandle, path: String) { // Tell the app we're processing app.emit("auth/processing", ()).unwrap(); - let handshake_result = recieve_handshake_logic(&app, path); + let handshake_result = recieve_handshake_logic(&app, path).await; if let Err(e) = handshake_result { warn!("error with authentication: {e}"); app.emit("auth/failed", e.to_string()).unwrap(); @@ -153,9 +151,10 @@ pub fn recieve_handshake(app: AppHandle, path: String) { } let app_state = app.state::>(); - let mut state_lock = app_state.lock().unwrap(); - let (app_status, user) = setup(); + let (app_status, user) = setup().await; + + let mut state_lock = app_state.lock().unwrap(); state_lock.status = app_status; state_lock.user = user; @@ -199,13 +198,14 @@ pub fn auth_initiate_logic(mode: String) -> Result { Ok(response) } -pub fn setup() -> (AppStatus, Option) { - let data = borrow_db_checked(); - let auth = data.auth.clone(); - drop(data); +pub async fn setup() -> (AppStatus, Option) { + let auth = { + let data = borrow_db_checked(); + data.auth.clone() + }; if auth.is_some() { - let user_result = match fetch_user() { + let user_result = match fetch_user().await { Ok(data) => data, Err(RemoteAccessError::FetchError(_)) => { let user = get_cached_object::("user").unwrap(); diff --git a/src-tauri/src/remote/cache.rs b/src-tauri/src/remote/cache.rs index 2c07037..c23be49 100644 --- a/src-tauri/src/remote/cache.rs +++ b/src-tauri/src/remote/cache.rs @@ -11,16 +11,16 @@ use crate::{ }; use bitcode::{Decode, DecodeOwned, Encode}; use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder}; -use log::debug; #[macro_export] macro_rules! offline { ($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => { - if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline { - $func2( $( $arg ), *) + async move { if $crate::borrow_db_checked().settings.force_offline || $var.lock().unwrap().status == $crate::AppStatus::Offline { + $func2( $( $arg ), *).await } else { - $func1( $( $arg ), *) + $func1( $( $arg ), *).await + } } } } @@ -68,18 +68,9 @@ pub fn get_cached_object_db( key: &str, db: &Database, ) -> Result { - let start = SystemTime::now(); let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?; - let read = start.elapsed().unwrap(); let data = bitcode::decode::(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?; - let decode = start.elapsed().unwrap(); - debug!( - "cache object took: r:{}, d:{}, b:{}", - read.as_millis(), - read.abs_diff(decode).as_millis(), - bytes.len() - ); Ok(data) } #[derive(Encode, Decode)] diff --git a/src-tauri/src/remote/commands.rs b/src-tauri/src/remote/commands.rs index 15c80cd..1468505 100644 --- a/src-tauri/src/remote/commands.rs +++ b/src-tauri/src/remote/commands.rs @@ -8,11 +8,14 @@ use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ - database::db::{borrow_db_checked, borrow_db_mut_checked}, error::remote_access_error::RemoteAccessError, remote::{ + AppState, AppStatus, + database::db::{borrow_db_checked, borrow_db_mut_checked}, + error::remote_access_error::RemoteAccessError, + remote::{ auth::generate_authorization_header, - requests::make_request, + requests::generate_url, utils::{DROP_CLIENT_SYNC, DROP_CLIENT_WS_CLIENT}, - }, AppState, AppStatus + }, }; use super::{ @@ -45,10 +48,11 @@ pub fn gen_drop_url(path: String) -> Result { #[tauri::command] pub fn fetch_drop_object(path: String) -> Result, RemoteAccessError> { let _drop_url = gen_drop_url(path.clone())?; - let req = make_request(&DROP_CLIENT_SYNC, &[&path], &[], |r| { - r.header("Authorization", generate_authorization_header()) - })? - .send(); + let req = generate_url(&[&path], &[])?; + let req = DROP_CLIENT_SYNC + .get(req) + .header("Authorization", generate_authorization_header()) + .send(); match req { Ok(data) => { @@ -83,13 +87,15 @@ pub fn sign_out(app: AppHandle) { } #[tauri::command] -pub fn retry_connect(state: tauri::State<'_, Mutex>) { - let (app_status, user) = setup(); +pub async fn retry_connect(state: tauri::State<'_, Mutex>>) -> Result<(), ()> { + let (app_status, user) = setup().await; let mut guard = state.lock().unwrap(); guard.status = app_status; guard.user = user; drop(guard); + + Ok(()) } #[tauri::command] @@ -145,9 +151,7 @@ pub fn auth_initiate_code(app: AppHandle) -> Result { match response.response_type.as_str() { "token" => { let recieve_app = app.clone(); - tauri::async_runtime::spawn_blocking(move || { - manual_recieve_handshake(recieve_app, response.value); - }); + manual_recieve_handshake(recieve_app, response.value).await.unwrap(); return Ok(()); } _ => return Err(RemoteAccessError::HandshakeFailed(response.value)), @@ -171,6 +175,8 @@ pub fn auth_initiate_code(app: AppHandle) -> Result { } #[tauri::command] -pub fn manual_recieve_handshake(app: AppHandle, token: String) { - recieve_handshake(app, format!("handshake/{token}")); +pub async fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), ()> { + recieve_handshake(app, format!("handshake/{token}")).await; + + Ok(()) } diff --git a/src-tauri/src/remote/requests.rs b/src-tauri/src/remote/requests.rs index 44cdc83..7ebf7b1 100644 --- a/src-tauri/src/remote/requests.rs +++ b/src-tauri/src/remote/requests.rs @@ -1,13 +1,16 @@ -use reqwest::blocking::{Client, RequestBuilder}; +use url::Url; -use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB}; +use crate::{ + DB, + database::db::DatabaseImpls, + error::remote_access_error::RemoteAccessError, + remote::{auth::generate_authorization_header, utils::DROP_CLIENT_ASYNC}, +}; -pub fn make_request, F: FnOnce(RequestBuilder) -> RequestBuilder>( - client: &Client, +pub fn generate_url>( path_components: &[T], query: &[(T, T)], - f: F, -) -> Result { +) -> Result { let mut base_url = DB.fetch_base_url(); for endpoint in path_components { base_url = base_url.join(endpoint.as_ref())?; @@ -18,6 +21,13 @@ pub fn make_request, F: FnOnce(RequestBuilder) -> RequestBuilder>( queries.append_pair(param.as_ref(), val.as_ref()); } } - let response = client.get(base_url); - Ok(f(response)) + Ok(base_url) +} + +pub async fn make_authenticated_get(url: Url) -> Result { + DROP_CLIENT_ASYNC + .get(url) + .header("Authorization", generate_authorization_header()) + .send() + .await } diff --git a/src-tauri/src/remote/server_proto.rs b/src-tauri/src/remote/server_proto.rs index d3c87af..ed07c9b 100644 --- a/src-tauri/src/remote/server_proto.rs +++ b/src-tauri/src/remote/server_proto.rs @@ -5,7 +5,7 @@ use tauri::UriSchemeResponder; use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC}; -pub fn handle_server_proto_offline(_request: Request>, responder: UriSchemeResponder) { +pub async fn handle_server_proto_offline(_request: Request>, responder: UriSchemeResponder) { let four_oh_four = Response::builder() .status(StatusCode::NOT_FOUND) .body(Vec::new()) @@ -13,7 +13,7 @@ pub fn handle_server_proto_offline(_request: Request>, responder: UriSch responder.respond(four_oh_four); } -pub fn handle_server_proto(request: Request>, responder: UriSchemeResponder) { +pub async fn handle_server_proto(request: Request>, responder: UriSchemeResponder) { let db_handle = borrow_db_checked(); let web_token = match &db_handle.auth.as_ref().unwrap().web_token { Some(e) => e, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b85c8d9..ef7b335 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2.0.0", "productName": "Drop Desktop Client", - "version": "0.3.1", + "version": "0.3.2", "identifier": "dev.drop.client", "build": { "beforeDevCommand": "yarn --cwd main dev --port 1432", From e11db851a5af2b2bd40c8198adbc3a1d30e9c6a6 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 11 Aug 2025 14:37:46 +1000 Subject: [PATCH 02/13] fix: #92 (#115) --- src-tauri/Cargo.lock | 29 ++++++++++++++++++++++++++++- src-tauri/Cargo.toml | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 302425d..40900a4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2381,6 +2381,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -3110,7 +3111,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -4446,6 +4447,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -4689,6 +4691,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.2.0", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -4786,6 +4800,19 @@ dependencies = [ "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.14.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cf7ae51..d33d098 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -106,7 +106,7 @@ features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc" [dependencies.reqwest] version = "0.12" default-features = false -features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-webpki-roots"] +features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"] [dependencies.serde] version = "1" From cb55ac2bf5ead27d1cffcddd21aea88d81855202 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 12 Aug 2025 15:08:50 +1000 Subject: [PATCH 03/13] Fix platform builds --- .github/workflows/release.yml | 2 +- src-tauri/Cargo.lock | 19 ++++++++----------- src-tauri/Cargo.toml | 2 +- src-tauri/build.rs | 1 - 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 455fdc7..7a7a03e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above. run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 40900a4..59f1c19 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1312,7 +1312,7 @@ dependencies = [ "rand 0.9.1", "rayon", "regex", - "reqwest 0.12.16", + "reqwest 0.12.22", "reqwest-middleware 0.4.2", "reqwest-middleware-cache", "reqwest-websocket", @@ -4420,9 +4420,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.16" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf597b113be201cb2269b4c39b39a804d01b99ee95a4278f0ed04e45cff1c71" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -4437,12 +4437,9 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", @@ -4493,7 +4490,7 @@ dependencies = [ "anyhow", "async-trait", "http 1.3.1", - "reqwest 0.12.16", + "reqwest 0.12.22", "serde", "thiserror 1.0.69", "tower-service", @@ -4528,7 +4525,7 @@ dependencies = [ "async-tungstenite", "bytes", "futures-util", - "reqwest 0.12.16", + "reqwest 0.12.22", "thiserror 2.0.12", "tokio", "tokio-util", @@ -5533,7 +5530,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.12.16", + "reqwest 0.12.22", "serde", "serde_json", "serde_repr", @@ -6178,9 +6175,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d33d098..0fc87e0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -104,7 +104,7 @@ version = "2" features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc" [dependencies.reqwest] -version = "0.12" +version = "0.12.22" default-features = false features = ["json", "http2", "blocking", "rustls-tls", "native-tls-alpn", "rustls-tls-native-roots"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 54326bf..261851f 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,4 +1,3 @@ fn main() { - println!("cargo::rustc-link-lib=appindicator3"); tauri_build::build(); } From 17c375bcabf2d1711966d58ba15a7e746a9564b2 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Fri, 15 Aug 2025 22:56:49 +1000 Subject: [PATCH 04/13] UI & error fixes & QoL (#116) * fix: use Arc instead of just ErrorKind * fix: game status updates for UI * fix: missing game version on push_game_update calls * feat: wait if library load takes <300ms * fix: clippy --- main/components/LibrarySearch.vue | 108 +++++++----------- .../download_manager_builder.rs | 13 ++- .../src/error/application_download_error.rs | 5 +- .../src/games/downloads/download_agent.rs | 21 ++-- .../src/games/downloads/download_logic.rs | 20 ++-- src-tauri/src/games/library.rs | 24 ++-- src-tauri/src/process/utils.rs | 6 +- 7 files changed, 90 insertions(+), 107 deletions(-) diff --git a/main/components/LibrarySearch.vue b/main/components/LibrarySearch.vue index 17a44e9..be9129b 100644 --- a/main/components/LibrarySearch.vue +++ b/main/components/LibrarySearch.vue @@ -1,92 +1,55 @@