diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1735f73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Arch Linux, Windows] + - App Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7d0ff2c..0fc3abe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: build-linux: stage: build - image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rustlang/rust:nightly script: - apt-get update -y - apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y @@ -13,9 +13,10 @@ build-linux: - yarnpkg tauri build - cp src-tauri/target/release/bundle/deb/*.deb . - cp src-tauri/target/release/bundle/rpm/*.rpm . + - cp src-tauri/target/release/bundle/appimage/*.AppImage . artifacts: paths: - - "*.{deb,rpm}" + - "*.{deb,rpm,AppImage}" build-windows: stage: build diff --git a/app.vue b/app.vue index f81c432..b9011c6 100644 --- a/app.vue +++ b/app.vue @@ -1,8 +1,8 @@ diff --git a/pages/library.vue b/pages/library.vue index a1838d9..ab5079a 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -1,21 +1,25 @@ \ No newline at end of file diff --git a/pages/queue.vue b/pages/queue.vue index d78967c..8d46bea 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -1,23 +1,37 @@ diff --git a/pages/settings/compatibility.vue b/pages/settings/compatibility.vue deleted file mode 100644 index 27e0f69..0000000 --- a/pages/settings/compatibility.vue +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/pages/settings/debug.vue b/pages/settings/debug.vue index 3ef3e06..7a549ca 100644 --- a/pages/settings/debug.vue +++ b/pages/settings/debug.vue @@ -58,26 +58,6 @@

-
-
- -

- Compatibility Settings -

-
-
-

- Enabled: {{ compatInfo.enabled ? "Yes" : "No" }} -

-

- Runner: {{ compatInfo.runner || "Not configured" }} -

-

- Prefix: {{ compatInfo.prefix || "Not configured" }} -

-
-
-
+
+

+ Download Settings +

+

+ Configure how Drop downloads games and other content. +

+ +
+ +
+ +
+

+ The maximum number of concurrent download threads. Higher values may + download faster but use more system resources. Default is 4. +

+
+ +
+ +
+
@@ -172,6 +220,7 @@ import { } from "@headlessui/vue"; import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid"; import { invoke } from "@tauri-apps/api/core"; +import { type Settings } from "~/types"; const open = ref(false); const currentDirectory = ref(undefined); @@ -180,6 +229,14 @@ const createDirectoryLoading = ref(false); const dirs = ref>([]); +const settings = await invoke("fetch_settings"); +const downloadThreads = ref(settings?.maxDownloadThreads ?? 4); + +const saveState = reactive({ + loading: false, + success: false +}); + async function updateDirs() { const newDirs = await invoke>("fetch_download_dir_stats"); dirs.value = newDirs; @@ -213,7 +270,7 @@ async function submitDirectory() { try { error.value = undefined; if (!currentDirectory.value) - throw new Error("Please select a directory first."); + throw new Error("Please select a directory first"); createDirectoryLoading.value = true; // Add directory @@ -235,4 +292,42 @@ async function deleteDirectory(index: number) { await invoke("delete_download_dir", { index }); await updateDirs(); } + +async function saveDownloadThreads() { + try { + saveState.loading = true; + await invoke("update_settings", { + newSettings: { maxDownloadThreads: downloadThreads.value }, + }); + + // Show success state + saveState.success = true; + + // Reset back to normal state after 2 seconds + setTimeout(() => { + saveState.success = false; + }, 2000); + + } catch (error) { + console.error('Failed to save settings:', error); + } finally { + saveState.loading = false; + } +} + +function validateNumberInput(event: KeyboardEvent) { + // Allow only numbers and basic control keys + if (!/^\d$/.test(event.key) && + !['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) { + event.preventDefault(); + } +} + +function validatePaste(event: ClipboardEvent) { + // Prevent paste if content contains non-numeric characters + const pastedData = event.clipboardData?.getData('text'); + if (pastedData && !/^\d+$/.test(pastedData)) { + event.preventDefault(); + } +} diff --git a/plugins/global-error-handler.ts b/plugins/global-error-handler.ts index f9d7249..b7bcd45 100644 --- a/plugins/global-error-handler.ts +++ b/plugins/global-error-handler.ts @@ -1,7 +1,7 @@ export default defineNuxtPlugin((nuxtApp) => { // Also possible nuxtApp.hook("vue:error", (error, instance, info) => { - console.log(error); + console.error(error, info); const router = useRouter(); router.replace(`/error`); }); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 069c108..22b5365 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -255,6 +255,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic-instant-full" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1000,8 +1006,9 @@ dependencies = [ [[package]] name = "drop-app" -version = "0.1.0" +version = "0.2.0-beta-prerelease-1" dependencies = [ + "atomic-instant-full", "boxcar", "chrono", "directories", @@ -1011,6 +1018,7 @@ dependencies = [ "log4rs", "md5", "openssl", + "parking_lot 0.12.3", "rayon", "reqwest", "rustbreak", @@ -1020,6 +1028,7 @@ dependencies = [ "serde_json", "serde_with", "shared_child", + "slice-deque", "tauri", "tauri-build", "tauri-plugin-autostart", @@ -1028,6 +1037,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-shell", "tauri-plugin-single-instance", + "throttle_my_fn", "tokio", "umu-wrapper-lib", "url", @@ -2341,7 +2351,7 @@ dependencies = [ "log", "log-mdc", "once_cell", - "parking_lot", + "parking_lot 0.12.3", "rand 0.8.5", "serde", "serde-value", @@ -2359,6 +2369,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2958,6 +2977,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -2965,7 +2995,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -2976,7 +3020,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -3411,6 +3455,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -3998,6 +4051,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -4030,7 +4094,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.8", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -4088,7 +4152,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot", + "parking_lot 0.12.3", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -4241,7 +4305,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot", + "parking_lot 0.12.3", "raw-window-handle", "scopeguard", "tao-macros", @@ -4702,6 +4766,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "throttle_my_fn" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482c185e5675626c9a130b3a8f362c322a239338c882f745a1d9a85838b987f0" +dependencies = [ + "parking_lot 0.11.2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "time" version = "0.3.37" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 73aa54e..f9f24c3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drop-app" -version = "0.1.0" +version = "0.2.0-beta-prerelease-1" description = "The client application for the open-source, self-hosted game distribution platform Drop" authors = ["Drop OSS"] edition = "2021" @@ -46,6 +46,10 @@ umu-wrapper-lib = "0.1.0" tauri-plugin-autostart = "2.0.0" shared_child = "1.0.1" serde_with = "3.12.0" +slice-deque = "0.3.0" +throttle_my_fn = "0.2.6" +parking_lot = "0.12.3" +atomic-instant-full = "0.1.0" [dependencies.tauri] version = "2.1.1" diff --git a/src-tauri/rust-toolchain.toml b/src-tauri/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/src-tauri/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs index bd526be..dec9148 100644 --- a/src-tauri/src/autostart.rs +++ b/src-tauri/src/autostart.rs @@ -1,69 +1,76 @@ -use log::info; -use tauri::AppHandle; -use tauri_plugin_autostart::ManagerExt; -use crate::DB; - -#[tauri::command] -pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> { - let manager = app.autolaunch(); - if enabled { - manager.enable().map_err(|e| e.to_string())?; - info!("Enabled autostart"); - } else { - manager.disable().map_err(|e| e.to_string())?; - info!("Disabled autostart"); - } - - // Store the state in DB - let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?; - db_handle.settings.autostart = enabled; - drop(db_handle); - DB.save().map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn get_autostart_enabled(app: AppHandle) -> Result { - // First check DB state - let db_handle = DB.borrow_data().map_err(|e| e.to_string())?; - let db_state = db_handle.settings.autostart; - drop(db_handle); - - // Get actual system state - let manager = app.autolaunch(); - let system_state = manager.is_enabled().map_err(|e| e.to_string())?; - - // If they don't match, sync to DB state - if db_state != system_state { - if db_state { - manager.enable().map_err(|e| e.to_string())?; - } else { - manager.disable().map_err(|e| e.to_string())?; - } - } - - Ok(db_state) -} - -// New function to sync state on startup -pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> { - let db_handle = DB.borrow_data().map_err(|e| e.to_string())?; - let should_be_enabled = db_handle.settings.autostart; - drop(db_handle); - - let manager = app.autolaunch(); - let current_state = manager.is_enabled().map_err(|e| e.to_string())?; - - if current_state != should_be_enabled { - if should_be_enabled { - manager.enable().map_err(|e| e.to_string())?; - info!("Synced autostart: enabled"); - } else { - manager.disable().map_err(|e| e.to_string())?; - info!("Synced autostart: disabled"); - } - } - - Ok(()) -} +use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; +use log::debug; +use tauri::AppHandle; +use tauri_plugin_autostart::ManagerExt; + +pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> { + let manager = app.autolaunch(); + if enabled { + manager.enable().map_err(|e| e.to_string())?; + debug!("enabled autostart"); + } else { + manager.disable().map_err(|e| e.to_string())?; + debug!("eisabled autostart"); + } + + // Store the state in DB + let mut db_handle = borrow_db_mut_checked(); + db_handle.settings.autostart = enabled; + drop(db_handle); + save_db(); + + Ok(()) +} + +pub fn get_autostart_enabled_logic(app: AppHandle) -> Result { + // First check DB state + let db_handle = borrow_db_checked(); + let db_state = db_handle.settings.autostart; + drop(db_handle); + + // Get actual system state + let manager = app.autolaunch(); + let system_state = manager.is_enabled()?; + + // If they don't match, sync to DB state + if db_state != system_state { + if db_state { + manager.enable()?; + } else { + manager.disable()?; + } + } + + Ok(db_state) +} + +// New function to sync state on startup +pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> { + let db_handle = borrow_db_checked(); + let should_be_enabled = db_handle.settings.autostart; + drop(db_handle); + + let manager = app.autolaunch(); + let current_state = manager.is_enabled().map_err(|e| e.to_string())?; + + if current_state != should_be_enabled { + if should_be_enabled { + manager.enable().map_err(|e| e.to_string())?; + debug!("synced autostart: enabled"); + } else { + manager.disable().map_err(|e| e.to_string())?; + debug!("synced autostart: disabled"); + } + } + + Ok(()) +} +#[tauri::command] +pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> { + toggle_autostart_logic(app, enabled) +} + +#[tauri::command] +pub fn get_autostart_enabled(app: AppHandle) -> Result { + get_autostart_enabled_logic(app) +} diff --git a/src-tauri/src/cleanup.rs b/src-tauri/src/cleanup.rs index 925ea61..de0e4e6 100644 --- a/src-tauri/src/cleanup.rs +++ b/src-tauri/src/cleanup.rs @@ -1,15 +1,23 @@ - -use log::info; +use log::{debug, error}; use tauri::AppHandle; +use crate::AppState; #[tauri::command] -pub fn quit(app: tauri::AppHandle) { - cleanup_and_exit(&app); +pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex>>) { + cleanup_and_exit(&app, &state); } -pub fn cleanup_and_exit(app: &AppHandle) { - info!("exiting drop application..."); +pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex>>) { + debug!("cleaning up and exiting application"); + let download_manager = state.lock().unwrap().download_manager.clone(); + match download_manager.ensure_terminated() { + Ok(res) => match res { + Ok(_) => debug!("download manager terminated correctly"), + Err(_) => error!("download manager failed to terminate correctly"), + }, + Err(e) => panic!("{:?}", e), + } app.exit(0); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..b47a348 --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,11 @@ +use crate::AppState; + +#[tauri::command] +pub fn fetch_state( + state: tauri::State<'_, std::sync::Mutex>>, +) -> Result { + let guard = state.lock().unwrap(); + let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?; + drop(guard); + Ok(cloned_state) +} diff --git a/src-tauri/src/database/commands.rs b/src-tauri/src/database/commands.rs new file mode 100644 index 0000000..18befb8 --- /dev/null +++ b/src-tauri/src/database/commands.rs @@ -0,0 +1,92 @@ +use std::{ + fs::create_dir_all, + io::{Error, ErrorKind}, + path::{Path, PathBuf}, +}; + +use serde_json::Value; + +use crate::{ + database::{db::borrow_db_mut_checked, settings::Settings}, + download_manager::internal_error::InternalError, +}; + +use super::{ + db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, + debug::SystemData, +}; + +// Will, in future, return disk/remaining size +// Just returns the directories that have been set up +#[tauri::command] +pub fn fetch_download_dir_stats() -> Vec { + let lock = borrow_db_checked(); + lock.applications.install_dirs.clone() +} + +#[tauri::command] +pub fn delete_download_dir(index: usize) { + let mut lock = borrow_db_mut_checked(); + lock.applications.install_dirs.remove(index); + drop(lock); + save_db(); +} + +#[tauri::command] +pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> { + // Check the new directory is all good + let new_dir_path = Path::new(&new_dir); + if new_dir_path.exists() { + let dir_contents = new_dir_path.read_dir()?; + if dir_contents.count() != 0 { + return Err(Error::new( + ErrorKind::DirectoryNotEmpty, + "Selected directory cannot contain any existing files", + ) + .into()); + } + } else { + create_dir_all(new_dir_path)?; + } + + // Add it to the dictionary + let mut lock = borrow_db_mut_checked(); + if lock.applications.install_dirs.contains(&new_dir) { + return Err(Error::new( + ErrorKind::AlreadyExists, + "Selected directory already exists in database", + ) + .into()); + } + lock.applications.install_dirs.push(new_dir); + drop(lock); + save_db(); + + Ok(()) +} + +#[tauri::command] +pub fn update_settings(new_settings: Value) { + let mut db_lock = borrow_db_mut_checked(); + let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap(); + for (key, value) in new_settings.as_object().unwrap() { + current_settings[key] = value.clone(); + } + let new_settings: Settings = serde_json::from_value(current_settings).unwrap(); + db_lock.settings = new_settings; + println!("new Settings: {:?}", db_lock.settings); +} +#[tauri::command] +pub fn fetch_settings() -> Settings { + borrow_db_checked().settings.clone() +} +#[tauri::command] +pub fn fetch_system_data() -> SystemData { + let db_handle = borrow_db_checked(); + SystemData::new( + db_handle.auth.as_ref().unwrap().client_id.clone(), + db_handle.base_url.clone(), + DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(), + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), + ) +} diff --git a/src-tauri/src/db.rs b/src-tauri/src/database/db.rs similarity index 50% rename from src-tauri/src/db.rs rename to src-tauri/src/database/db.rs index a9ad8da..b92de3d 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/database/db.rs @@ -1,20 +1,27 @@ use std::{ collections::HashMap, fs::{self, create_dir_all}, + hash::Hash, path::{Path, PathBuf}, - sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH}, + sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, }; use chrono::Utc; use directories::BaseDirs; -use log::debug; +use log::{debug, error, info}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::serde_as; use tauri::AppHandle; use url::Url; -use crate::{download_manager::downloadable_metadata::DownloadableMetadata, games::{library::push_game_update, state::GameStatusManager}, process::process_manager::Platform, DB}; +use crate::{ + database::settings::Settings, + download_manager::downloadable_metadata::DownloadableMetadata, + games::{library::push_game_update, state::GameStatusManager}, + process::process_manager::Platform, + DB, +}; #[derive(serde::Serialize, Clone, Deserialize)] pub struct DatabaseAuth { @@ -50,18 +57,30 @@ pub enum ApplicationTransientStatus { #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct GameVersion { - pub version_index: usize, + pub game_id: String, pub version_name: String, - pub launch_command: String, - pub setup_command: String, + pub platform: Platform, + + pub launch_command: String, + pub launch_args: Vec, + + pub setup_command: String, + pub setup_args: Vec, + + pub only_setup: bool, + + pub version_index: usize, + pub delta: bool, + + pub umu_id_override: Option, } #[serde_as] -#[derive(Serialize, Clone, Deserialize)] +#[derive(Serialize, Clone, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct DatabaseApplications { - pub install_dirs: Vec, + pub install_dirs: Vec, // Guaranteed to exist if the game also exists in the app state map pub game_statuses: HashMap, pub game_versions: HashMap>, @@ -71,29 +90,34 @@ pub struct DatabaseApplications { pub transient_statuses: HashMap, } -#[derive(Serialize, Deserialize, Clone)] -pub struct Settings { - pub autostart: bool, - // ... other settings ... -} - -impl Default for Settings { - fn default() -> Self { - Self { - autostart: false, - // ... other settings defaults ... - } - } -} - -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Default)] pub struct Database { #[serde(default)] pub settings: Settings, pub auth: Option, pub base_url: String, pub applications: DatabaseApplications, - pub prev_database: Option + pub prev_database: Option, +} +impl Database { + fn new>(games_base_dir: T, prev_database: Option) -> Self { + Self { + applications: DatabaseApplications { + install_dirs: vec![games_base_dir.into()], + game_statuses: HashMap::new(), + game_versions: HashMap::new(), + installed_game_version: HashMap::new(), + transient_statuses: HashMap::new(), + }, + prev_database, + base_url: "".to_owned(), + auth: None, + settings: Settings { + autostart: false, + max_download_threads: 4, + }, + } + } } pub static DATA_ROOT_DIR: LazyLock> = LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); @@ -127,35 +151,20 @@ impl DatabaseImpls for DatabaseInterface { let games_base_dir = data_root_dir.join("games"); let logs_root_dir = data_root_dir.join("logs"); - debug!("Creating data directory at {:?}", data_root_dir); + debug!("creating data directory at {:?}", data_root_dir); create_dir_all(data_root_dir.clone()).unwrap(); - debug!("Creating games directory"); create_dir_all(games_base_dir.clone()).unwrap(); - debug!("Creating logs directory"); create_dir_all(logs_root_dir.clone()).unwrap(); let exists = fs::exists(db_path.clone()).unwrap(); match exists { - true => - match PathDatabase::load_from_path(db_path.clone()) { - Ok(db) => db, - Err(e) => handle_invalid_database(e, db_path, games_base_dir), - }, + true => match PathDatabase::load_from_path(db_path.clone()) { + Ok(db) => db, + Err(e) => handle_invalid_database(e, db_path, games_base_dir), + }, false => { - let default = Database { - settings: Settings::default(), - auth: None, - base_url: "".to_string(), - applications: DatabaseApplications { - install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], - game_statuses: HashMap::new(), - transient_statuses: HashMap::new(), - game_versions: HashMap::new(), - installed_game_version: HashMap::new(), - }, - prev_database: None, - }; + let default = Database::new(games_base_dir, None); debug!( "Creating database at path {}", db_path.as_os_str().to_str().unwrap() @@ -176,103 +185,72 @@ impl DatabaseImpls for DatabaseInterface { } } -#[tauri::command] -pub fn add_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))?; - 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))?; - if dir_contents.count() != 0 { - return Err("Directory is not empty".to_string()); - } - } else { - create_dir_all(new_dir_path) - .map_err(|e| format!("Unable to create directories to path: {}", e))?; - } - - // Add it to the dictionary - let mut lock = DB.borrow_data_mut().unwrap(); - if lock.applications.install_dirs.contains(&new_dir) { - return Err("Download directory already used".to_string()); - } - lock.applications.install_dirs.push(new_dir); - drop(lock); - DB.save().unwrap(); - - Ok(()) -} - -#[tauri::command] -pub fn delete_download_dir(index: usize) -> Result<(), String> { - let mut lock = DB.borrow_data_mut().unwrap(); - lock.applications.install_dirs.remove(index); - drop(lock); - DB.save().unwrap(); - - Ok(()) -} - -// Will, in future, return disk/remaining size -// Just returns the directories that have been set up -#[tauri::command] -pub fn fetch_download_dir_stats() -> Result, String> { - let lock = DB.borrow_data().unwrap(); - let directories = lock.applications.install_dirs.clone(); - drop(lock); - - Ok(directories) -} - pub fn set_game_status, &DownloadableMetadata)>( app_handle: &AppHandle, meta: DownloadableMetadata, setter: F, ) { - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); setter(&mut db_handle, &meta); drop(db_handle); - DB.save().unwrap(); + save_db(); let status = GameStatusManager::fetch_state(&meta.id); - push_game_update(app_handle, &meta, status); + push_game_update(app_handle, &meta.id, status); } - // TODO: Make the error relelvant rather than just assume that it's a Deserialize error -fn handle_invalid_database(_e: RustbreakError, db_path: PathBuf, games_base_dir: PathBuf) -> rustbreak::Database { - let new_path = { +fn handle_invalid_database( + _e: RustbreakError, + db_path: PathBuf, + games_base_dir: PathBuf, +) -> rustbreak::Database { + let new_path = { let time = Utc::now().timestamp(); - let mut base = db_path.clone().into_os_string(); - base.push("."); - base.push(time.to_string()); + let mut base = db_path.clone(); + base.set_file_name(format!("drop.db.backup-{}", time)); base }; - fs::copy(&db_path, &new_path).unwrap(); + info!( + "old database stored at: {}", + new_path.to_string_lossy().to_string() + ); + fs::rename(&db_path, &new_path).unwrap(); - let db = Database { - auth: None, - base_url: "".to_string(), - applications: DatabaseApplications { - install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], - game_statuses: HashMap::new(), - transient_statuses: HashMap::new(), - game_versions: HashMap::new(), - installed_game_version: HashMap::new(), - }, - prev_database: Some(new_path.into()), - settings: Settings { autostart: false }, - }; + let db = Database::new( + games_base_dir.into_os_string().into_string().unwrap(), + Some(new_path), + ); - PathDatabase::create_at_path(db_path, db) - .expect("Database could not be created") + PathDatabase::create_at_path(db_path, db).expect("Database could not be created") +} +pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> { + match DB.borrow_data() { + Ok(data) => data, + Err(e) => { + error!("database borrow failed with error {}", e); + panic!("database borrow failed with error {}", e); + } + } +} -} \ No newline at end of file +pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> { + match DB.borrow_data_mut() { + Ok(data) => data, + Err(e) => { + error!("database borrow mut failed with error {}", e); + panic!("database borrow mut failed with error {}", e); + } + } +} + +pub fn save_db() { + match DB.save() { + Ok(_) => {} + Err(e) => { + error!("database failed to save with error {}", e); + panic!("database failed to save with error {}", e) + } + } +} diff --git a/src-tauri/src/database/debug.rs b/src-tauri/src/database/debug.rs new file mode 100644 index 0000000..45d2034 --- /dev/null +++ b/src-tauri/src/database/debug.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemData { + client_id: String, + base_url: String, + data_dir: String, + log_level: String, +} + +impl SystemData { + pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self { + Self { + client_id, + base_url, + data_dir, + log_level, + } + } +} diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs new file mode 100644 index 0000000..2a4255b --- /dev/null +++ b/src-tauri/src/database/mod.rs @@ -0,0 +1,4 @@ +pub mod commands; +pub mod db; +pub mod debug; +pub mod settings; diff --git a/src-tauri/src/database/settings.rs b/src-tauri/src/database/settings.rs new file mode 100644 index 0000000..b8c1742 --- /dev/null +++ b/src-tauri/src/database/settings.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Settings { + pub autostart: bool, + pub max_download_threads: usize, + // ... other settings ... +} +impl Default for Settings { + fn default() -> Self { + Self { + autostart: false, + max_download_threads: 4, + } + } +} +// Ideally use pointers instead of a macro to assign the settings +// fn deserialize_into(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error> +// where T: for<'a> Deserialize<'a> +// { +// *t = serde_json::from_value(v)?; +// Ok(()) +// } diff --git a/src-tauri/src/debug.rs b/src-tauri/src/debug.rs deleted file mode 100644 index de42dd6..0000000 --- a/src-tauri/src/debug.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{DATA_ROOT_DIR, DB}; -use serde::Serialize; - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SystemData { - client_id: String, - base_url: String, - data_dir: String, -} - -#[tauri::command] -pub fn fetch_system_data() -> Result { - let db_handle = DB.borrow_data().map_err(|e| e.to_string())?; - let system_data = SystemData { - client_id: db_handle.auth.as_ref().unwrap().client_id.clone(), - base_url: db_handle.base_url.clone(), - data_dir: DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(), - }; - drop(db_handle); - - Ok(system_data) -} diff --git a/src-tauri/src/download_manager/commands.rs b/src-tauri/src/download_manager/commands.rs new file mode 100644 index 0000000..0a65c0d --- /dev/null +++ b/src-tauri/src/download_manager/commands.rs @@ -0,0 +1,31 @@ +use std::sync::Mutex; + +use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState}; + +#[tauri::command] +pub fn pause_downloads(state: tauri::State<'_, Mutex>) { + state.lock().unwrap().download_manager.pause_downloads() +} + +#[tauri::command] +pub fn resume_downloads(state: tauri::State<'_, Mutex>) { + state.lock().unwrap().download_manager.resume_downloads() +} + +#[tauri::command] +pub fn move_download_in_queue( + state: tauri::State<'_, Mutex>, + old_index: usize, + new_index: usize, +) { + state + .lock() + .unwrap() + .download_manager + .rearrange(old_index, new_index) +} + +#[tauri::command] +pub fn cancel_game(state: tauri::State<'_, Mutex>, meta: DownloadableMetadata) { + state.lock().unwrap().download_manager.cancel(meta) +} diff --git a/src-tauri/src/download_manager/download_manager.rs b/src-tauri/src/download_manager/download_manager.rs index ec950f8..b3dee68 100644 --- a/src-tauri/src/download_manager/download_manager.rs +++ b/src-tauri/src/download_manager/download_manager.rs @@ -4,16 +4,21 @@ use std::{ fmt::Debug, sync::{ mpsc::{SendError, Sender}, - Arc, Mutex, MutexGuard, + Mutex, MutexGuard, }, thread::JoinHandle, }; -use log::info; +use log::{debug, info}; use serde::Serialize; +use crate::error::application_download_error::ApplicationDownloadError; -use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent}, downloadable_metadata::DownloadableMetadata, queue::Queue}; +use super::{ + download_manager_builder::{CurrentProgressObject, DownloadAgent}, + downloadable_metadata::DownloadableMetadata, + queue::Queue, +}; pub enum DownloadManagerSignal { /// Resumes (or starts) the DownloadManager @@ -79,7 +84,7 @@ pub enum DownloadStatus { /// which provides raw access to the underlying queue. /// THIS EDITING IS BLOCKING!!! pub struct DownloadManager { - terminator: JoinHandle>, + terminator: Mutex>>>, download_queue: Queue, progress: CurrentProgressObject, command_sender: Sender, @@ -94,7 +99,7 @@ impl DownloadManager { command_sender: Sender, ) -> Self { Self { - terminator, + terminator: Mutex::new(Some(terminator)), download_queue, progress, command_sender, @@ -103,10 +108,11 @@ impl DownloadManager { pub fn queue_download( &self, - download: DownloadAgent + download: DownloadAgent, ) -> Result<(), SendError> { - info!("Adding download id {:?}", download.metadata()); - self.command_sender.send(DownloadManagerSignal::Queue(download))?; + info!("creating download with meta {:?}", download.metadata()); + self.command_sender + .send(DownloadManagerSignal::Queue(download))?; self.command_sender.send(DownloadManagerSignal::Go) } pub fn edit(&self) -> MutexGuard<'_, VecDeque> { @@ -145,12 +151,14 @@ impl DownloadManager { .unwrap(); } - info!("moving {} to {}", current_index, new_index); + debug!( + "moving download at index {} to index {}", + current_index, new_index + ); let mut queue = self.edit(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); - info!("new queue: {:?}", queue); drop(queue); if needs_pause { @@ -159,6 +167,10 @@ impl DownloadManager { self.command_sender .send(DownloadManagerSignal::UpdateUIQueue) .unwrap(); + self.command_sender + .send(DownloadManagerSignal::Go) + .unwrap(); + } pub fn pause_downloads(&self) { self.command_sender @@ -168,11 +180,12 @@ impl DownloadManager { pub fn resume_downloads(&self) { self.command_sender.send(DownloadManagerSignal::Go).unwrap(); } - pub fn ensure_terminated(self) -> Result, Box> { + pub fn ensure_terminated(&self) -> Result, Box> { self.command_sender .send(DownloadManagerSignal::Finish) .unwrap(); - self.terminator.join() + let terminator = self.terminator.lock().unwrap().take(); + terminator.unwrap().join() } pub fn uninstall_application(&self, meta: DownloadableMetadata) { self.command_sender diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs index 406a500..dd43dc2 100644 --- a/src-tauri/src/download_manager/download_manager_builder.rs +++ b/src-tauri/src/download_manager/download_manager_builder.rs @@ -1,19 +1,28 @@ use std::{ collections::HashMap, - fs::remove_dir_all, sync::{ mpsc::{channel, Receiver, Sender}, - Arc, Mutex, RwLockWriteGuard, + Arc, Mutex, }, thread::{spawn, JoinHandle}, }; -use log::{error, info}; +use log::{debug, error, info, warn}; use tauri::{AppHandle, Emitter}; -use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}; +use crate::{ + error::application_download_error::ApplicationDownloadError, + games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}, +}; -use super::{application_download_error::ApplicationDownloadError, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, queue::Queue}; +use super::{ + download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, + download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, + downloadable::Downloadable, + downloadable_metadata::DownloadableMetadata, + progress_object::ProgressObject, + queue::Queue, +}; pub type DownloadAgent = Arc>; pub type CurrentProgressObject = Arc>>>; @@ -47,7 +56,7 @@ whichever download queue order is required. +----------------------------------------------------------------------------+ This download queue does not actually own any of the DownloadAgents. It is -simply a id-based reference system. The actual Agents are stored in the +simply an id-based reference system. The actual Agents are stored in the download_agent_registry HashMap, as ordering is no issue here. This is why appending or removing from the download_queue must be done via signals. @@ -115,7 +124,6 @@ impl DownloadManagerBuilder { let mut download_thread_lock = self.current_download_thread.lock().unwrap(); *download_thread_lock = None; drop(download_thread_lock); - } fn stop_and_wait_current_download(&self) { @@ -130,7 +138,6 @@ impl DownloadManagerBuilder { } } - fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.command_receiver.recv() { @@ -172,13 +179,13 @@ impl DownloadManagerBuilder { } } fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { - info!("Got signal Queue"); + debug!("got signal Queue"); let meta = download_agent.metadata(); - info!("Meta: {:?}", meta); + debug!("queue metadata: {:?}", meta); if self.download_queue.exists(meta.clone()) { - info!("Download with same ID already exists"); + warn!("download with same ID already exists"); return; } @@ -186,27 +193,37 @@ impl DownloadManagerBuilder { self.download_queue.append(meta.clone()); self.download_agent_registry.insert(meta, download_agent); - self.sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap(); + self.sender + .send(DownloadManagerSignal::UpdateUIQueue) + .unwrap(); } fn manage_go_signal(&mut self) { - info!("Got signal Go"); - if self.download_agent_registry.is_empty() { - info!("Download agent registry: {:?}", self.download_agent_registry.len()); - return; - } - - if self.current_download_agent.is_some() { - info!("Current download agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata()); + debug!("got signal Go"); + if self.download_agent_registry.is_empty() { + debug!( + "Download agent registry: {:?}", + self.download_agent_registry.len() + ); return; } - info!("Current download queue: {:?}", self.download_queue.read()); + if self.current_download_agent.is_some() { + if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() { + debug!( + "Current download agent: {:?}", + self.current_download_agent.as_ref().unwrap().metadata() + ); + return; + } + } + + debug!("current download queue: {:?}", self.download_queue.read()); // Should always be Some if the above two statements keep going let agent_data = self.download_queue.read().front().unwrap().clone(); - info!("Starting download for {:?}", agent_data); + info!("starting download for {:?}", agent_data); let download_agent = self .download_agent_registry @@ -226,17 +243,21 @@ impl DownloadManagerBuilder { match download_agent.download(&app_handle) { // Ok(true) is for completed and exited properly Ok(true) => { + debug!("download {:?} has completed", download_agent.metadata()); download_agent.on_complete(&app_handle); - sender.send(DownloadManagerSignal::Completed(download_agent.metadata())).unwrap(); - }, + sender + .send(DownloadManagerSignal::Completed(download_agent.metadata())) + .unwrap(); + } // Ok(false) is for incomplete but exited properly Ok(false) => { download_agent.on_incomplete(&app_handle); - }, + } Err(e) => { + error!("download {:?} has error {}", download_agent.metadata(), &e); download_agent.on_error(&app_handle, e.clone()); sender.send(DownloadManagerSignal::Error(e)).unwrap(); - }, + } } sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap(); })); @@ -246,7 +267,7 @@ impl DownloadManagerBuilder { active_control_flag.set(DownloadThreadControlFlag::Go); } fn manage_stop_signal(&mut self) { - info!("Got signal Stop"); + debug!("got signal Stop"); if let Some(active_control_flag) = self.active_control_flag.clone() { self.set_status(DownloadManagerStatus::Paused); @@ -254,10 +275,9 @@ impl DownloadManagerBuilder { } } fn manage_completed_signal(&mut self, meta: DownloadableMetadata) { - info!("Got signal Completed"); + debug!("got signal Completed"); if let Some(interface) = &self.current_download_agent { if interface.metadata() == meta { - info!("Popping consumed data"); self.remove_and_cleanup_front_download(&meta); } } @@ -265,18 +285,17 @@ impl DownloadManagerBuilder { self.sender.send(DownloadManagerSignal::Go).unwrap(); } fn manage_error_signal(&mut self, error: ApplicationDownloadError) { - info!("Got signal Error"); - let current_agent = self.current_download_agent.clone().unwrap(); - - current_agent.on_error(&self.app_handle, error.clone()); - - self.stop_and_wait_current_download(); - self.remove_and_cleanup_front_download(¤t_agent.metadata()); + debug!("got signal Error"); + if let Some(current_agent) = self.current_download_agent.clone() { + current_agent.on_error(&self.app_handle, error.clone()); + self.stop_and_wait_current_download(); + self.remove_and_cleanup_front_download(¤t_agent.metadata()); + } self.set_status(DownloadManagerStatus::Error(error)); } fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { - info!("Got signal Cancel"); + debug!("got signal Cancel"); if let Some(current_download) = &self.current_download_agent { if ¤t_download.metadata() == meta { @@ -287,33 +306,34 @@ impl DownloadManagerBuilder { self.download_queue.pop_front(); self.cleanup_current_download(); - info!("Current donwload queue: {:?}", self.download_queue.read()); + debug!("current download queue: {:?}", self.download_queue.read()); } // TODO: Collapse these two into a single if statement somehow - else { - if let Some(download_agent) = self.download_agent_registry.get(meta) { - info!("Object exists in registry"); - let index = self.download_queue.get_by_meta(meta); - if let Some(index) = index { - download_agent.on_cancelled(&self.app_handle); - let _ = self.download_queue.edit().remove(index).unwrap(); - let removed = self.download_agent_registry.remove(meta); - info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read()); - } - } - } - } - else { - if let Some(download_agent) = self.download_agent_registry.get(meta) { - info!("Object exists in registry"); + else if let Some(download_agent) = self.download_agent_registry.get(meta) { let index = self.download_queue.get_by_meta(meta); if let Some(index) = index { download_agent.on_cancelled(&self.app_handle); let _ = self.download_queue.edit().remove(index).unwrap(); let removed = self.download_agent_registry.remove(meta); - info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read()); + debug!( + "removed {:?} from queue {:?}", + removed.map(|x| x.metadata()), + self.download_queue.read() + ); } } + } else if let Some(download_agent) = self.download_agent_registry.get(meta) { + let index = self.download_queue.get_by_meta(meta); + if let Some(index) = index { + download_agent.on_cancelled(&self.app_handle); + let _ = self.download_queue.edit().remove(index).unwrap(); + let removed = self.download_agent_registry.remove(meta); + debug!( + "removed {:?} from queue {:?}", + removed.map(|x| x.metadata()), + self.download_queue.read() + ); + } } self.push_ui_queue_update(); } @@ -326,18 +346,19 @@ impl DownloadManagerBuilder { let queue = &self.download_queue.read(); let queue_objs = queue .iter() - .map(|(key)| { + .map(|key| { let val = self.download_agent_registry.get(key).unwrap(); QueueUpdateEventQueueData { - meta: DownloadableMetadata::clone(&key), + meta: DownloadableMetadata::clone(key), status: val.status(), - progress: val.progress().get_progress() - }}) + progress: val.progress().get_progress(), + current: val.progress().sum(), + max: val.progress().get_max(), + } + }) .collect(); - let event_data = QueueUpdateEvent { - queue: queue_objs, - }; + let event_data = QueueUpdateEvent { queue: queue_objs }; self.app_handle.emit("update_queue", event_data).unwrap(); } -} \ No newline at end of file +} diff --git a/src-tauri/src/download_manager/downloadable.rs b/src-tauri/src/download_manager/downloadable.rs index e917882..181b329 100644 --- a/src-tauri/src/download_manager/downloadable.rs +++ b/src-tauri/src/download_manager/downloadable.rs @@ -1,9 +1,12 @@ -use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}}; +use std::sync::Arc; use tauri::AppHandle; +use crate::error::application_download_error::ApplicationDownloadError; + use super::{ - application_download_error::ApplicationDownloadError, download_manager::{DownloadManagerSignal, DownloadStatus}, download_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject + download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl, + downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, }; pub trait Downloadable: Send + Sync { @@ -17,4 +20,4 @@ pub trait Downloadable: Send + Sync { fn on_complete(&self, app_handle: &AppHandle); fn on_incomplete(&self, app_handle: &AppHandle); fn on_cancelled(&self, app_handle: &AppHandle); -} \ No newline at end of file +} diff --git a/src-tauri/src/download_manager/downloadable_metadata.rs b/src-tauri/src/download_manager/downloadable_metadata.rs index 7512af4..790b669 100644 --- a/src-tauri/src/download_manager/downloadable_metadata.rs +++ b/src-tauri/src/download_manager/downloadable_metadata.rs @@ -5,7 +5,7 @@ pub enum DownloadType { Game, Tool, DLC, - Mod + Mod, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)] @@ -13,14 +13,14 @@ pub enum DownloadType { pub struct DownloadableMetadata { pub id: String, pub version: Option, - pub download_type: DownloadType + pub download_type: DownloadType, } impl DownloadableMetadata { pub fn new(id: String, version: Option, download_type: DownloadType) -> Self { Self { id, version, - download_type + download_type, } } -} \ No newline at end of file +} diff --git a/src-tauri/src/download_manager/generate_downloadable.rs b/src-tauri/src/download_manager/generate_downloadable.rs deleted file mode 100644 index 8f53185..0000000 --- a/src-tauri/src/download_manager/generate_downloadable.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::sync::Arc; - -use super::{download_manager_builder::DownloadAgent, downloadable_metadata::DownloadableMetadata}; - -pub fn generate_downloadable(meta: DownloadableMetadata) -> DownloadAgent { - todo!() -} \ No newline at end of file diff --git a/src-tauri/src/download_manager/internal_error.rs b/src-tauri/src/download_manager/internal_error.rs new file mode 100644 index 0000000..4864599 --- /dev/null +++ b/src-tauri/src/download_manager/internal_error.rs @@ -0,0 +1,27 @@ +use std::{fmt::Display, io, sync::mpsc::SendError}; + +use serde_with::SerializeDisplay; + +#[derive(SerializeDisplay)] +pub enum InternalError { + IOError(io::Error), + SignalError(SendError), +} +impl Display for InternalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InternalError::IOError(error) => write!(f, "{}", error), + InternalError::SignalError(send_error) => write!(f, "{}", send_error), + } + } +} +impl From> for InternalError { + fn from(value: SendError) -> Self { + InternalError::SignalError(value) + } +} +impl From for InternalError { + fn from(value: io::Error) -> Self { + InternalError::IOError(value) + } +} diff --git a/src-tauri/src/download_manager/mod.rs b/src-tauri/src/download_manager/mod.rs index 6299068..0bac198 100644 --- a/src-tauri/src/download_manager/mod.rs +++ b/src-tauri/src/download_manager/mod.rs @@ -1,9 +1,10 @@ +pub mod commands; pub mod download_manager; pub mod download_manager_builder; -pub mod progress_object; -pub mod queue; pub mod download_thread_control_flag; pub mod downloadable; -pub mod application_download_error; pub mod downloadable_metadata; -pub mod generate_downloadable; \ No newline at end of file +pub mod internal_error; +pub mod progress_object; +pub mod queue; +pub mod rolling_progress_updates; diff --git a/src-tauri/src/download_manager/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs index a3c3c24..fc0907b 100644 --- a/src-tauri/src/download_manager/progress_object.rs +++ b/src-tauri/src/download_manager/progress_object.rs @@ -2,14 +2,17 @@ use std::{ sync::{ atomic::{AtomicUsize, Ordering}, mpsc::Sender, - Arc, Mutex, RwLock, + Arc, Mutex, }, - time::Instant, + time::{Duration, Instant}, }; -use log::info; +use atomic_instant_full::AtomicInstant; +use throttle_my_fn::throttle; -use super::download_manager::DownloadManagerSignal; +use super::{ + download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow, +}; #[derive(Clone)] pub struct ProgressObject { @@ -17,11 +20,10 @@ pub struct ProgressObject { progress_instances: Arc>>>, start: Arc>, sender: Sender, - - points_towards_update: Arc, - points_to_push_update: Arc, - last_update: Arc>, - amount_last_update: Arc, + //last_update: Arc>, + last_update_time: Arc, + bytes_last_update: Arc, + rolling: RollingProgressWindow<250>, } pub struct ProgressHandle { @@ -42,72 +44,32 @@ impl ProgressHandle { pub fn add(&self, amount: usize) { self.progress .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); - self.progress_object.check_push_update(amount); + calculate_update(&self.progress_object); + } + pub fn skip(&self, amount: usize) { + self.progress + .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); + // Offset the bytes at last offset by this amount + self.progress_object + .bytes_last_update + .fetch_add(amount, Ordering::Relaxed); + // Dont' fire update } } -static PROGRESS_UPDATES: usize = 100; - impl ProgressObject { pub fn new(max: usize, length: usize, sender: Sender) -> Self { let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect()); // TODO: consolidate this calculation with the set_max function below - let points_to_push_update = max / PROGRESS_UPDATES; Self { max: Arc::new(Mutex::new(max)), progress_instances: Arc::new(arr), start: Arc::new(Mutex::new(Instant::now())), sender, - points_towards_update: Arc::new(AtomicUsize::new(0)), - points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)), - last_update: Arc::new(RwLock::new(Instant::now())), - amount_last_update: Arc::new(AtomicUsize::new(0)), - } - } - - pub fn check_push_update(&self, amount_added: usize) { - let current_amount = self - .points_towards_update - .fetch_add(amount_added, Ordering::Relaxed); - - let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed); - - if current_amount >= to_update { - self.points_towards_update - .fetch_sub(to_update, Ordering::Relaxed); - self.sender - .send(DownloadManagerSignal::UpdateUIQueue) - .unwrap(); - } - - let last_update = self.last_update.read().unwrap(); - let last_update_difference = Instant::now().duration_since(*last_update).as_millis(); - if last_update_difference > 1000 { - // push update - drop(last_update); - let mut last_update = self.last_update.write().unwrap(); - *last_update = Instant::now(); - drop(last_update); - - let current_amount = self.sum(); - let max = self.get_max(); - let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed); - self.amount_last_update - .store(current_amount, Ordering::Relaxed); - - let amount_since_last_update = current_amount - amount_at_last_update; - - let kilobytes_per_second = amount_since_last_update / (last_update_difference as usize).max(1); - - let remaining = max - current_amount; // bytes - let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1); - self.sender - .send(DownloadManagerSignal::UpdateUIStats( - kilobytes_per_second, - time_remaining, - )) - .unwrap(); + last_update_time: Arc::new(AtomicInstant::now()), + bytes_last_update: Arc::new(AtomicUsize::new(0)), + rolling: RollingProgressWindow::new(), } } @@ -127,9 +89,6 @@ impl ProgressObject { } pub fn set_max(&self, new_max: usize) { *self.max.lock().unwrap() = new_max; - self.points_to_push_update - .store(new_max / PROGRESS_UPDATES, Ordering::Relaxed); - info!("points to push update: {}", new_max / PROGRESS_UPDATES); } pub fn set_size(&self, length: usize) { *self.progress_instances.lock().unwrap() = @@ -141,4 +100,56 @@ impl ProgressObject { pub fn get(&self, index: usize) -> Arc { self.progress_instances.lock().unwrap()[index].clone() } + fn update_window(&self, kilobytes_per_second: usize) { + self.rolling.update(kilobytes_per_second); + } +} + +#[throttle(1, Duration::from_millis(20))] +pub fn calculate_update(progress: &ProgressObject) { + let last_update_time = progress + .last_update_time + .swap(Instant::now(), Ordering::SeqCst); + let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis(); + + let current_bytes_downloaded = progress.sum(); + let max = progress.get_max(); + let bytes_at_last_update = progress + .bytes_last_update + .swap(current_bytes_downloaded, Ordering::Relaxed); + + let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update; + + let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1); + + let bytes_remaining = max - current_bytes_downloaded; // bytes + + progress.update_window(kilobytes_per_second); + push_update(progress, bytes_remaining); +} + +#[throttle(1, Duration::from_millis(500))] +pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) { + let average_speed = progress.rolling.get_average(); + let time_remaining = (bytes_remaining / 1000) / average_speed.max(1); + + update_ui(progress, average_speed, time_remaining); + update_queue(progress); +} + +fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) { + progress_object + .sender + .send(DownloadManagerSignal::UpdateUIStats( + kilobytes_per_second, + time_remaining, + )) + .unwrap(); +} + +fn update_queue(progress: &ProgressObject) { + progress + .sender + .send(DownloadManagerSignal::UpdateUIQueue) + .unwrap(); } diff --git a/src-tauri/src/download_manager/queue.rs b/src-tauri/src/download_manager/queue.rs index cda08df..f3e9493 100644 --- a/src-tauri/src/download_manager/queue.rs +++ b/src-tauri/src/download_manager/queue.rs @@ -11,6 +11,12 @@ pub struct Queue { } #[allow(dead_code)] +impl Default for Queue { + fn default() -> Self { + Self::new() + } +} + impl Queue { pub fn new() -> Self { Self { @@ -26,7 +32,7 @@ impl Queue { pub fn pop_front(&self) -> Option { self.edit().pop_front() } - pub fn empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.inner.lock().unwrap().len() == 0 } pub fn exists(&self, meta: DownloadableMetadata) -> bool { @@ -44,15 +50,9 @@ impl Queue { pub fn append(&self, interface: DownloadableMetadata) { self.edit().push_back(interface); } - pub fn pop_front_if_equal( - &self, - meta: &DownloadableMetadata, - ) -> Option { + pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option { let mut queue = self.edit(); - let front = match queue.front() { - Some(front) => front, - None => return None, - }; + let front = queue.front()?; if front == meta { return queue.pop_front(); } @@ -61,7 +61,11 @@ impl Queue { pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option { self.read().iter().position(|data| data == meta) } - pub fn move_to_index_by_meta(&self, meta: &DownloadableMetadata, new_index: usize) -> Result<(), ()> { + pub fn move_to_index_by_meta( + &self, + meta: &DownloadableMetadata, + new_index: usize, + ) -> Result<(), ()> { let index = match self.get_by_meta(meta) { Some(index) => index, None => return Err(()), diff --git a/src-tauri/src/download_manager/rolling_progress_updates.rs b/src-tauri/src/download_manager/rolling_progress_updates.rs new file mode 100644 index 0000000..2239b9a --- /dev/null +++ b/src-tauri/src/download_manager/rolling_progress_updates.rs @@ -0,0 +1,33 @@ +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +#[derive(Clone)] +pub struct RollingProgressWindow { + window: Arc<[AtomicUsize; S]>, + current: Arc, +} +impl RollingProgressWindow { + pub fn new() -> Self { + Self { + window: Arc::new([(); S].map(|_| AtomicUsize::new(0))), + current: Arc::new(AtomicUsize::new(0)), + } + } + pub fn update(&self, kilobytes_per_second: usize) { + let index = self.current.fetch_add(1, Ordering::SeqCst); + let current = &self.window[index % S]; + current.store(kilobytes_per_second, Ordering::SeqCst); + } + pub fn get_average(&self) -> usize { + let current = self.current.load(Ordering::SeqCst); + self.window + .iter() + .enumerate() + .filter(|(i, _)| i < ¤t) + .map(|(_, x)| x.load(Ordering::Relaxed)) + .sum::() + / S + } +} diff --git a/src-tauri/src/download_manager/application_download_error.rs b/src-tauri/src/error/application_download_error.rs similarity index 55% rename from src-tauri/src/download_manager/application_download_error.rs rename to src-tauri/src/error/application_download_error.rs index 38c690d..d68bd71 100644 --- a/src-tauri/src/download_manager/application_download_error.rs +++ b/src-tauri/src/error/application_download_error.rs @@ -1,9 +1,14 @@ -use std::{fmt::{Display, Formatter}, io}; +use std::{ + fmt::{Display, Formatter}, + io, +}; -use crate::remote::RemoteAccessError; +use serde_with::SerializeDisplay; + +use super::{remote_access_error::RemoteAccessError, setup_error::SetupError}; // TODO: Rename / separate from downloads -#[derive(Debug, Clone)] +#[derive(Debug, Clone, SerializeDisplay)] pub enum ApplicationDownloadError { Communication(RemoteAccessError), Checksum, @@ -17,25 +22,11 @@ impl Display for ApplicationDownloadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ApplicationDownloadError::Communication(error) => write!(f, "{}", error), - ApplicationDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error), - ApplicationDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"), - ApplicationDownloadError::Checksum => write!(f, "Checksum failed to validate for download"), + ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", error), + ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"), + ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"), ApplicationDownloadError::IoError(error) => write!(f, "{}", error), - ApplicationDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"), + ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"), } } } - -#[derive(Debug, Clone)] -pub enum SetupError { - Context, -} - -impl Display for SetupError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - SetupError::Context => write!(f, "Failed to generate contexts for download"), - } - } -} - diff --git a/src-tauri/src/error/drop_server_error.rs b/src-tauri/src/error/drop_server_error.rs new file mode 100644 index 0000000..ab42263 --- /dev/null +++ b/src-tauri/src/error/drop_server_error.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DropServerError { + pub status_code: usize, + pub status_message: String, + pub message: String, + pub url: String, +} diff --git a/src-tauri/src/error/library_error.rs b/src-tauri/src/error/library_error.rs new file mode 100644 index 0000000..c13dd23 --- /dev/null +++ b/src-tauri/src/error/library_error.rs @@ -0,0 +1,19 @@ +use std::fmt::Display; + +use serde_with::SerializeDisplay; + +#[derive(SerializeDisplay)] +pub enum LibraryError { + MetaNotFound(String), +} +impl Display for LibraryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LibraryError::MetaNotFound(id) => write!( + f, + "Could not locate any installed version of game ID {} in the database", + id + ), + } + } +} diff --git a/src-tauri/src/error/mod.rs b/src-tauri/src/error/mod.rs new file mode 100644 index 0000000..89b74ae --- /dev/null +++ b/src-tauri/src/error/mod.rs @@ -0,0 +1,6 @@ +pub mod application_download_error; +pub mod drop_server_error; +pub mod library_error; +pub mod process_error; +pub mod remote_access_error; +pub mod setup_error; diff --git a/src-tauri/src/error/process_error.rs b/src-tauri/src/error/process_error.rs new file mode 100644 index 0000000..8afc9dc --- /dev/null +++ b/src-tauri/src/error/process_error.rs @@ -0,0 +1,31 @@ +use std::{fmt::Display, io::Error}; + +use serde_with::SerializeDisplay; + +#[derive(SerializeDisplay)] +pub enum ProcessError { + SetupRequired, + NotInstalled, + AlreadyRunning, + NotDownloaded, + InvalidID, + InvalidVersion, + IOError(Error), + InvalidPlatform, +} + +impl Display for ProcessError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + ProcessError::SetupRequired => "Game not set up", + ProcessError::NotInstalled => "Game not installed", + ProcessError::AlreadyRunning => "Game already running", + ProcessError::NotDownloaded => "Game not downloaded", + ProcessError::InvalidID => "Invalid Game ID", + ProcessError::InvalidVersion => "Invalid Game version", + ProcessError::IOError(error) => &error.to_string(), + ProcessError::InvalidPlatform => "This Game cannot be played on the current platform", + }; + write!(f, "{}", s) + } +} diff --git a/src-tauri/src/error/remote_access_error.rs b/src-tauri/src/error/remote_access_error.rs new file mode 100644 index 0000000..32572b2 --- /dev/null +++ b/src-tauri/src/error/remote_access_error.rs @@ -0,0 +1,69 @@ +use std::{ + error::Error, + fmt::{Display, Formatter}, + sync::Arc, +}; + +use http::StatusCode; +use serde_with::SerializeDisplay; +use url::ParseError; + +use super::drop_server_error::DropServerError; + +#[derive(Debug, Clone, SerializeDisplay)] +pub enum RemoteAccessError { + FetchError(Arc), + ParsingError(ParseError), + InvalidEndpoint, + HandshakeFailed(String), + GameNotFound, + InvalidResponse(DropServerError), + InvalidRedirect, + ManifestDownloadFailed(StatusCode, String), + OutOfSync, + Generic(String), +} + +impl Display for RemoteAccessError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RemoteAccessError::FetchError(error) => write!( + f, + "{}: {}", + error, + error + .source() + .map(|e| e.to_string()) + .or_else(|| Some("Unknown error".to_string())) + .unwrap() + ), + RemoteAccessError::ParsingError(parse_error) => { + write!(f, "{}", parse_error) + } + RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"), + RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message), + RemoteAccessError::GameNotFound => write!(f, "could not find game on server"), + RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message), + RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"), + RemoteAccessError::ManifestDownloadFailed(status, response) => write!( + f, + "failed to download game manifest: {} {}", + status, response + ), + RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"), + RemoteAccessError::Generic(message) => write!(f, "{}", message), + } + } +} + +impl From for RemoteAccessError { + fn from(err: reqwest::Error) -> Self { + RemoteAccessError::FetchError(Arc::new(err)) + } +} +impl From for RemoteAccessError { + fn from(err: ParseError) -> Self { + RemoteAccessError::ParsingError(err) + } +} +impl std::error::Error for RemoteAccessError {} diff --git a/src-tauri/src/error/setup_error.rs b/src-tauri/src/error/setup_error.rs new file mode 100644 index 0000000..bd76ce5 --- /dev/null +++ b/src-tauri/src/error/setup_error.rs @@ -0,0 +1,14 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Clone)] +pub enum SetupError { + Context, +} + +impl Display for SetupError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SetupError::Context => write!(f, "failed to generate contexts for download"), + } + } +} diff --git a/src-tauri/src/games/commands.rs b/src-tauri/src/games/commands.rs new file mode 100644 index 0000000..8f1d652 --- /dev/null +++ b/src-tauri/src/games/commands.rs @@ -0,0 +1,53 @@ +use std::sync::Mutex; + +use tauri::AppHandle; + +use crate::{ + database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState +}; + +use super::{ + library::{ + fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct, + Game, + }, + state::{GameStatusManager, GameStatusWithTransient}, +}; + +#[tauri::command] +pub fn fetch_library(app: AppHandle) -> Result, RemoteAccessError> { + fetch_library_logic(app) +} + +#[tauri::command] +pub fn fetch_game( + game_id: String, + app: tauri::AppHandle, +) -> Result { + fetch_game_logic(game_id, app) +} + +#[tauri::command] +pub fn fetch_game_status(id: String) -> GameStatusWithTransient { + GameStatusManager::fetch_state(&id) +} + +#[tauri::command] +pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> { + let meta = match get_current_meta(&game_id) { + Some(data) => data, + None => return Err(LibraryError::MetaNotFound(game_id)), + }; + println!("{:?}", meta); + uninstall_game_logic(meta, &app_handle); + + Ok(()) +} + +#[tauri::command] +pub fn fetch_game_verion_options( + game_id: String, + state: tauri::State<'_, Mutex>, +) -> Result, RemoteAccessError> { + fetch_game_verion_options_logic(game_id, state) +} diff --git a/src-tauri/src/games/downloads/commands.rs b/src-tauri/src/games/downloads/commands.rs new file mode 100644 index 0000000..67b1359 --- /dev/null +++ b/src-tauri/src/games/downloads/commands.rs @@ -0,0 +1,32 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ + download_manager::{ + download_manager::DownloadManagerSignal, downloadable::Downloadable, + internal_error::InternalError, + }, + AppState, +}; + +use super::download_agent::GameDownloadAgent; + +#[tauri::command] +pub fn download_game( + game_id: String, + game_version: String, + install_dir: usize, + state: tauri::State<'_, Mutex>, +) -> Result<(), InternalError> { + let sender = state.lock().unwrap().download_manager.get_sender(); + let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new( + game_id, + game_version, + install_dir, + sender, + )) as Box); + Ok(state + .lock() + .unwrap() + .download_manager + .queue_download(game_download_agent)?) +} diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs index 5a92380..d4fb1cd 100644 --- a/src-tauri/src/games/downloads/download_agent.rs +++ b/src-tauri/src/games/downloads/download_agent.rs @@ -1,25 +1,30 @@ use crate::auth::generate_authorization_header; -use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls}; -use crate::download_manager::application_download_error::ApplicationDownloadError; +use crate::database::db::{ + borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls, + GameDownloadStatus, +}; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; -use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; +use crate::download_manager::download_thread_control_flag::{ + DownloadThreadControl, DownloadThreadControlFlag, +}; use crate::download_manager::downloadable::Downloadable; use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}; use crate::download_manager::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::library::{on_game_complete, push_game_update}; -use crate::remote::RemoteAccessError; +use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent}; +use crate::remote::requests::make_request; use crate::DB; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; -use tauri::{AppHandle, Emitter}; -use std::collections::VecDeque; -use std::fs::{create_dir_all, remove_dir_all, File}; +use slice_deque::SliceDeque; +use std::fs::{create_dir_all, File}; use std::path::Path; use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; -use std::thread::spawn; use std::time::Instant; +use tauri::{AppHandle, Emitter}; use urlencoding::encode; #[cfg(target_os = "linux")] @@ -33,12 +38,12 @@ pub struct GameDownloadAgent { pub version: String, pub control_flag: DownloadThreadControl, contexts: Mutex>, - completed_contexts: Mutex>, + completed_contexts: Mutex>, pub manifest: Mutex>, pub progress: Arc, sender: Sender, pub stored_manifest: StoredManifest, - status: Mutex + status: Mutex, } impl GameDownloadAgent { @@ -51,7 +56,7 @@ impl GameDownloadAgent { // Don't run by default let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); - let db_lock = DB.borrow_data().unwrap(); + let db_lock = borrow_db_checked(); let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); drop(db_lock); @@ -67,7 +72,7 @@ impl GameDownloadAgent { control_flag, manifest: Mutex::new(None), contexts: Mutex::new(Vec::new()), - completed_contexts: Mutex::new(VecDeque::new()), + completed_contexts: Mutex::new(SliceDeque::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, stored_manifest, @@ -78,10 +83,8 @@ impl GameDownloadAgent { // Blocking pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> { self.ensure_manifest_exists()?; - info!("Ensured manifest exists"); self.ensure_contexts()?; - info!("Ensured contexts exists"); self.control_flag.set(DownloadThreadControlFlag::Go); @@ -90,16 +93,24 @@ impl GameDownloadAgent { // Blocking pub fn download(&self, app_handle: &AppHandle) -> Result { - info!("Setting up download"); self.setup_download()?; - info!("Setting progress object params"); self.set_progress_object_params(); - info!("Running"); let timer = Instant::now(); - push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() }))); - let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError); + push_game_update( + app_handle, + &self.metadata().id, + ( + None, + Some(ApplicationTransientStatus::Downloading { + version_name: self.version.clone(), + }), + ), + ); + let res = self + .run() + .map_err(|_| ApplicationDownloadError::DownloadError); - info!( + debug!( "{} took {}ms to download", self.id, timer.elapsed().as_millis() @@ -116,25 +127,17 @@ impl GameDownloadAgent { } fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { - let base_url = DB.fetch_base_url(); - let manifest_url = base_url - .join( - format!( - "/api/v1/client/metadata/manifest?id={}&version={}", - self.id, - encode(&self.version) - ) - .as_str(), - ) - .unwrap(); - let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = client - .get(manifest_url.to_string()) - .header("Authorization", header) - .send() - .unwrap(); + let response = make_request( + &client, + &["/api/v1/client/game/manifest"], + &[("id", &self.id), ("version", &self.version)], + |f| f.header("Authorization", header), + ) + .map_err(|e| ApplicationDownloadError::Communication(e))? + .send() + .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; if response.status() != 200 { return Err(ApplicationDownloadError::Communication( @@ -145,7 +148,7 @@ impl GameDownloadAgent { )); } - let manifest_download = response.json::().unwrap(); + let manifest_download: DropManifest = response.json().unwrap(); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); @@ -167,11 +170,8 @@ impl GameDownloadAgent { let chunk_count = contexts.iter().map(|chunk| chunk.length).sum(); - debug!("Setting ProgressObject max to {}", chunk_count); self.progress.set_max(chunk_count); - debug!("Setting ProgressObject size to {}", length); self.progress.set_size(length); - debug!("Setting ProgressObject time to now"); self.progress.set_time_now(); } @@ -192,12 +192,11 @@ impl GameDownloadAgent { let base_path = Path::new(&self.stored_manifest.base_path); create_dir_all(base_path).unwrap(); - { let mut completed_contexts_lock = self.completed_contexts.lock().unwrap(); completed_contexts_lock.clear(); completed_contexts_lock - .extend(self.stored_manifest.get_completed_contexts()); + .extend_from_slice(&self.stored_manifest.get_completed_contexts()); } for (raw_path, chunk) in manifest { @@ -234,22 +233,22 @@ impl GameDownloadAgent { Ok(()) } + // TODO: Change return value on Err pub fn run(&self) -> Result { - info!("downloading game: {}", self.id); - const DOWNLOAD_MAX_THREADS: usize = 1; + let max_download_threads = borrow_db_checked().settings.max_download_threads; + debug!( + "downloading game: {} with {} threads", + self.id, max_download_threads + ); let pool = ThreadPoolBuilder::new() - .num_threads(DOWNLOAD_MAX_THREADS) + .num_threads(max_download_threads) .build() .unwrap(); let completed_indexes = Arc::new(boxcar::Vec::new()); let completed_indexes_loop_arc = completed_indexes.clone(); - let base_url = DB.fetch_base_url(); - - - let contexts = self.contexts.lock().unwrap(); pool.scope(|scope| { let client = &reqwest::blocking::Client::new(); @@ -259,19 +258,36 @@ impl GameDownloadAgent { let progress = self.progress.get(index); let progress_handle = ProgressHandle::new(progress, self.progress.clone()); + // If we've done this one already, skip it if self.completed_contexts.lock().unwrap().contains(&index) { - progress_handle.add(context.length); + progress_handle.skip(context.length); continue; } let sender = self.sender.clone(); - let request = generate_request(&base_url, client, &context); - + 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.header("Authorization", generate_authorization_header()), + ) { + Ok(request) => request, + Err(e) => { + sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap(); + continue; + }, + }; scope.spawn(move |_| { - match download_game_chunk(context, &self.control_flag, progress_handle, request) { + match download_game_chunk(context, &self.control_flag, progress_handle, request) + { Ok(res) => { if res { completed_indexes.push(index); @@ -282,39 +298,35 @@ impl GameDownloadAgent { sender.send(DownloadManagerSignal::Error(e)).unwrap(); } } - info!("Completed context id {}", index); }); } }); let newly_completed = completed_indexes.to_owned(); - let completed_lock_len = { let mut completed_contexts_lock = self.completed_contexts.lock().unwrap(); - for (item, _) in newly_completed.iter() { - completed_contexts_lock.push_front(item); + for (_, item) in newly_completed.iter() { + completed_contexts_lock.push_front(*item); } completed_contexts_lock.len() }; - info!("Got newly completed"); - // If we're not out of contexts, we're not done, so we don't fire completed if completed_lock_len != contexts.len() { - info!("da for {} exited without completing", self.id.clone()); + info!( + "download agent for {} exited without completing ({}/{})", + self.id.clone(), + completed_lock_len, + contexts.len(), + ); self.stored_manifest - .set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into()); - info!("Setting completed contexts"); + .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice()); self.stored_manifest.write(); - info!("Wrote completed contexts"); return Ok(false); } - info!("Sending completed signal"); - - // We've completed self.sender .send(DownloadManagerSignal::Completed(self.metadata())) @@ -324,26 +336,6 @@ impl GameDownloadAgent { } } -fn generate_request(base_url: &url::Url, client: reqwest::blocking::Client, context: &DropDownloadContext) -> reqwest::blocking::RequestBuilder { - let chunk_url = base_url - .join(&format!( - "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}", - // Encode the parts we don't trust - context.game_id, - encode(&context.version), - encode(&context.file_name), - context.index - )) - .unwrap(); - - let header = generate_authorization_header(); - - let request = client - .get(chunk_url) - .header("Authorization", header); - request -} - impl Downloadable for GameDownloadAgent { fn download(&self, app_handle: &AppHandle) -> Result { *self.status.lock().unwrap() = DownloadStatus::Downloading; @@ -368,7 +360,6 @@ impl Downloadable for GameDownloadAgent { fn on_initialised(&self, _app_handle: &tauri::AppHandle) { *self.status.lock().unwrap() = DownloadStatus::Queued; - return; } fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) { @@ -382,23 +373,35 @@ impl Downloadable for GameDownloadAgent { set_game_status(app_handle, self.metadata(), |db_handle, meta| { db_handle.applications.transient_statuses.remove(meta); }); - } fn on_complete(&self, app_handle: &tauri::AppHandle) { - on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).unwrap(); + on_game_complete( + &self.metadata(), + self.stored_manifest.base_path.to_string_lossy().to_string(), + app_handle, + ) + .unwrap(); } - fn on_incomplete(&self, _app_handle: &tauri::AppHandle) { + // TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly + fn on_incomplete(&self, app_handle: &tauri::AppHandle) { + let meta = self.metadata(); *self.status.lock().unwrap() = DownloadStatus::Queued; - return; + app_handle + .emit( + &format!("update_game/{}", meta.id), + GameUpdateEvent { + game_id: meta.id.clone(), + status: (Some(GameDownloadStatus::Remote {}), None), + }, + ) + .unwrap(); } - fn on_cancelled(&self, _app_handle: &tauri::AppHandle) { - return; - } - + fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {} + fn status(&self) -> DownloadStatus { self.status.lock().unwrap().clone() } -} \ No newline at end of file +} diff --git a/src-tauri/src/games/downloads/download_commands.rs b/src-tauri/src/games/downloads/download_commands.rs deleted file mode 100644 index 2bbd5e8..0000000 --- a/src-tauri/src/games/downloads/download_commands.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use crate::{download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, AppState}; - -use super::download_agent::GameDownloadAgent; - -#[tauri::command] -pub fn download_game( - game_id: String, - game_version: String, - install_dir: usize, - state: tauri::State<'_, Mutex>, -) -> Result<(), String> { - let sender = state.lock().unwrap().download_manager.get_sender(); - let game_download_agent = Arc::new( - Box::new(GameDownloadAgent::new(game_id, game_version, install_dir, sender)) as Box - ); - state - .lock() - .unwrap() - .download_manager - .queue_download(game_download_agent) - .map_err(|_| "An error occurred while communicating with the download manager.".to_string()) -} - -#[tauri::command] -pub fn pause_game_downloads(state: tauri::State<'_, Mutex>) { - state.lock().unwrap().download_manager.pause_downloads() -} - -#[tauri::command] -pub fn resume_game_downloads(state: tauri::State<'_, Mutex>) { - state.lock().unwrap().download_manager.resume_downloads() -} - -#[tauri::command] -pub fn move_game_in_queue( - state: tauri::State<'_, Mutex>, - old_index: usize, - new_index: usize, -) { - state - .lock() - .unwrap() - .download_manager - .rearrange(old_index, new_index) -} - -#[tauri::command] -pub fn cancel_game(state: tauri::State<'_, Mutex>, meta: DownloadableMetadata) { - state.lock().unwrap().download_manager.cancel(meta) -} - -/* -#[tauri::command] -pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} -*/ - -/* -fn use_download_agent( - state: tauri::State<'_, Mutex>, - game_id: String, -) -> Result, String> { - let lock = state.lock().unwrap(); - let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?; - Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure -} -*/ diff --git a/src-tauri/src/games/downloads/download_logic.rs b/src-tauri/src/games/downloads/download_logic.rs index ec4e87f..70e5b7e 100644 --- a/src-tauri/src/games/downloads/download_logic.rs +++ b/src-tauri/src/games/downloads/download_logic.rs @@ -1,26 +1,23 @@ -use crate::auth::generate_authorization_header; -use crate::db::DatabaseImpls; -use crate::download_manager::application_download_error::ApplicationDownloadError; -use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; +use crate::download_manager::download_thread_control_flag::{ + DownloadThreadControl, DownloadThreadControlFlag, +}; use crate::download_manager::progress_object::ProgressHandle; +use crate::error::application_download_error::ApplicationDownloadError; +use crate::error::remote_access_error::RemoteAccessError; use crate::games::downloads::manifest::DropDownloadContext; -use crate::remote::{DropServerError, RemoteAccessError}; -use crate::DB; -use log::{error, info, warn}; +use log::warn; use md5::{Context, Digest}; -use reqwest::blocking::{Client, Request, RequestBuilder, Response}; +use reqwest::blocking::{RequestBuilder, Response}; use std::fs::{set_permissions, Permissions}; -use std::io::Read; +use std::io::{ErrorKind, Read}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use std::sync::Arc; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, Seek, SeekFrom, Write}, path::PathBuf, }; -use urlencoding::encode; pub struct DropWriter { hasher: Context, @@ -42,19 +39,17 @@ impl DropWriter { // Write automatically pushes to file and hasher impl Write for DropWriter { fn write(&mut self, buf: &[u8]) -> io::Result { - /* 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.hasher.flush()?; self.destination.flush() } } @@ -124,7 +119,7 @@ pub fn download_game_chunk( ctx: &DropDownloadContext, control_flag: &DownloadThreadControl, progress: ProgressHandle, - request: RequestBuilder + request: RequestBuilder, ) -> Result { // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { @@ -137,9 +132,9 @@ pub fn download_game_chunk( .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; if response.status() != 200 { - warn!("{}", response.text().unwrap()); + let err = response.json().unwrap(); return Err(ApplicationDownloadError::Communication( - RemoteAccessError::InvalidCodeError(400), + RemoteAccessError::InvalidResponse(err), )); } @@ -153,9 +148,9 @@ pub fn download_game_chunk( let content_length = response.content_length(); if content_length.is_none() { - error!("Recieved 0 length content from server"); + warn!("recieved 0 length content from server"); return Err(ApplicationDownloadError::Communication( - RemoteAccessError::InvalidResponse(response.json::().unwrap()), + RemoteAccessError::InvalidResponse(response.json().unwrap()), )); } @@ -181,16 +176,14 @@ pub fn download_game_chunk( set_permissions(ctx.path.clone(), permissions).unwrap(); } - /* let checksum = pipeline .finish() - .map_err(|e| GameDownloadError::IoError(e))?; + .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; let res = hex::encode(checksum.0); if res != ctx.checksum { - return Err(GameDownloadError::Checksum); + return Err(ApplicationDownloadError::Checksum); } - */ Ok(true) } diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs index ba0fac2..c9b3cd4 100644 --- a/src-tauri/src/games/downloads/mod.rs +++ b/src-tauri/src/games/downloads/mod.rs @@ -1,5 +1,5 @@ +pub mod commands; pub mod download_agent; -pub mod download_commands; mod download_logic; mod manifest; -mod stored_manifest; \ No newline at end of file +mod stored_manifest; diff --git a/src-tauri/src/games/downloads/stored_manifest.rs b/src-tauri/src/games/downloads/stored_manifest.rs index bb5e6b9..fdc232b 100644 --- a/src-tauri/src/games/downloads/stored_manifest.rs +++ b/src-tauri/src/games/downloads/stored_manifest.rs @@ -5,7 +5,7 @@ use std::{ sync::Mutex, }; -use log::error; +use log::{error, warn}; use serde::{Deserialize, Serialize}; use serde_binary::binary_stream::Endian; @@ -43,12 +43,10 @@ impl StoredManifest { } }; - - match serde_binary::from_vec::(s, Endian::Little) { Ok(manifest) => manifest, Err(e) => { - error!("{}", e); + warn!("{}", e); StoredManifest::new(game_id, game_version, base_path) } } @@ -72,8 +70,8 @@ impl StoredManifest { Err(e) => error!("{}", e), }; } - pub fn set_completed_contexts(&self, completed_contexts: &Vec) { - *self.completed_contexts.lock().unwrap() = completed_contexts.clone(); + pub fn set_completed_contexts(&self, completed_contexts: &[usize]) { + *self.completed_contexts.lock().unwrap() = completed_contexts.to_owned(); } pub fn get_completed_contexts(&self) -> Vec { self.completed_contexts.lock().unwrap().clone() diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs index 8d40274..b2f2fe2 100644 --- a/src-tauri/src/games/library.rs +++ b/src-tauri/src/games/library.rs @@ -2,20 +2,20 @@ use std::fs::remove_dir_all; use std::sync::Mutex; use std::thread::spawn; -use log::{error, info, warn}; +use log::{debug, error, warn}; use serde::{Deserialize, Serialize}; use tauri::Emitter; use tauri::{AppHandle, Manager}; -use urlencoding::encode; -use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus}; -use crate::db::GameVersion; +use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion}; +use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus}; use crate::download_manager::download_manager::DownloadStatus; use crate::download_manager::downloadable_metadata::DownloadableMetadata; -use crate::process::process_manager::Platform; -use crate::remote::RemoteAccessError; +use crate::error::remote_access_error::RemoteAccessError; use crate::games::state::{GameStatusManager, GameStatusWithTransient}; -use crate::{auth::generate_authorization_header, AppState, DB}; +use crate::remote::auth::generate_authorization_header; +use crate::remote::requests::make_request; +use crate::AppState; #[derive(serde::Serialize)] pub struct FetchGameStruct { @@ -40,7 +40,10 @@ pub struct Game { #[derive(serde::Serialize, Clone)] pub struct GameUpdateEvent { pub game_id: String, - pub status: (Option, Option), + pub status: ( + Option, + Option, + ), } #[derive(Serialize, Clone)] @@ -48,6 +51,8 @@ pub struct QueueUpdateEventQueueData { pub meta: DownloadableMetadata, pub status: DownloadStatus, pub progress: f64, + pub current: usize, + pub max: usize, } #[derive(serde::Serialize, Clone)] @@ -61,42 +66,27 @@ pub struct StatsUpdateEvent { pub time: usize, } -// Game version with some fields missing and size information -#[derive(serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GameVersionOption { - version_index: usize, - version_name: String, - platform: Platform, - setup_command: String, - launch_command: String, - delta: bool, - umu_id_override: Option, - // total_size: usize, -} - -fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { - let base_url = DB.fetch_base_url(); - let library_url = base_url.join("/api/v1/client/user/library")?; - +pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = client - .get(library_url.to_string()) - .header("Authorization", header) - .send()?; + let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| { + f.header("Authorization", header) + })? + .send()?; if response.status() != 200 { - return Err(response.status().as_u16().into()); + let err = response.json().unwrap(); + warn!("{:?}", err); + return Err(RemoteAccessError::InvalidResponse(err)); } - let games: Vec = response.json::>()?; + let games: Vec = response.json()?; let state = app.state::>(); let mut handle = state.lock().unwrap(); - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); for game in games.iter() { handle.games.insert(game.id.clone(), game.clone()); @@ -113,12 +103,7 @@ fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { Ok(games) } -#[tauri::command] -pub fn fetch_library(app: AppHandle) -> Result, String> { - fetch_library_logic(app).map_err(|e| e.to_string()) -} - -fn fetch_game_logic( +pub fn fetch_game_logic( id: String, app: tauri::AppHandle, ) -> Result { @@ -136,31 +121,25 @@ fn fetch_game_logic( return Ok(data); } - - let base_url = DB.fetch_base_url(); - - let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?; - let header = generate_authorization_header(); - let client = reqwest::blocking::Client::new(); - let response = client - .get(endpoint.to_string()) - .header("Authorization", header) - .send()?; + let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| { + r.header("Authorization", generate_authorization_header()) + })? + .send()?; if response.status() == 404 { return Err(RemoteAccessError::GameNotFound); } if response.status() != 200 { - return Err(RemoteAccessError::InvalidCodeError( - response.status().into(), - )); + let err = response.json().unwrap(); + warn!("{:?}", err); + return Err(RemoteAccessError::InvalidResponse(err)); } - let game = response.json::()?; + let game: Game = response.json()?; state_handle.games.insert(id.clone(), game.clone()); - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); db_handle .applications @@ -179,95 +158,62 @@ fn fetch_game_logic( Ok(data) } -#[tauri::command] -pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result { - let result = fetch_game_logic(game_id, app); - - if result.is_err() { - return Err(result.err().unwrap().to_string()); - } - - Ok(result.unwrap()) -} - -#[tauri::command] -pub fn fetch_game_status(id: String) -> Result { - let status = GameStatusManager::fetch_state(&id); - - Ok(status) -} - -fn fetch_game_verion_options_logic<'a>( +pub fn fetch_game_verion_options_logic( game_id: String, state: tauri::State<'_, Mutex>, -) -> Result, RemoteAccessError> { - let base_url = DB.fetch_base_url(); - - let endpoint = - base_url.join(format!("/api/v1/client/metadata/versions?id={}", game_id).as_str())?; - let header = generate_authorization_header(); - +) -> Result, RemoteAccessError> { let client = reqwest::blocking::Client::new(); - let response = client - .get(endpoint.to_string()) - .header("Authorization", header) - .send()?; + + let response = make_request( + &client, + &["/api/v1/client/game/versions"], + &[("id", &game_id)], + |r| r.header("Authorization", generate_authorization_header()), + )? + .send()?; if response.status() != 200 { - return Err(RemoteAccessError::InvalidCodeError( - response.status().into(), - )); + let err = response.json().unwrap(); + warn!("{:?}", err); + return Err(RemoteAccessError::InvalidResponse(err)); } - let data = response.json::>()?; + let data: Vec = response.json()?; let state_lock = state.lock().unwrap(); let process_manager_lock = state_lock.process_manager.lock().unwrap(); - let data = data + let data: Vec = data .into_iter() .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) - .collect::>(); + .collect(); drop(process_manager_lock); drop(state_lock); Ok(data) } -#[tauri::command] -pub fn uninstall_game( - game_id: String, - state: tauri::State<'_, Mutex>, - app_handle: AppHandle -) -> Result<(), String> { - let meta = get_current_meta(&game_id)?; - println!("{:?}", meta); - uninstall_game_logic(meta, &app_handle); - - Ok(()) -} - -fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { - println!("Triggered uninstall for agent"); - let mut db_handle = DB.borrow_data_mut().unwrap(); +pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { + println!("triggered uninstall for agent"); + let mut db_handle = borrow_db_mut_checked(); db_handle .applications .transient_statuses .entry(meta.clone()) .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {}); - + push_game_update( app_handle, - &meta, + &meta.id, (None, Some(ApplicationTransientStatus::Uninstalling {})), ); let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); if previous_state.is_none() { - info!("uninstall job doesn't have previous state, failing silently"); + warn!("uninstall job doesn't have previous state, failing silently"); return; } let previous_state = previous_state.unwrap(); - if let Some((version_name, install_dir)) = match previous_state { + if let Some((_, install_dir)) = match previous_state { GameDownloadStatus::Installed { version_name, install_dir, @@ -291,7 +237,7 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { error!("{}", e); } Ok(_) => { - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); db_handle.applications.transient_statuses.remove(&meta); db_handle .applications @@ -299,29 +245,26 @@ fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { .entry(meta.id.clone()) .and_modify(|e| *e = GameDownloadStatus::Remote {}); drop(db_handle); - DB.save().unwrap(); + save_db(); - info!("uninstalled game id {}", &meta.id); + debug!("uninstalled game id {}", &meta.id); - push_game_update(&app_handle, &meta, (Some(GameDownloadStatus::Remote {}), None)); + push_game_update( + &app_handle, + &meta.id, + (Some(GameDownloadStatus::Remote {}), None), + ); } }); } } -pub fn get_current_meta(game_id: &String) -> Result { - match DB.borrow_data().unwrap().applications.installed_game_version.get(game_id) { - Some(meta) => Ok(meta.clone()), - None => Err(String::from("Could not find installed version")), - } -} - -#[tauri::command] -pub fn fetch_game_verion_options<'a>( - game_id: String, - state: tauri::State<'_, Mutex>, -) -> Result, String> { - fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string()) +pub fn get_current_meta(game_id: &String) -> Option { + borrow_db_checked() + .applications + .installed_game_version + .get(game_id) + .cloned() } pub fn on_game_complete( @@ -330,28 +273,27 @@ pub fn on_game_complete( app_handle: &AppHandle, ) -> Result<(), RemoteAccessError> { // Fetch game version information from remote - let base_url = DB.fetch_base_url(); - if meta.version.is_none() { return Err(RemoteAccessError::GameNotFound) } + if meta.version.is_none() { + return Err(RemoteAccessError::GameNotFound); + } - let endpoint = base_url.join( - format!( - "/api/v1/client/metadata/version?id={}&version={}", - meta.id, - encode(meta.version.as_ref().unwrap()) - ) - .as_str(), - )?; let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = client - .get(endpoint.to_string()) - .header("Authorization", header) - .send()?; + let response = make_request( + &client, + &["/api/v1/client/metadata/version"], + &[ + ("id", &meta.id), + ("version", meta.version.as_ref().unwrap()), + ], + |f| f.header("Authorization", header), + )? + .send()?; - let data = response.json::()?; + let data: GameVersion = response.json()?; - let mut handle = DB.borrow_data_mut().unwrap(); + let mut handle = borrow_db_mut_checked(); handle .applications .game_versions @@ -364,7 +306,7 @@ pub fn on_game_complete( .insert(meta.id.clone(), meta.clone()); drop(handle); - DB.save().unwrap(); + save_db(); let status = if data.setup_command.is_empty() { GameDownloadStatus::Installed { @@ -378,13 +320,13 @@ pub fn on_game_complete( } }; - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); db_handle .applications .game_statuses .insert(meta.id.clone(), status.clone()); drop(db_handle); - DB.save().unwrap(); + save_db(); app_handle .emit( &format!("update_game/{}", meta.id), @@ -398,14 +340,14 @@ pub fn on_game_complete( Ok(()) } -pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) { +pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) { app_handle .emit( - &format!("update_game/{}", meta.id), + &format!("update_game/{}", game_id), GameUpdateEvent { - game_id: meta.id.clone(), + game_id: game_id.clone(), status, }, ) .unwrap(); -} \ No newline at end of file +} diff --git a/src-tauri/src/games/mod.rs b/src-tauri/src/games/mod.rs index e49a4ef..65c5c6b 100644 --- a/src-tauri/src/games/mod.rs +++ b/src-tauri/src/games/mod.rs @@ -1,3 +1,4 @@ +pub mod commands; pub mod downloads; pub mod library; -pub mod state; \ No newline at end of file +pub mod state; diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs index 778da84..19b1769 100644 --- a/src-tauri/src/games/state.rs +++ b/src-tauri/src/games/state.rs @@ -1,19 +1,16 @@ -use crate::{ - db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, DB -}; +use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus}; -pub type GameStatusWithTransient = (Option, Option); +pub type GameStatusWithTransient = ( + Option, + Option, +); pub struct GameStatusManager {} impl GameStatusManager { pub fn fetch_state(game_id: &String) -> GameStatusWithTransient { - let db_lock = DB.borrow_data().unwrap(); + let db_lock = borrow_db_checked(); let online_state = match db_lock.applications.installed_game_version.get(game_id) { - Some(meta) => db_lock - .applications - .transient_statuses - .get(meta) - .cloned(), + Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(), None => None, }; let offline_state = db_lock.applications.game_statuses.get(game_id).cloned(); @@ -29,4 +26,4 @@ impl GameStatusManager { (None, None) } -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5bc1538..1e374eb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,50 +1,54 @@ -mod auth; -mod db; +mod database; mod games; mod autostart; mod cleanup; -mod debug; +mod commands; +mod download_manager; +mod error; mod process; mod remote; -mod tools; -pub mod download_manager; -#[cfg(test)] -mod tests; -use crate::autostart::{get_autostart_enabled, toggle_autostart}; -use crate::db::DatabaseImpls; -use auth::{ - auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake, - retry_connect, sign_out, -}; +use crate::database::db::DatabaseImpls; +use autostart::{get_autostart_enabled, toggle_autostart}; use cleanup::{cleanup_and_exit, quit}; -use db::{ - add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus, - DATA_ROOT_DIR, +use commands::fetch_state; +use database::commands::{ + add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings, + fetch_system_data, update_settings, +}; +use database::db::{ + borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR, +}; +use download_manager::commands::{ + cancel_game, move_download_in_queue, pause_downloads, resume_downloads, }; -use debug::fetch_system_data; use download_manager::download_manager::DownloadManager; use download_manager::download_manager_builder::DownloadManagerBuilder; -use games::downloads::download_commands::{cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads}; +use games::commands::{ + fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, +}; +use games::downloads::commands::download_game; +use games::library::Game; use http::Response; use http::{header::*, response::Builder as ResponseBuilder}; -use games::library::{ - fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game -}; use log::{debug, info, warn, LevelFilter}; use log4rs::append::console::ConsoleAppender; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; use log4rs::Config; -use process::compat::CompatibilityManager; -use process::process_commands::{kill_game, launch_game}; +use process::commands::{kill_game, launch_game}; use process::process_manager::ProcessManager; -use remote::{gen_drop_url, use_remote}; +use remote::auth::{self, generate_authorization_header, recieve_handshake}; +use remote::commands::{ + auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote, +}; +use remote::requests::make_request; use serde::{Deserialize, Serialize}; -use tauri_plugin_dialog::DialogExt; +use std::env; use std::path::Path; +use std::str::FromStr; use std::sync::Arc; use std::{ collections::HashMap, @@ -52,8 +56,9 @@ use std::{ }; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIconBuilder; -use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent}; +use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; +use tauri_plugin_dialog::DialogExt; #[derive(Clone, Copy, Serialize)] pub enum AppStatus { @@ -86,29 +91,25 @@ pub struct AppState<'a> { download_manager: Arc, #[serde(skip_serializing)] process_manager: Arc>>, - #[serde(skip_serializing)] - compat_manager: Arc>, -} - -#[tauri::command] -fn fetch_state(state: tauri::State<'_, Mutex>>) -> Result { - let guard = state.lock().unwrap(); - let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?; - drop(guard); - Ok(cloned_state) } fn setup(handle: AppHandle) -> AppState<'static> { let logfile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}"))) + .encoder(Box::new(PatternEncoder::new( + "{d} | {l} | {f}:{L} - {m}{n}", + ))) .append(false) .build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log")) .unwrap(); let console = ConsoleAppender::builder() - .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}"))) + .encoder(Box::new(PatternEncoder::new( + "{d} | {l} | {f}:{L} - {m}{n}", + ))) .build(); + let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info")); + let config = Config::builder() .appenders(vec![ Appender::builder().build("logfile", Box::new(logfile)), @@ -117,7 +118,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { .build( Root::builder() .appenders(vec!["logfile", "console"]) - .build(LevelFilter::Info), + .build(LevelFilter::from_str(&log_level).expect("Invalid log level")), ) .unwrap(); @@ -126,9 +127,8 @@ fn setup(handle: AppHandle) -> AppState<'static> { let games = HashMap::new(); let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone())); let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone()))); - let compat_manager = Arc::new(Mutex::new(CompatibilityManager::new())); - debug!("Checking if database is set up"); + debug!("checking if database is set up"); let is_set_up = DB.database_is_set_up(); if !is_set_up { return AppState { @@ -137,22 +137,22 @@ fn setup(handle: AppHandle) -> AppState<'static> { games, download_manager, process_manager, - compat_manager, }; } - debug!("Database is set up"); + debug!("database is set up"); - let (app_status, user) = auth::setup().unwrap(); + // TODO: Account for possible failure + let (app_status, user) = auth::setup(); - let db_handle = DB.borrow_data().unwrap(); + let db_handle = borrow_db_checked(); let mut missing_games = Vec::new(); let statuses = db_handle.applications.game_statuses.clone(); drop(db_handle); for (game_id, status) in statuses.into_iter() { match status { - db::GameDownloadStatus::Remote {} => {} - db::GameDownloadStatus::SetupRequired { + database::db::GameDownloadStatus::Remote {} => {} + database::db::GameDownloadStatus::SetupRequired { version_name: _, install_dir, } => { @@ -161,7 +161,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { missing_games.push(game_id); } } - db::GameDownloadStatus::Installed { + database::db::GameDownloadStatus::Installed { version_name: _, install_dir, } => { @@ -175,7 +175,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { info!("detected games missing: {:?}", missing_games); - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); for game_id in missing_games { db_handle .applications @@ -186,12 +186,11 @@ fn setup(handle: AppHandle) -> AppState<'static> { drop(db_handle); - - info!("finished setup!"); + debug!("finished setup!"); // Sync autostart state if let Err(e) = autostart::sync_autostart_on_startup(&handle) { - warn!("Failed to sync autostart state: {}", e); + warn!("failed to sync autostart state: {}", e); } AppState { @@ -200,7 +199,6 @@ fn setup(handle: AppHandle) -> AppState<'static> { games, download_manager, process_manager, - compat_manager, } } @@ -227,6 +225,9 @@ pub fn run() { fetch_state, quit, fetch_system_data, + // User utils + update_settings, + fetch_settings, // Auth auth_initiate, retry_connect, @@ -245,9 +246,9 @@ pub fn run() { fetch_game_verion_options, // Downloads download_game, - move_game_in_queue, - pause_game_downloads, - resume_game_downloads, + move_download_in_queue, + pause_downloads, + resume_downloads, cancel_game, uninstall_game, // Processes @@ -265,14 +266,14 @@ pub fn run() { .setup(|app| { let handle = app.handle().clone(); let state = setup(handle); - info!("initialized drop client"); + debug!("initialized drop client"); app.manage(Mutex::new(state)); #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { use tauri_plugin_deep_link::DeepLinkExt; app.deep_link().register_all()?; - info!("registered all pre-defined deep links"); + debug!("registered all pre-defined deep links"); } let handle = app.handle().clone(); @@ -292,7 +293,7 @@ pub fn run() { .unwrap(); app.deep_link().on_open_url(move |event| { - info!("handling drop:// url"); + debug!("handling drop:// url"); let binding = event.urls(); let url = binding.first().unwrap(); if url.host_str().unwrap() == "handshake" { @@ -322,48 +323,50 @@ pub fn run() { app.webview_windows().get("main").unwrap().show().unwrap(); } "quit" => { - cleanup_and_exit(app); + cleanup_and_exit(app, &app.state()); } _ => { - println!("Menu event not handled: {:?}", event.id); + println!("menu event not handled: {:?}", event.id); } }) .build(app) .expect("error while setting up tray menu"); { - let mut db_handle = DB.borrow_data_mut().unwrap(); + 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().to_string()); + warn!( + "Database corrupted. Original file at {}", + original + .canonicalize() + .unwrap() + .to_string_lossy() + .to_string() + ); app.dialog() - .message("Database corrupted. A copy has been saved at: ".to_string() + original.to_str().unwrap()) + .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| { - let base_url = DB.fetch_base_url(); - // Drop leading / let object_id = &request.uri().path()[1..]; - let object_url = base_url - .join("/api/v1/client/object/") - .unwrap() - .join(object_id) - .unwrap(); - let header = generate_authorization_header(); let client: reqwest::blocking::Client = reqwest::blocking::Client::new(); - let response = client - .get(object_url.to_string()) - .header("Authorization", header) - .send(); + let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| { + f.header("Authorization", header) + }) + .unwrap() + .send(); if response.is_err() { warn!( "failed to fetch object with error: {}", @@ -392,7 +395,7 @@ pub fn run() { .build(tauri::generate_context!()) .expect("error while running tauri application"); - app.run(|app_handle, event| { + app.run(|_app_handle, event| { if let RunEvent::ExitRequested { code, api, .. } = event { if code.is_none() { api.prevent_exit(); diff --git a/src-tauri/src/process/commands.rs b/src-tauri/src/process/commands.rs new file mode 100644 index 0000000..8a213a4 --- /dev/null +++ b/src-tauri/src/process/commands.rs @@ -0,0 +1,40 @@ +use std::sync::Mutex; + +use crate::{error::process_error::ProcessError, AppState}; + +#[tauri::command] +pub fn launch_game( + id: String, + state: tauri::State<'_, Mutex>, +) -> Result<(), ProcessError> { + let state_lock = state.lock().unwrap(); + let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); + + //let meta = DownloadableMetadata { + // id, + // version: Some(version), + // download_type: DownloadType::Game, + //}; + + match process_manager_lock.launch_process(id) { + Ok(_) => {} + Err(e) => return Err(e), + }; + + drop(process_manager_lock); + drop(state_lock); + + Ok(()) +} + +#[tauri::command] +pub fn kill_game( + game_id: String, + state: tauri::State<'_, Mutex>, +) -> Result<(), ProcessError> { + let state_lock = state.lock().unwrap(); + let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); + process_manager_lock + .kill_game(game_id) + .map_err(ProcessError::IOError) +} diff --git a/src-tauri/src/process/compat.rs b/src-tauri/src/process/compat.rs index 65e8ece..2b4f307 100644 --- a/src-tauri/src/process/compat.rs +++ b/src-tauri/src/process/compat.rs @@ -1,51 +1,13 @@ -use std::{ - fs::create_dir_all, - path::PathBuf, - sync::atomic::{AtomicBool, Ordering}, -}; +// Since this code isn't being used, we can either: +// 1. Delete the entire file if compatibility features are not planned +// 2. Or add a TODO comment if planning to implement later -use crate::db::DATA_ROOT_DIR; - -pub struct CompatibilityManager { - compat_tools_path: PathBuf, - prefixes_path: PathBuf, - created_paths: AtomicBool, -} +// Option 1: Delete the file +// Delete src-tauri/src/process/compat.rs +// Option 2: Add TODO comment /* -This gets built into both the Windows & Linux client, but -we only need it in the Linux client. Therefore, it should -do nothing but take a little bit of memory if we're on -Windows. +TODO: Compatibility layer for running Windows games on Linux +This module is currently unused but reserved for future implementation +of Windows game compatibility features on Linux. */ -impl CompatibilityManager { - pub fn new() -> Self { - let root_dir_lock = DATA_ROOT_DIR.lock().unwrap(); - let compat_tools_path = root_dir_lock.join("compatibility_tools"); - let prefixes_path = root_dir_lock.join("prefixes"); - drop(root_dir_lock); - - Self { - compat_tools_path, - prefixes_path, - created_paths: AtomicBool::new(false), - } - } - - fn ensure_paths_exist(&self) -> Result<(), String> { - if self.created_paths.fetch_and(true, Ordering::Relaxed) { - return Ok(()); - } - if !self.compat_tools_path.exists() { - create_dir_all(self.compat_tools_path.clone()).map_err(|e| e.to_string())?; - } - if !self.prefixes_path.exists() { - create_dir_all(self.prefixes_path.clone()).map_err(|e| e.to_string())?; - } - self.created_paths.store(true, Ordering::Relaxed); - - Ok(()) - } - - -} diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs index 85692c9..6a1aed5 100644 --- a/src-tauri/src/process/mod.rs +++ b/src-tauri/src/process/mod.rs @@ -1,3 +1,3 @@ +pub mod commands; pub mod compat; -pub mod process_commands; pub mod process_manager; diff --git a/src-tauri/src/process/process_commands.rs b/src-tauri/src/process/process_commands.rs deleted file mode 100644 index c74ff8b..0000000 --- a/src-tauri/src/process/process_commands.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::sync::Mutex; - -use crate::{db::GameDownloadStatus, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, games::library::get_current_meta, AppState, DB}; - -#[tauri::command] -pub fn launch_game( - id: String, - state: tauri::State<'_, Mutex>, -) -> Result<(), String> { - let state_lock = state.lock().unwrap(); - let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); - - let version = match DB.borrow_data().unwrap().applications.game_statuses.get(&id).cloned() { - Some(GameDownloadStatus::Installed { version_name, install_dir }) => version_name, - Some(GameDownloadStatus::SetupRequired { version_name, install_dir }) => return Err(String::from("Game setup still required")), - _ => return Err(String::from("Game not installed")) - }; - - - let meta = DownloadableMetadata { id, version: Some(version), download_type: DownloadType::Game }; - - - process_manager_lock.launch_process(meta)?; - - drop(process_manager_lock); - drop(state_lock); - - Ok(()) -} - -#[tauri::command] -pub fn kill_game( - game_id: String, - state: tauri::State<'_, Mutex>, -) -> Result<(), String> { - let meta = get_current_meta(&game_id)?; - let state_lock = state.lock().unwrap(); - let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); - process_manager_lock.kill_game(meta).map_err(|x| x.to_string()) -} \ No newline at end of file diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index c376037..4d8ae12 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -1,27 +1,33 @@ use std::{ collections::HashMap, fs::{File, OpenOptions}, - io, + io::{self, Error}, path::{Path, PathBuf}, process::{Child, Command, ExitStatus}, sync::{Arc, Mutex}, thread::spawn, }; -use log::{info, warn}; +use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; use shared_child::SharedChild; use tauri::{AppHandle, Manager}; use umu_wrapper_lib::command_builder::UmuCommandBuilder; use crate::{ - db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, games::library::push_game_update, games::state::GameStatusManager, AppState, DB + database::db::{ + borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR + }, + download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, + error::process_error::ProcessError, + games::{library::push_game_update, state::GameStatusManager}, + AppState, DB, }; pub struct ProcessManager<'a> { current_platform: Platform, log_output_dir: PathBuf, - processes: HashMap>, + processes: HashMap>, app_handle: AppHandle, game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>, } @@ -60,13 +66,8 @@ impl ProcessManager<'_> { } } - // There's no easy way to distinguish between an executable name with - // spaces and it's arguments. - // I think if we just join the install_dir to whatever the user provides us, we'll be alright - // In future, we should have a separate field for executable name and it's arguments - fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec) { - // let command_components = raw_command.split(" ").collect::>(); - let root = raw_command; + fn process_command(&self, install_dir: &String, command: Vec) -> (PathBuf, Vec) { + let root = &command[0]; let install_dir = Path::new(install_dir); let absolute_exe = install_dir.join(root); @@ -79,34 +80,40 @@ impl ProcessManager<'_> { */ (absolute_exe, Vec::new()) } - pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> { - return match self.processes.get(&meta) { + pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> { + match self.processes.get(&game_id) { Some(child) => { child.kill()?; child.wait()?; Ok(()) - }, + } None => Err(io::Error::new( io::ErrorKind::NotFound, "Game ID not running", )), - }; + } } - fn on_process_finish(&mut self, meta: DownloadableMetadata, result: Result) { - if !self.processes.contains_key(&meta) { + fn on_process_finish(&mut self, game_id: String, result: Result) { + if !self.processes.contains_key(&game_id) { warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result); return; } - info!("process for {:?} exited with {:?}", meta, result); + debug!("process for {:?} exited with {:?}", &game_id, result); - self.processes.remove(&meta); + self.processes.remove(&game_id); - let mut db_handle = DB.borrow_data_mut().unwrap(); + let mut db_handle = borrow_db_mut_checked(); + let meta = db_handle + .applications + .installed_game_version + .get(&game_id) + .cloned() + .unwrap(); db_handle.applications.transient_statuses.remove(&meta); - let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); + let current_state = db_handle.applications.game_statuses.get(&game_id).cloned(); if let Some(saved_state) = current_state { if let GameDownloadStatus::SetupRequired { version_name, @@ -116,7 +123,7 @@ impl ProcessManager<'_> { if let Ok(exit_code) = result { if exit_code.success() { db_handle.applications.game_statuses.insert( - meta.id.clone(), + game_id.clone(), GameDownloadStatus::Installed { version_name: version_name.to_string(), install_dir: install_dir.to_string(), @@ -128,9 +135,9 @@ impl ProcessManager<'_> { } drop(db_handle); - let status = GameStatusManager::fetch_state(&meta.id); + let status = GameStatusManager::fetch_state(&game_id); - push_game_update(&self.app_handle, &meta, status); + push_game_update(&self.app_handle, &game_id, status); // TODO better management } @@ -142,59 +149,86 @@ impl ProcessManager<'_> { .contains_key(&(current.clone(), platform.clone()))) } - pub fn launch_process(&mut self, meta: DownloadableMetadata) -> Result<(), String> { - if self.processes.contains_key(&meta) { - return Err("Game or setup is already running.".to_owned()); + pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> { + if self.processes.contains_key(&game_id) { + return Err(ProcessError::AlreadyRunning); } - let mut db_lock = DB.borrow_data_mut().unwrap(); - info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions); + let version = match DB + .borrow_data() + .unwrap() + .applications + .game_statuses + .get(&game_id) + .cloned() + { + Some(GameDownloadStatus::Installed { version_name, .. }) => version_name, + Some(GameDownloadStatus::SetupRequired { .. }) => { + return Err(ProcessError::SetupRequired) + } + _ => return Err(ProcessError::NotInstalled), + }; + let meta = DownloadableMetadata { + id: game_id.clone(), + version: Some(version.clone()), + download_type: DownloadType::Game, + }; + + let mut db_lock = borrow_db_mut_checked(); + debug!( + "Launching process {:?} with games {:?}", + &game_id, db_lock.applications.game_versions + ); let game_status = db_lock .applications .game_statuses - .get(&meta.id) - .ok_or("Game not installed")?; + .get(&game_id) + .ok_or(ProcessError::NotInstalled)?; - let status_metadata: Option<(&String, &String)> = match game_status { + let (version_name, install_dir) = match game_status { GameDownloadStatus::Installed { version_name, install_dir, - } => Some((version_name, install_dir)), + } => (version_name, install_dir), GameDownloadStatus::SetupRequired { version_name, install_dir, - } => Some((version_name, install_dir)), - _ => None, + } => (version_name, install_dir), + _ => return Err(ProcessError::NotDownloaded), }; - if status_metadata.is_none() { - return Err("Game has not been downloaded.".to_owned()); - } - - let (version_name, install_dir) = status_metadata.unwrap(); let game_version = db_lock .applications .game_versions - .get(&meta.id) - .ok_or("Invalid game ID".to_owned())? + .get(&game_id) + .ok_or(ProcessError::InvalidID)? .get(version_name) - .ok_or("Invalid version name".to_owned())?; + .ok_or(ProcessError::InvalidVersion)?; - let raw_command: String = match game_status { + let mut command: Vec = Vec::new(); + + match game_status { GameDownloadStatus::Installed { version_name: _, install_dir: _, - } => game_version.launch_command.clone(), + } => { + command.extend([game_version.launch_command.clone()]); + command.extend(game_version.launch_args.clone()); + }, GameDownloadStatus::SetupRequired { version_name: _, install_dir: _, - } => game_version.setup_command.clone(), + } => { + command.extend([game_version.setup_command.clone()]); + command.extend(game_version.setup_args.clone()); + }, _ => panic!("unreachable code"), }; + info!("Command: {:?}", &command); - let (command, args) = self.process_command(install_dir, raw_command); + let (command, args) = self.process_command(install_dir, command); let target_current_dir = command.parent().unwrap().to_str().unwrap(); @@ -210,11 +244,13 @@ impl ProcessManager<'_> { .truncate(true) .read(true) .create(true) - .open( - self.log_output_dir - .join(format!("{}-{}-{}.log", meta.id.clone(), meta.version.clone().unwrap_or_default(), current_time.timestamp())), - ) - .map_err(|v| v.to_string())?; + .open(self.log_output_dir.join(format!( + "{}-{}-{}.log", + &game_id, + &version, + current_time.timestamp() + ))) + .map_err(ProcessError::IOError)?; let error_file = OpenOptions::new() .write(true) @@ -223,11 +259,11 @@ impl ProcessManager<'_> { .create(true) .open(self.log_output_dir.join(format!( "{}-{}-{}-error.log", - meta.id.clone(), - meta.version.clone().unwrap_or_default(), + &game_id, + &version, current_time.timestamp() ))) - .map_err(|v| v.to_string())?; + .map_err(ProcessError::IOError)?; let current_platform = self.current_platform.clone(); let target_platform = game_version.platform.clone(); @@ -235,20 +271,21 @@ impl ProcessManager<'_> { let game_launcher = self .game_launchers .get(&(current_platform, target_platform)) - .ok_or("Invalid version for this platform.") - .map_err(|e| e.to_string())?; + .ok_or(ProcessError::InvalidPlatform)?; - let launch_process = game_launcher.launch_process( - &meta, - command.to_str().unwrap().to_owned(), - args, - &target_current_dir.to_string(), - log_file, - error_file, - )?; + let launch_process = game_launcher + .launch_process( + &meta, + command.to_string_lossy().to_string(), + game_version, + target_current_dir, + log_file, + error_file, + ) + .map_err(ProcessError::IOError)?; let launch_process_handle = - Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?); + Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?); db_lock .applications @@ -257,7 +294,7 @@ impl ProcessManager<'_> { push_game_update( &self.app_handle, - &meta, + &meta.id, (None, Some(ApplicationTransientStatus::Running {})), ); @@ -272,7 +309,7 @@ impl ProcessManager<'_> { let app_state_handle = app_state.lock().unwrap(); let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap(); - process_manager_handle.on_process_finish(wait_thread_game_id, result); + process_manager_handle.on_process_finish(wait_thread_game_id.id, result); // As everything goes out of scope, they should get dropped // But just to explicit about it @@ -280,10 +317,7 @@ impl ProcessManager<'_> { drop(app_state_handle); }); - self.processes.insert(meta, wait_thread_handle); - - info!("finished spawning process"); - + self.processes.insert(meta.id, wait_thread_handle); Ok(()) } } @@ -298,32 +332,31 @@ pub trait ProcessHandler: Send + 'static { fn launch_process( &self, meta: &DownloadableMetadata, - command: String, - args: Vec, - current_dir: &String, + launch_command: String, + game_version: &GameVersion, + current_dir: &str, log_file: File, error_file: File, - ) -> Result; + ) -> Result; } struct NativeGameLauncher; impl ProcessHandler for NativeGameLauncher { fn launch_process( &self, - meta: &DownloadableMetadata, - command: String, - args: Vec, - current_dir: &String, + _meta: &DownloadableMetadata, + launch_command: String, + game_version: &GameVersion, + current_dir: &str, log_file: File, error_file: File, - ) -> Result { - Command::new(command) + ) -> Result { + Command::new(PathBuf::from(launch_command)) .current_dir(current_dir) .stdout(log_file) .stderr(error_file) - .args(args) + .args(game_version.launch_args.clone()) .spawn() - .map_err(|v| v.to_string()) } } @@ -332,18 +365,23 @@ struct UMULauncher; impl ProcessHandler for UMULauncher { fn launch_process( &self, - meta: &DownloadableMetadata, - command: String, - args: Vec, - current_dir: &String, - log_file: File, - error_file: File, - ) -> Result { - UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command) - .game_id(String::from("0")) - .launch_args(args) + _meta: &DownloadableMetadata, + launch_command: String, + game_version: &GameVersion, + _current_dir: &str, + _log_file: File, + _error_file: File, + ) -> Result { + println!("Game override: .{:?}.", &game_version.umu_id_override); + let game_id = match &game_version.umu_id_override { + Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) , + None => game_version.game_id.clone() + }; + info!("Game ID: {}", game_id); + UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, launch_command) + .game_id(game_id) + .launch_args(game_version.launch_args.clone()) .build() .spawn() - .map_err(|x| x.to_string()) } } diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs deleted file mode 100644 index 04c56db..0000000 --- a/src-tauri/src/remote.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::{ - error::Error, - fmt::{Display, Formatter}, - sync::{Arc, Mutex}, -}; - -use http::StatusCode; -use log::{info, warn}; -use reqwest::blocking::Response; -use serde::Deserialize; -use url::{ParseError, Url}; - -use crate::{AppState, AppStatus, DB}; - -#[derive(Debug, Clone)] -pub enum RemoteAccessError { - FetchError(Arc), - ParsingError(ParseError), - InvalidCodeError(u16), - InvalidEndpoint, - HandshakeFailed(String), - GameNotFound, - InvalidResponse(DropServerError), - InvalidRedirect, - ManifestDownloadFailed(StatusCode, String), - OutOfSync, - Generic(String), -} - -impl Display for RemoteAccessError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - RemoteAccessError::FetchError(error) => write!( - f, - "{}: {}", - error, - error - .source() - .map(|e| e.to_string()) - .or_else(|| Some("Unknown error".to_string())) - .unwrap() - ), - RemoteAccessError::ParsingError(parse_error) => { - write!(f, "{}", parse_error) - } - RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error), - RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"), - RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message), - RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"), - RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message), - RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"), - RemoteAccessError::ManifestDownloadFailed(status, response) => write!( - f, - "Failed to download game manifest: {} {}", - status, response - ), - RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."), - RemoteAccessError::Generic(message) => write!(f, "{}", message), - } - } -} - -impl From for RemoteAccessError { - fn from(err: reqwest::Error) -> Self { - RemoteAccessError::FetchError(Arc::new(err)) - } -} -impl From for RemoteAccessError { - fn from(err: ParseError) -> Self { - RemoteAccessError::ParsingError(err) - } -} -impl From for RemoteAccessError { - fn from(err: u16) -> Self { - RemoteAccessError::InvalidCodeError(err) - } -} - -impl std::error::Error for RemoteAccessError {} - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct DropServerError { - pub status_code: usize, - pub status_message: String, - pub message: String, - pub url: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct DropHealthcheck { - app_name: String, -} - -async fn use_remote_logic<'a>( - url: String, - state: tauri::State<'_, Mutex>>, -) -> Result<(), RemoteAccessError> { - info!("connecting to url {}", url); - let base_url = Url::parse(&url)?; - - // Test Drop url - let test_endpoint = base_url.join("/api/v1")?; - let response = reqwest::get(test_endpoint.to_string()).await?; - - let result = response.json::().await?; - - if result.app_name != "Drop" { - warn!("user entered drop endpoint that connected, but wasn't identified as Drop"); - return Err(RemoteAccessError::InvalidEndpoint); - } - - let mut app_state = state.lock().unwrap(); - app_state.status = AppStatus::SignedOut; - drop(app_state); - - let mut db_state = DB.borrow_data_mut().unwrap(); - db_state.base_url = base_url.to_string(); - drop(db_state); - - DB.save().unwrap(); - - Ok(()) -} - -#[tauri::command] -pub async fn use_remote<'a>( - url: String, - state: tauri::State<'_, Mutex>>, -) -> Result<(), String> { - let result = use_remote_logic(url, state).await; - - if result.is_err() { - return Err(result.err().unwrap().to_string()); - } - - Ok(()) -} - -#[tauri::command] -pub fn gen_drop_url(path: String) -> Result { - let base_url = { - let handle = DB.borrow_data().unwrap(); - - if handle.base_url.is_empty() { - return Ok("".to_string()); - }; - - Url::parse(&handle.base_url).unwrap() - }; - - let url = base_url.join(&path).unwrap(); - - Ok(url.to_string()) -} diff --git a/src-tauri/src/auth.rs b/src-tauri/src/remote/auth.rs similarity index 58% rename from src-tauri/src/auth.rs rename to src-tauri/src/remote/auth.rs index 7044140..d2594de 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/remote/auth.rs @@ -1,18 +1,22 @@ use std::{env, sync::Mutex}; use chrono::Utc; -use log::{info, warn}; +use log::{debug, error, warn}; use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer}; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ - db::{DatabaseAuth, DatabaseImpls}, - remote::{DropServerError, RemoteAccessError}, + database::db::{ + borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls, + }, + error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, AppState, AppStatus, User, DB, }; +use super::requests::make_request; + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct InitiateRequestBody { @@ -35,6 +39,7 @@ struct HandshakeResponse { id: String, } +// TODO: Change return value on Err pub fn sign_nonce(private_key: String, nonce: String) -> Result { let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap(); let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap(); @@ -50,7 +55,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result { pub fn generate_authorization_header() -> String { let certs = { - let db = DB.borrow_data().unwrap(); + let db = borrow_db_checked(); db.auth.clone().unwrap() }; @@ -64,29 +69,25 @@ pub fn generate_authorization_header() -> String { pub fn fetch_user() -> Result { let base_url = DB.fetch_base_url(); - let endpoint = base_url.join("/api/v1/client/user")?; let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = client - .get(endpoint.to_string()) - .header("Authorization", header) - .send()?; - + let response = make_request(&client, &["/api/v1/client/user"], &[], |f| { + f.header("Authorization", header) + })? + .send()?; if response.status() != 200 { - let data = response.json::()?; - info!("Could not fetch user: {}", data.status_message); + let err: DropServerError = response.json()?; + warn!("{:?}", err); - if data.status_message == "Nonce expired" { + if err.status_message == "Nonce expired" { return Err(RemoteAccessError::OutOfSync); } - return Err(RemoteAccessError::InvalidCodeError(0)); + return Err(RemoteAccessError::InvalidResponse(err)); } - let user = response.json::()?; - - Ok(user) + response.json::().map_err(|e| e.into()) } fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> { @@ -99,7 +100,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc } let base_url = { - let handle = DB.borrow_data().unwrap(); + let handle = borrow_db_checked(); Url::parse(handle.base_url.as_str())? }; @@ -113,18 +114,18 @@ 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!("{}", response.status().as_u16()); - let response_struct = response.json::()?; + debug!("handshake responsded with {}", response.status().as_u16()); + let response_struct: HandshakeResponse = response.json()?; { - let mut handle = DB.borrow_data_mut().unwrap(); + let mut handle = borrow_db_mut_checked(); handle.auth = Some(DatabaseAuth { private: response_struct.private, cert: response_struct.certificate, client_id: response_struct.id, }); drop(handle); - DB.save().unwrap(); + save_db(); } { @@ -137,12 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc Ok(()) } -#[tauri::command] -pub fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), String> { - recieve_handshake(app, format!("handshake/{}", token)); - Ok(()) -} - pub fn recieve_handshake(app: AppHandle, path: String) { // Tell the app we're processing app.emit("auth/processing", ()).unwrap(); @@ -157,9 +152,9 @@ pub fn recieve_handshake(app: AppHandle, path: String) { app.emit("auth/finished", ()).unwrap(); } -fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> { +pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { let base_url = { - let db_lock = DB.borrow_data().unwrap(); + let db_lock = borrow_db_checked(); Url::parse(&db_lock.base_url.clone())? }; @@ -173,8 +168,8 @@ fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> { let response = client.post(endpoint.to_string()).json(&body).send()?; if response.status() != 200 { - let data = response.json::()?; - info!("Could not start handshake: {}", data.status_message); + let data: DropServerError = response.json()?; + error!("could not start handshake: {}", data.status_message); return Err(RemoteAccessError::HandshakeFailed(data.status_message)); } @@ -182,79 +177,25 @@ fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> { let redir_url = response.text()?; let complete_redir_url = base_url.join(&redir_url)?; - info!("opening web browser to continue authentication"); + debug!("opening web browser to continue authentication"); webbrowser::open(complete_redir_url.as_ref()).unwrap(); Ok(()) } -#[tauri::command] -pub fn auth_initiate<'a>() -> Result<(), String> { - let result = auth_initiate_wrapper(); - if result.is_err() { - return Err(result.err().unwrap().to_string()); - } - - Ok(()) -} - -#[tauri::command] -pub fn retry_connect(state: tauri::State<'_, Mutex>) -> Result<(), ()> { - let (app_status, user) = setup()?; - - let mut guard = state.lock().unwrap(); - guard.status = app_status; - guard.user = user; - drop(guard); - - Ok(()) -} - -pub fn setup() -> Result<(AppStatus, Option), ()> { - let data = DB.borrow_data().unwrap(); +pub fn setup() -> (AppStatus, Option) { + let data = borrow_db_checked(); let auth = data.auth.clone(); drop(data); if auth.is_some() { - let user_result = fetch_user(); - if user_result.is_err() { - let error = user_result.err().unwrap(); - warn!("auth setup failed with: {}", error); - match error { - RemoteAccessError::FetchError(_) => { - return Ok((AppStatus::ServerUnavailable, None)) - } - _ => return Ok((AppStatus::SignedInNeedsReauth, None)), - } - } - return Ok((AppStatus::SignedIn, Some(user_result.unwrap()))); + let user_result = match fetch_user() { + Ok(data) => data, + Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None), + Err(_) => return (AppStatus::SignedInNeedsReauth, None), + }; + return (AppStatus::SignedIn, Some(user_result)); } - Ok((AppStatus::SignedOut, None)) -} - -#[tauri::command] -pub fn sign_out(app: AppHandle) -> Result<(), String> { - info!("Signing out user"); - - // Clear auth from database - { - let mut handle = DB.borrow_data_mut().unwrap(); - handle.auth = None; - drop(handle); - DB.save().unwrap(); - } - - // Update app state - { - let app_state = app.state::>(); - let mut app_state_handle = app_state.lock().unwrap(); - app_state_handle.status = AppStatus::SignedOut; - app_state_handle.user = None; - } - - // Emit event for frontend - app.emit("auth/signedout", ()).unwrap(); - - Ok(()) + (AppStatus::SignedOut, None) } diff --git a/src-tauri/src/remote/commands.rs b/src-tauri/src/remote/commands.rs new file mode 100644 index 0000000..82781c8 --- /dev/null +++ b/src-tauri/src/remote/commands.rs @@ -0,0 +1,78 @@ +use std::sync::Mutex; + +use tauri::{AppHandle, Emitter, Manager}; +use url::Url; + +use crate::{ + database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, + error::remote_access_error::RemoteAccessError, + AppState, AppStatus, +}; + +use super::{ + auth::{auth_initiate_logic, recieve_handshake, setup}, + remote::use_remote_logic, +}; + +#[tauri::command] +pub fn use_remote( + url: String, + state: tauri::State<'_, Mutex>>, +) -> Result<(), RemoteAccessError> { + use_remote_logic(url, state) +} + +#[tauri::command] +pub fn gen_drop_url(path: String) -> Result { + let base_url = { + let handle = borrow_db_checked(); + + Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)? + }; + + let url = base_url.join(&path).unwrap(); + + Ok(url.to_string()) +} + +#[tauri::command] +pub fn sign_out(app: AppHandle) { + // Clear auth from database + { + let mut handle = borrow_db_mut_checked(); + handle.auth = None; + drop(handle); + save_db(); + } + + // Update app state + { + let app_state = app.state::>(); + let mut app_state_handle = app_state.lock().unwrap(); + app_state_handle.status = AppStatus::SignedOut; + app_state_handle.user = None; + } + + // Emit event for frontend + app.emit("auth/signedout", ()).unwrap(); +} + +#[tauri::command] +pub fn retry_connect(state: tauri::State<'_, Mutex>) { + let (app_status, user) = setup(); + + let mut guard = state.lock().unwrap(); + guard.status = app_status; + guard.user = user; + drop(guard); +} + +#[tauri::command] +pub fn auth_initiate() -> Result<(), RemoteAccessError> { + auth_initiate_logic() +} + +#[tauri::command] +pub fn manual_recieve_handshake(app: AppHandle, token: String) { + recieve_handshake(app, format!("handshake/{}", token)); +} diff --git a/src-tauri/src/remote/mod.rs b/src-tauri/src/remote/mod.rs new file mode 100644 index 0000000..a2eb666 --- /dev/null +++ b/src-tauri/src/remote/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod commands; +pub mod remote; +pub mod requests; diff --git a/src-tauri/src/remote/remote.rs b/src-tauri/src/remote/remote.rs new file mode 100644 index 0000000..01f2c2b --- /dev/null +++ b/src-tauri/src/remote/remote.rs @@ -0,0 +1,48 @@ +use std::sync::Mutex; + +use log::{debug, warn}; +use serde::Deserialize; +use url::Url; + +use crate::{ + database::db::{borrow_db_mut_checked, save_db}, + error::remote_access_error::RemoteAccessError, + AppState, AppStatus, +}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct DropHealthcheck { + app_name: String, +} + +pub fn use_remote_logic( + url: String, + state: tauri::State<'_, Mutex>>, +) -> Result<(), RemoteAccessError> { + debug!("connecting to url {}", url); + let base_url = Url::parse(&url)?; + + // Test Drop url + let test_endpoint = base_url.join("/api/v1")?; + let response = reqwest::blocking::get(test_endpoint.to_string())?; + + let result: DropHealthcheck = response.json()?; + + if result.app_name != "Drop" { + warn!("user entered drop endpoint that connected, but wasn't identified as Drop"); + return Err(RemoteAccessError::InvalidEndpoint); + } + + let mut app_state = state.lock().unwrap(); + app_state.status = AppStatus::SignedOut; + drop(app_state); + + let mut db_state = borrow_db_mut_checked(); + db_state.base_url = base_url.to_string(); + drop(db_state); + + save_db(); + + Ok(()) +} diff --git a/src-tauri/src/remote/requests.rs b/src-tauri/src/remote/requests.rs new file mode 100644 index 0000000..44cdc83 --- /dev/null +++ b/src-tauri/src/remote/requests.rs @@ -0,0 +1,23 @@ +use reqwest::blocking::{Client, RequestBuilder}; + +use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB}; + +pub fn make_request, F: FnOnce(RequestBuilder) -> RequestBuilder>( + client: &Client, + path_components: &[T], + query: &[(T, T)], + f: F, +) -> Result { + let mut base_url = DB.fetch_base_url(); + for endpoint in path_components { + base_url = base_url.join(endpoint.as_ref())?; + } + { + let mut queries = base_url.query_pairs_mut(); + for (param, val) in query { + queries.append_pair(param.as_ref(), val.as_ref()); + } + } + let response = client.get(base_url); + Ok(f(response)) +} diff --git a/src-tauri/src/tests/mod.rs b/src-tauri/src/tests/mod.rs deleted file mode 100644 index 401ca7e..0000000 --- a/src-tauri/src/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod progress_tests; diff --git a/src-tauri/src/tests/progress_tests.rs b/src-tauri/src/tests/progress_tests.rs deleted file mode 100644 index 228b22b..0000000 --- a/src-tauri/src/tests/progress_tests.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* -use atomic_counter::RelaxedCounter; - -use crate::downloads::progress::ProgressChecker; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - - -#[test] -fn test_progress_sequentially() { - let counter = Arc::new(RelaxedCounter::new(0)); - let callback = Arc::new(AtomicBool::new(false)); - let p = ProgressChecker::new(Box::new(test_fn), counter.clone(), callback, 100); - p.run_contexts_sequentially((1..100).collect()); - println!("Progress: {}", p.get_progress_percentage()); -} -#[test] -fn test_progress_parallel() { - let counter = Arc::new(RelaxedCounter::new(0)); - let callback = Arc::new(AtomicBool::new(false)); - let p = ProgressChecker::new(Box::new(test_fn), counter.clone(), callback, 100); - p.run_contexts_parallel_background((1..100).collect(), 10); -} - -fn test_fn(int: usize, _callback: Arc, _counter: Arc) { - println!("{}", int); -} - -*/ diff --git a/src-tauri/src/tools/compatibility_layer.rs b/src-tauri/src/tools/compatibility_layer.rs deleted file mode 100644 index 3644319..0000000 --- a/src-tauri/src/tools/compatibility_layer.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub struct CompatibilityLayer { - -} \ No newline at end of file diff --git a/src-tauri/src/tools/mod.rs b/src-tauri/src/tools/mod.rs deleted file mode 100644 index 0279e82..0000000 --- a/src-tauri/src/tools/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod prefix; -mod registry; -mod tool; -mod compatibility_layer; \ No newline at end of file diff --git a/src-tauri/src/tools/prefix.rs b/src-tauri/src/tools/prefix.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src-tauri/src/tools/registry.rs b/src-tauri/src/tools/registry.rs deleted file mode 100644 index fa50426..0000000 --- a/src-tauri/src/tools/registry.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::collections::HashMap; - -use crate::download_manager::downloadable::Downloadable; - -pub struct Registry { - tools: HashMap -} diff --git a/src-tauri/src/tools/tool.rs b/src-tauri/src/tools/tool.rs deleted file mode 100644 index 43fc9f0..0000000 --- a/src-tauri/src/tools/tool.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::sync::Arc; - -use tauri::AppHandle; - -use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject}; - -pub struct ToolDownloadAgent { - id: String, - version: String, - location: String, - control_flag: DownloadThreadControl, - progress: Arc, -} -impl Downloadable for ToolDownloadAgent { - fn download(&self, app_handle: &AppHandle) -> Result { - todo!() - } - - fn progress(&self) -> Arc { - todo!() - } - - fn control_flag(&self) -> DownloadThreadControl { - todo!() - } - - fn status(&self) -> crate::download_manager::download_manager::DownloadStatus { - todo!() - } - - fn metadata(&self) -> DownloadableMetadata { - todo!() - } - - fn on_initialised(&self, app_handle: &tauri::AppHandle) { - todo!() - } - - fn on_error(&self, app_handle: &tauri::AppHandle, error: crate::download_manager::application_download_error::ApplicationDownloadError) { - todo!() - } - - fn on_complete(&self, app_handle: &tauri::AppHandle) { - todo!() - } - - fn on_incomplete(&self, app_handle: &tauri::AppHandle) { - todo!() - } - - fn on_cancelled(&self, app_handle: &tauri::AppHandle) { - todo!() - } -} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0efb8e0..1dead92 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.1.0", + "version": "0.2.0-beta", "identifier": "dev.drop.app", "build": { "beforeDevCommand": "yarn dev --port 1432", @@ -23,7 +23,7 @@ }, "bundle": { "active": true, - "targets": ["nsis", "deb", "rpm", "dmg"], + "targets": ["nsis", "deb", "rpm", "dmg", "appimage"], "windows": { "nsis": { "installMode": "both" diff --git a/types.ts b/types.ts index 720ec28..60a5b23 100644 --- a/types.ts +++ b/types.ts @@ -71,4 +71,9 @@ export type DownloadableMetadata = { id: string, version: string, downloadType: DownloadableType +} + +export type Settings = { + autostart: boolean, + maxDownloadThreads: number, } \ No newline at end of file