diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 1735f73..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -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 deleted file mode 100644 index bbcbbe7..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -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 0fc3abe..7d0ff2c 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}/rustlang/rust:nightly + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm 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,10 +13,9 @@ 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,AppImage}" + - "*.{deb,rpm}" build-windows: stage: build diff --git a/app.vue b/app.vue index b9011c6..f81c432 100644 --- a/app.vue +++ b/app.vue @@ -1,8 +1,8 @@ diff --git a/pages/library.vue b/pages/library.vue index ab5079a..a1838d9 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -1,25 +1,21 @@ \ No newline at end of file diff --git a/pages/queue.vue b/pages/queue.vue index 8d46bea..d78967c 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -1,37 +1,23 @@ diff --git a/pages/settings/compatibility.vue b/pages/settings/compatibility.vue new file mode 100644 index 0000000..27e0f69 --- /dev/null +++ b/pages/settings/compatibility.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/pages/settings/debug.vue b/pages/settings/debug.vue index 7a549ca..3ef3e06 100644 --- a/pages/settings/debug.vue +++ b/pages/settings/debug.vue @@ -58,6 +58,26 @@

+
+
+ +

+ 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. -

-
- -
- -
-
@@ -220,7 +172,6 @@ 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); @@ -229,14 +180,6 @@ 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; @@ -270,7 +213,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 @@ -292,42 +235,4 @@ 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 b7bcd45..f9d7249 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.error(error, info); + console.log(error); const router = useRouter(); router.replace(`/error`); }); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 22b5365..069c108 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -255,12 +255,6 @@ 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" @@ -1006,9 +1000,8 @@ dependencies = [ [[package]] name = "drop-app" -version = "0.2.0-beta-prerelease-1" +version = "0.1.0" dependencies = [ - "atomic-instant-full", "boxcar", "chrono", "directories", @@ -1018,7 +1011,6 @@ dependencies = [ "log4rs", "md5", "openssl", - "parking_lot 0.12.3", "rayon", "reqwest", "rustbreak", @@ -1028,7 +1020,6 @@ dependencies = [ "serde_json", "serde_with", "shared_child", - "slice-deque", "tauri", "tauri-build", "tauri-plugin-autostart", @@ -1037,7 +1028,6 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-shell", "tauri-plugin-single-instance", - "throttle_my_fn", "tokio", "umu-wrapper-lib", "url", @@ -2351,7 +2341,7 @@ dependencies = [ "log", "log-mdc", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "rand 0.8.5", "serde", "serde-value", @@ -2369,15 +2359,6 @@ 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" @@ -2977,17 +2958,6 @@ 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" @@ -2995,21 +2965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "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", + "parking_lot_core", ] [[package]] @@ -3020,7 +2976,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -3455,15 +3411,6 @@ 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" @@ -4051,17 +3998,6 @@ 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" @@ -4094,7 +4030,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.8", + "redox_syscall", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -4152,7 +4088,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "phf_shared 0.10.0", "precomputed-hash", "serde", @@ -4305,7 +4241,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "raw-window-handle", "scopeguard", "tao-macros", @@ -4766,18 +4702,6 @@ 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 f9f24c3..73aa54e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drop-app" -version = "0.2.0-beta-prerelease-1" +version = "0.1.0" description = "The client application for the open-source, self-hosted game distribution platform Drop" authors = ["Drop OSS"] edition = "2021" @@ -46,10 +46,6 @@ 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 deleted file mode 100644 index 271800c..0000000 --- a/src-tauri/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "nightly" \ No newline at end of file diff --git a/src-tauri/src/remote/auth.rs b/src-tauri/src/auth.rs similarity index 58% rename from src-tauri/src/remote/auth.rs rename to src-tauri/src/auth.rs index d2594de..7044140 100644 --- a/src-tauri/src/remote/auth.rs +++ b/src-tauri/src/auth.rs @@ -1,22 +1,18 @@ use std::{env, sync::Mutex}; use chrono::Utc; -use log::{debug, error, warn}; +use log::{info, 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::{ - database::db::{ - borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls, - }, - error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, + db::{DatabaseAuth, DatabaseImpls}, + remote::{DropServerError, RemoteAccessError}, AppState, AppStatus, User, DB, }; -use super::requests::make_request; - #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct InitiateRequestBody { @@ -39,7 +35,6 @@ 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(); @@ -55,7 +50,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result { pub fn generate_authorization_header() -> String { let certs = { - let db = borrow_db_checked(); + let db = DB.borrow_data().unwrap(); db.auth.clone().unwrap() }; @@ -69,25 +64,29 @@ 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 = make_request(&client, &["/api/v1/client/user"], &[], |f| { - f.header("Authorization", header) - })? - .send()?; - if response.status() != 200 { - let err: DropServerError = response.json()?; - warn!("{:?}", err); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; - if err.status_message == "Nonce expired" { + if response.status() != 200 { + let data = response.json::()?; + info!("Could not fetch user: {}", data.status_message); + + if data.status_message == "Nonce expired" { return Err(RemoteAccessError::OutOfSync); } - return Err(RemoteAccessError::InvalidResponse(err)); + return Err(RemoteAccessError::InvalidCodeError(0)); } - response.json::().map_err(|e| e.into()) + let user = response.json::()?; + + Ok(user) } fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> { @@ -100,7 +99,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc } let base_url = { - let handle = borrow_db_checked(); + let handle = DB.borrow_data().unwrap(); Url::parse(handle.base_url.as_str())? }; @@ -114,18 +113,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()?; - debug!("handshake responsded with {}", response.status().as_u16()); - let response_struct: HandshakeResponse = response.json()?; + info!("{}", response.status().as_u16()); + let response_struct = response.json::()?; { - let mut handle = borrow_db_mut_checked(); + let mut handle = DB.borrow_data_mut().unwrap(); handle.auth = Some(DatabaseAuth { private: response_struct.private, cert: response_struct.certificate, client_id: response_struct.id, }); drop(handle); - save_db(); + DB.save().unwrap(); } { @@ -138,6 +137,12 @@ 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(); @@ -152,9 +157,9 @@ pub fn recieve_handshake(app: AppHandle, path: String) { app.emit("auth/finished", ()).unwrap(); } -pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { +fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> { let base_url = { - let db_lock = borrow_db_checked(); + let db_lock = DB.borrow_data().unwrap(); Url::parse(&db_lock.base_url.clone())? }; @@ -168,8 +173,8 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { let response = client.post(endpoint.to_string()).json(&body).send()?; if response.status() != 200 { - let data: DropServerError = response.json()?; - error!("could not start handshake: {}", data.status_message); + let data = response.json::()?; + info!("Could not start handshake: {}", data.status_message); return Err(RemoteAccessError::HandshakeFailed(data.status_message)); } @@ -177,25 +182,79 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { let redir_url = response.text()?; let complete_redir_url = base_url.join(&redir_url)?; - debug!("opening web browser to continue authentication"); + info!("opening web browser to continue authentication"); webbrowser::open(complete_redir_url.as_ref()).unwrap(); Ok(()) } -pub fn setup() -> (AppStatus, Option) { - let data = borrow_db_checked(); +#[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(); let auth = data.auth.clone(); drop(data); if auth.is_some() { - 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)); + 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()))); } - (AppStatus::SignedOut, None) + 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(()) } diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs index dec9148..bd526be 100644 --- a/src-tauri/src/autostart.rs +++ b/src-tauri/src/autostart.rs @@ -1,76 +1,69 @@ -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) -} +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(()) +} diff --git a/src-tauri/src/cleanup.rs b/src-tauri/src/cleanup.rs index de0e4e6..925ea61 100644 --- a/src-tauri/src/cleanup.rs +++ b/src-tauri/src/cleanup.rs @@ -1,23 +1,15 @@ -use log::{debug, error}; + +use log::info; use tauri::AppHandle; -use crate::AppState; #[tauri::command] -pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex>>) { - cleanup_and_exit(&app, &state); +pub fn quit(app: tauri::AppHandle) { + cleanup_and_exit(&app); } -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), - } +pub fn cleanup_and_exit(app: &AppHandle) { + info!("exiting drop application..."); app.exit(0); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs deleted file mode 100644 index b47a348..0000000 --- a/src-tauri/src/commands.rs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 18befb8..0000000 --- a/src-tauri/src/database/commands.rs +++ /dev/null @@ -1,92 +0,0 @@ -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/database/debug.rs b/src-tauri/src/database/debug.rs deleted file mode 100644 index 45d2034..0000000 --- a/src-tauri/src/database/debug.rs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 2a4255b..0000000 --- a/src-tauri/src/database/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index b8c1742..0000000 --- a/src-tauri/src/database/settings.rs +++ /dev/null @@ -1,24 +0,0 @@ -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/database/db.rs b/src-tauri/src/db.rs similarity index 50% rename from src-tauri/src/database/db.rs rename to src-tauri/src/db.rs index b92de3d..a9ad8da 100644 --- a/src-tauri/src/database/db.rs +++ b/src-tauri/src/db.rs @@ -1,27 +1,20 @@ use std::{ collections::HashMap, fs::{self, create_dir_all}, - hash::Hash, path::{Path, PathBuf}, - sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, + sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH}, }; use chrono::Utc; use directories::BaseDirs; -use log::{debug, error, info}; +use log::debug; 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::{ - database::settings::Settings, - download_manager::downloadable_metadata::DownloadableMetadata, - games::{library::push_game_update, state::GameStatusManager}, - process::process_manager::Platform, - DB, -}; +use crate::{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 { @@ -57,30 +50,18 @@ pub enum ApplicationTransientStatus { #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct GameVersion { - pub game_id: String, - pub version_name: 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, + pub version_name: String, + pub launch_command: String, + pub setup_command: String, + pub platform: Platform, } #[serde_as] -#[derive(Serialize, Clone, Deserialize, Default)] +#[derive(Serialize, Clone, Deserialize)] #[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>, @@ -90,34 +71,29 @@ pub struct DatabaseApplications { pub transient_statuses: HashMap, } -#[derive(Serialize, Deserialize, Clone, Default)] +#[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)] pub struct Database { #[serde(default)] pub settings: Settings, pub auth: Option, pub base_url: String, pub applications: DatabaseApplications, - 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 prev_database: Option } pub static DATA_ROOT_DIR: LazyLock> = LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); @@ -151,20 +127,35 @@ 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::new(games_base_dir, None); + 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, + }; debug!( "Creating database at path {}", db_path.as_os_str().to_str().unwrap() @@ -185,72 +176,103 @@ 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 = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); setter(&mut db_handle, &meta); drop(db_handle); - save_db(); + DB.save().unwrap(); let status = GameStatusManager::fetch_state(&meta.id); - push_game_update(app_handle, &meta.id, status); + push_game_update(app_handle, &meta, 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(); - base.set_file_name(format!("drop.db.backup-{}", time)); + let mut base = db_path.clone().into_os_string(); + base.push("."); + base.push(time.to_string()); base }; - info!( - "old database stored at: {}", - new_path.to_string_lossy().to_string() - ); - fs::rename(&db_path, &new_path).unwrap(); + fs::copy(&db_path, &new_path).unwrap(); - let db = Database::new( - games_base_dir.into_os_string().into_string().unwrap(), - Some(new_path), - ); + 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 }, + }; - 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); - } - } -} -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) - } - } -} +} \ No newline at end of file diff --git a/src-tauri/src/debug.rs b/src-tauri/src/debug.rs new file mode 100644 index 0000000..de42dd6 --- /dev/null +++ b/src-tauri/src/debug.rs @@ -0,0 +1,23 @@ +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/error/application_download_error.rs b/src-tauri/src/download_manager/application_download_error.rs similarity index 55% rename from src-tauri/src/error/application_download_error.rs rename to src-tauri/src/download_manager/application_download_error.rs index d68bd71..38c690d 100644 --- a/src-tauri/src/error/application_download_error.rs +++ b/src-tauri/src/download_manager/application_download_error.rs @@ -1,14 +1,9 @@ -use std::{ - fmt::{Display, Formatter}, - io, -}; +use std::{fmt::{Display, Formatter}, io}; -use serde_with::SerializeDisplay; - -use super::{remote_access_error::RemoteAccessError, setup_error::SetupError}; +use crate::remote::RemoteAccessError; // TODO: Rename / separate from downloads -#[derive(Debug, Clone, SerializeDisplay)] +#[derive(Debug, Clone)] pub enum ApplicationDownloadError { Communication(RemoteAccessError), Checksum, @@ -22,11 +17,25 @@ 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/download_manager/commands.rs b/src-tauri/src/download_manager/commands.rs deleted file mode 100644 index 0a65c0d..0000000 --- a/src-tauri/src/download_manager/commands.rs +++ /dev/null @@ -1,31 +0,0 @@ -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 b3dee68..ec950f8 100644 --- a/src-tauri/src/download_manager/download_manager.rs +++ b/src-tauri/src/download_manager/download_manager.rs @@ -4,21 +4,16 @@ use std::{ fmt::Debug, sync::{ mpsc::{SendError, Sender}, - Mutex, MutexGuard, + Arc, Mutex, MutexGuard, }, thread::JoinHandle, }; -use log::{debug, info}; +use log::info; use serde::Serialize; -use crate::error::application_download_error::ApplicationDownloadError; -use super::{ - download_manager_builder::{CurrentProgressObject, DownloadAgent}, - downloadable_metadata::DownloadableMetadata, - queue::Queue, -}; +use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent}, downloadable_metadata::DownloadableMetadata, queue::Queue}; pub enum DownloadManagerSignal { /// Resumes (or starts) the DownloadManager @@ -84,7 +79,7 @@ pub enum DownloadStatus { /// which provides raw access to the underlying queue. /// THIS EDITING IS BLOCKING!!! pub struct DownloadManager { - terminator: Mutex>>>, + terminator: JoinHandle>, download_queue: Queue, progress: CurrentProgressObject, command_sender: Sender, @@ -99,7 +94,7 @@ impl DownloadManager { command_sender: Sender, ) -> Self { Self { - terminator: Mutex::new(Some(terminator)), + terminator, download_queue, progress, command_sender, @@ -108,11 +103,10 @@ impl DownloadManager { pub fn queue_download( &self, - download: DownloadAgent, + download: DownloadAgent ) -> Result<(), SendError> { - info!("creating download with meta {:?}", download.metadata()); - self.command_sender - .send(DownloadManagerSignal::Queue(download))?; + info!("Adding download id {:?}", download.metadata()); + self.command_sender.send(DownloadManagerSignal::Queue(download))?; self.command_sender.send(DownloadManagerSignal::Go) } pub fn edit(&self) -> MutexGuard<'_, VecDeque> { @@ -151,14 +145,12 @@ impl DownloadManager { .unwrap(); } - debug!( - "moving download at index {} to index {}", - current_index, new_index - ); + info!("moving {} to {}", 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 { @@ -167,10 +159,6 @@ impl DownloadManager { self.command_sender .send(DownloadManagerSignal::UpdateUIQueue) .unwrap(); - self.command_sender - .send(DownloadManagerSignal::Go) - .unwrap(); - } pub fn pause_downloads(&self) { self.command_sender @@ -180,12 +168,11 @@ 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(); - let terminator = self.terminator.lock().unwrap().take(); - terminator.unwrap().join() + self.terminator.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 dd43dc2..406a500 100644 --- a/src-tauri/src/download_manager/download_manager_builder.rs +++ b/src-tauri/src/download_manager/download_manager_builder.rs @@ -1,28 +1,19 @@ use std::{ collections::HashMap, + fs::remove_dir_all, sync::{ mpsc::{channel, Receiver, Sender}, - Arc, Mutex, + Arc, Mutex, RwLockWriteGuard, }, thread::{spawn, JoinHandle}, }; -use log::{debug, error, info, warn}; +use log::{error, info}; use tauri::{AppHandle, Emitter}; -use crate::{ - error::application_download_error::ApplicationDownloadError, - games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}, -}; +use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}; -use super::{ - download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, - download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, - downloadable::Downloadable, - downloadable_metadata::DownloadableMetadata, - progress_object::ProgressObject, - queue::Queue, -}; +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}; pub type DownloadAgent = Arc>; pub type CurrentProgressObject = Arc>>>; @@ -56,7 +47,7 @@ whichever download queue order is required. +----------------------------------------------------------------------------+ This download queue does not actually own any of the DownloadAgents. It is -simply an id-based reference system. The actual Agents are stored in the +simply a 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. @@ -124,6 +115,7 @@ 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) { @@ -138,6 +130,7 @@ impl DownloadManagerBuilder { } } + fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.command_receiver.recv() { @@ -179,13 +172,13 @@ impl DownloadManagerBuilder { } } fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { - debug!("got signal Queue"); + info!("Got signal Queue"); let meta = download_agent.metadata(); - debug!("queue metadata: {:?}", meta); + info!("Meta: {:?}", meta); if self.download_queue.exists(meta.clone()) { - warn!("download with same ID already exists"); + info!("Download with same ID already exists"); return; } @@ -193,37 +186,27 @@ 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) { - debug!("got signal Go"); - if self.download_agent_registry.is_empty() { - debug!( - "Download agent registry: {:?}", - self.download_agent_registry.len() - ); - return; + 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() { - 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; - } + info!("Current download agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata()); + return; } - debug!("current download queue: {:?}", self.download_queue.read()); + info!("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 @@ -243,21 +226,17 @@ 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(); })); @@ -267,7 +246,7 @@ impl DownloadManagerBuilder { active_control_flag.set(DownloadThreadControlFlag::Go); } fn manage_stop_signal(&mut self) { - debug!("got signal Stop"); + info!("Got signal Stop"); if let Some(active_control_flag) = self.active_control_flag.clone() { self.set_status(DownloadManagerStatus::Paused); @@ -275,9 +254,10 @@ impl DownloadManagerBuilder { } } fn manage_completed_signal(&mut self, meta: DownloadableMetadata) { - debug!("got signal Completed"); + info!("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); } } @@ -285,17 +265,18 @@ impl DownloadManagerBuilder { self.sender.send(DownloadManagerSignal::Go).unwrap(); } fn manage_error_signal(&mut self, error: ApplicationDownloadError) { - debug!("got signal Error"); - if let Some(current_agent) = self.current_download_agent.clone() { - current_agent.on_error(&self.app_handle, error.clone()); + 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()); - 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) { - debug!("got signal Cancel"); + info!("Got signal Cancel"); if let Some(current_download) = &self.current_download_agent { if ¤t_download.metadata() == meta { @@ -306,34 +287,33 @@ impl DownloadManagerBuilder { self.download_queue.pop_front(); self.cleanup_current_download(); - debug!("current download queue: {:?}", self.download_queue.read()); + info!("Current donwload 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) { + 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"); 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() - ); + 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) { - 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(); } @@ -346,19 +326,18 @@ 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(), - current: val.progress().sum(), - max: val.progress().get_max(), - } - }) + progress: val.progress().get_progress() + }}) .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 181b329..e917882 100644 --- a/src-tauri/src/download_manager/downloadable.rs +++ b/src-tauri/src/download_manager/downloadable.rs @@ -1,12 +1,9 @@ -use std::sync::Arc; +use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}}; use tauri::AppHandle; -use crate::error::application_download_error::ApplicationDownloadError; - use super::{ - download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl, - downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, + application_download_error::ApplicationDownloadError, download_manager::{DownloadManagerSignal, DownloadStatus}, download_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject }; pub trait Downloadable: Send + Sync { @@ -20,4 +17,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 790b669..7512af4 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 new file mode 100644 index 0000000..8f53185 --- /dev/null +++ b/src-tauri/src/download_manager/generate_downloadable.rs @@ -0,0 +1,7 @@ +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 deleted file mode 100644 index 4864599..0000000 --- a/src-tauri/src/download_manager/internal_error.rs +++ /dev/null @@ -1,27 +0,0 @@ -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 0bac198..6299068 100644 --- a/src-tauri/src/download_manager/mod.rs +++ b/src-tauri/src/download_manager/mod.rs @@ -1,10 +1,9 @@ -pub mod commands; pub mod download_manager; pub mod download_manager_builder; -pub mod download_thread_control_flag; -pub mod downloadable; -pub mod downloadable_metadata; -pub mod internal_error; pub mod progress_object; pub mod queue; -pub mod rolling_progress_updates; +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 diff --git a/src-tauri/src/download_manager/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs index fc0907b..a3c3c24 100644 --- a/src-tauri/src/download_manager/progress_object.rs +++ b/src-tauri/src/download_manager/progress_object.rs @@ -2,17 +2,14 @@ use std::{ sync::{ atomic::{AtomicUsize, Ordering}, mpsc::Sender, - Arc, Mutex, + Arc, Mutex, RwLock, }, - time::{Duration, Instant}, + time::Instant, }; -use atomic_instant_full::AtomicInstant; -use throttle_my_fn::throttle; +use log::info; -use super::{ - download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow, -}; +use super::download_manager::DownloadManagerSignal; #[derive(Clone)] pub struct ProgressObject { @@ -20,10 +17,11 @@ pub struct ProgressObject { progress_instances: Arc>>>, start: Arc>, sender: Sender, - //last_update: Arc>, - last_update_time: Arc, - bytes_last_update: Arc, - rolling: RollingProgressWindow<250>, + + points_towards_update: Arc, + points_to_push_update: Arc, + last_update: Arc>, + amount_last_update: Arc, } pub struct ProgressHandle { @@ -44,32 +42,72 @@ impl ProgressHandle { pub fn add(&self, amount: usize) { self.progress .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); - 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 + self.progress_object.check_push_update(amount); } } +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, - last_update_time: Arc::new(AtomicInstant::now()), - bytes_last_update: Arc::new(AtomicUsize::new(0)), - rolling: RollingProgressWindow::new(), + 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(); } } @@ -89,6 +127,9 @@ 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() = @@ -100,56 +141,4 @@ 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 f3e9493..cda08df 100644 --- a/src-tauri/src/download_manager/queue.rs +++ b/src-tauri/src/download_manager/queue.rs @@ -11,12 +11,6 @@ pub struct Queue { } #[allow(dead_code)] -impl Default for Queue { - fn default() -> Self { - Self::new() - } -} - impl Queue { pub fn new() -> Self { Self { @@ -32,7 +26,7 @@ impl Queue { pub fn pop_front(&self) -> Option { self.edit().pop_front() } - pub fn is_empty(&self) -> bool { + pub fn empty(&self) -> bool { self.inner.lock().unwrap().len() == 0 } pub fn exists(&self, meta: DownloadableMetadata) -> bool { @@ -50,9 +44,15 @@ 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 = queue.front()?; + let front = match queue.front() { + Some(front) => front, + None => return None, + }; if front == meta { return queue.pop_front(); } @@ -61,11 +61,7 @@ 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 deleted file mode 100644 index 2239b9a..0000000 --- a/src-tauri/src/download_manager/rolling_progress_updates.rs +++ /dev/null @@ -1,33 +0,0 @@ -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/error/drop_server_error.rs b/src-tauri/src/error/drop_server_error.rs deleted file mode 100644 index ab42263..0000000 --- a/src-tauri/src/error/drop_server_error.rs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index c13dd23..0000000 --- a/src-tauri/src/error/library_error.rs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 89b74ae..0000000 --- a/src-tauri/src/error/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 8afc9dc..0000000 --- a/src-tauri/src/error/process_error.rs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 32572b2..0000000 --- a/src-tauri/src/error/remote_access_error.rs +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index bd76ce5..0000000 --- a/src-tauri/src/error/setup_error.rs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 8f1d652..0000000 --- a/src-tauri/src/games/commands.rs +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 67b1359..0000000 --- a/src-tauri/src/games/downloads/commands.rs +++ /dev/null @@ -1,32 +0,0 @@ -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 d4fb1cd..5a92380 100644 --- a/src-tauri/src/games/downloads/download_agent.rs +++ b/src-tauri/src/games/downloads/download_agent.rs @@ -1,30 +1,25 @@ use crate::auth::generate_authorization_header; -use crate::database::db::{ - borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls, - GameDownloadStatus, -}; +use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls}; +use crate::download_manager::application_download_error::ApplicationDownloadError; 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, GameUpdateEvent}; -use crate::remote::requests::make_request; +use crate::games::library::{on_game_complete, push_game_update}; +use crate::remote::RemoteAccessError; use crate::DB; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; -use slice_deque::SliceDeque; -use std::fs::{create_dir_all, File}; +use tauri::{AppHandle, Emitter}; +use std::collections::VecDeque; +use std::fs::{create_dir_all, remove_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")] @@ -38,12 +33,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 { @@ -56,7 +51,7 @@ impl GameDownloadAgent { // Don't run by default let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); - let db_lock = borrow_db_checked(); + let db_lock = DB.borrow_data().unwrap(); let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); drop(db_lock); @@ -72,7 +67,7 @@ impl GameDownloadAgent { control_flag, manifest: Mutex::new(None), contexts: Mutex::new(Vec::new()), - completed_contexts: Mutex::new(SliceDeque::new()), + completed_contexts: Mutex::new(VecDeque::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, stored_manifest, @@ -83,8 +78,10 @@ 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); @@ -93,24 +90,16 @@ 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().id, - ( - None, - Some(ApplicationTransientStatus::Downloading { - version_name: self.version.clone(), - }), - ), - ); - let res = self - .run() - .map_err(|_| ApplicationDownloadError::DownloadError); + push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() }))); + let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError); - debug!( + info!( "{} took {}ms to download", self.id, timer.elapsed().as_millis() @@ -127,17 +116,25 @@ 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 = 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()))?; + let response = client + .get(manifest_url.to_string()) + .header("Authorization", header) + .send() + .unwrap(); if response.status() != 200 { return Err(ApplicationDownloadError::Communication( @@ -148,7 +145,7 @@ impl GameDownloadAgent { )); } - let manifest_download: DropManifest = response.json().unwrap(); + let manifest_download = response.json::().unwrap(); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); @@ -170,8 +167,11 @@ 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,11 +192,12 @@ 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_from_slice(&self.stored_manifest.get_completed_contexts()); + .extend(self.stored_manifest.get_completed_contexts()); } for (raw_path, chunk) in manifest { @@ -233,22 +234,22 @@ impl GameDownloadAgent { Ok(()) } - // TODO: Change return value on Err pub fn run(&self) -> Result { - let max_download_threads = borrow_db_checked().settings.max_download_threads; + info!("downloading game: {}", self.id); + const DOWNLOAD_MAX_THREADS: usize = 1; - debug!( - "downloading game: {} with {} threads", - self.id, max_download_threads - ); let pool = ThreadPoolBuilder::new() - .num_threads(max_download_threads) + .num_threads(DOWNLOAD_MAX_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(); @@ -258,36 +259,19 @@ 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.skip(context.length); + progress_handle.add(context.length); continue; } let sender = self.sender.clone(); - let request = match make_request( - &client, - &["/api/v1/client/chunk"], - &[ - ("id", &context.game_id), - ("version", &context.version), - ("name", &context.file_name), - ("chunk", &context.index.to_string()), - ], - |r| r.header("Authorization", generate_authorization_header()), - ) { - Ok(request) => request, - Err(e) => { - sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap(); - continue; - }, - }; + let request = generate_request(&base_url, client, &context); + 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); @@ -298,35 +282,39 @@ 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!( - "download agent for {} exited without completing ({}/{})", - self.id.clone(), - completed_lock_len, - contexts.len(), - ); + info!("da for {} exited without completing", self.id.clone()); self.stored_manifest - .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice()); + .set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into()); + info!("Setting completed contexts"); 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())) @@ -336,6 +324,26 @@ 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; @@ -360,6 +368,7 @@ 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) { @@ -373,35 +382,23 @@ 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(); } - // 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(); + fn on_incomplete(&self, _app_handle: &tauri::AppHandle) { *self.status.lock().unwrap() = DownloadStatus::Queued; - app_handle - .emit( - &format!("update_game/{}", meta.id), - GameUpdateEvent { - game_id: meta.id.clone(), - status: (Some(GameDownloadStatus::Remote {}), None), - }, - ) - .unwrap(); + return; } - fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {} - + fn on_cancelled(&self, _app_handle: &tauri::AppHandle) { + return; + } + 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 new file mode 100644 index 0000000..2bbd5e8 --- /dev/null +++ b/src-tauri/src/games/downloads/download_commands.rs @@ -0,0 +1,68 @@ +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 70e5b7e..ec4e87f 100644 --- a/src-tauri/src/games/downloads/download_logic.rs +++ b/src-tauri/src/games/downloads/download_logic.rs @@ -1,23 +1,26 @@ -use crate::download_manager::download_thread_control_flag::{ - DownloadThreadControl, DownloadThreadControlFlag, -}; +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::progress_object::ProgressHandle; -use crate::error::application_download_error::ApplicationDownloadError; -use crate::error::remote_access_error::RemoteAccessError; use crate::games::downloads::manifest::DropDownloadContext; -use log::warn; +use crate::remote::{DropServerError, RemoteAccessError}; +use crate::DB; +use log::{error, info, warn}; use md5::{Context, Digest}; -use reqwest::blocking::{RequestBuilder, Response}; +use reqwest::blocking::{Client, Request, RequestBuilder, Response}; use std::fs::{set_permissions, Permissions}; -use std::io::{ErrorKind, Read}; +use std::io::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, @@ -39,17 +42,19 @@ 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() } } @@ -119,7 +124,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 { @@ -132,9 +137,9 @@ pub fn download_game_chunk( .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; if response.status() != 200 { - let err = response.json().unwrap(); + warn!("{}", response.text().unwrap()); return Err(ApplicationDownloadError::Communication( - RemoteAccessError::InvalidResponse(err), + RemoteAccessError::InvalidCodeError(400), )); } @@ -148,9 +153,9 @@ pub fn download_game_chunk( let content_length = response.content_length(); if content_length.is_none() { - warn!("recieved 0 length content from server"); + error!("Recieved 0 length content from server"); return Err(ApplicationDownloadError::Communication( - RemoteAccessError::InvalidResponse(response.json().unwrap()), + RemoteAccessError::InvalidResponse(response.json::().unwrap()), )); } @@ -176,14 +181,16 @@ pub fn download_game_chunk( set_permissions(ctx.path.clone(), permissions).unwrap(); } + /* let checksum = pipeline .finish() - .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; + .map_err(|e| GameDownloadError::IoError(e))?; let res = hex::encode(checksum.0); if res != ctx.checksum { - return Err(ApplicationDownloadError::Checksum); + return Err(GameDownloadError::Checksum); } + */ Ok(true) } diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs index c9b3cd4..ba0fac2 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; +mod stored_manifest; \ No newline at end of file diff --git a/src-tauri/src/games/downloads/stored_manifest.rs b/src-tauri/src/games/downloads/stored_manifest.rs index fdc232b..bb5e6b9 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, warn}; +use log::error; use serde::{Deserialize, Serialize}; use serde_binary::binary_stream::Endian; @@ -43,10 +43,12 @@ impl StoredManifest { } }; + + match serde_binary::from_vec::(s, Endian::Little) { Ok(manifest) => manifest, Err(e) => { - warn!("{}", e); + error!("{}", e); StoredManifest::new(game_id, game_version, base_path) } } @@ -70,8 +72,8 @@ impl StoredManifest { Err(e) => error!("{}", e), }; } - pub fn set_completed_contexts(&self, completed_contexts: &[usize]) { - *self.completed_contexts.lock().unwrap() = completed_contexts.to_owned(); + pub fn set_completed_contexts(&self, completed_contexts: &Vec) { + *self.completed_contexts.lock().unwrap() = completed_contexts.clone(); } 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 b2f2fe2..8d40274 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::{debug, error, warn}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use tauri::Emitter; use tauri::{AppHandle, Manager}; +use urlencoding::encode; -use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion}; -use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus}; +use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus}; +use crate::db::GameVersion; use crate::download_manager::download_manager::DownloadStatus; use crate::download_manager::downloadable_metadata::DownloadableMetadata; -use crate::error::remote_access_error::RemoteAccessError; +use crate::process::process_manager::Platform; +use crate::remote::RemoteAccessError; use crate::games::state::{GameStatusManager, GameStatusWithTransient}; -use crate::remote::auth::generate_authorization_header; -use crate::remote::requests::make_request; -use crate::AppState; +use crate::{auth::generate_authorization_header, AppState, DB}; #[derive(serde::Serialize)] pub struct FetchGameStruct { @@ -40,10 +40,7 @@ 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)] @@ -51,8 +48,6 @@ pub struct QueueUpdateEventQueueData { pub meta: DownloadableMetadata, pub status: DownloadStatus, pub progress: f64, - pub current: usize, - pub max: usize, } #[derive(serde::Serialize, Clone)] @@ -66,27 +61,42 @@ pub struct StatsUpdateEvent { pub time: usize, } -pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { +// 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")?; + let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| { - f.header("Authorization", header) - })? - .send()?; + let response = client + .get(library_url.to_string()) + .header("Authorization", header) + .send()?; if response.status() != 200 { - let err = response.json().unwrap(); - warn!("{:?}", err); - return Err(RemoteAccessError::InvalidResponse(err)); + return Err(response.status().as_u16().into()); } - let games: Vec = response.json()?; + let games: Vec = response.json::>()?; let state = app.state::>(); let mut handle = state.lock().unwrap(); - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); for game in games.iter() { handle.games.insert(game.id.clone(), game.clone()); @@ -103,7 +113,12 @@ pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessErro Ok(games) } -pub fn fetch_game_logic( +#[tauri::command] +pub fn fetch_library(app: AppHandle) -> Result, String> { + fetch_library_logic(app).map_err(|e| e.to_string()) +} + +fn fetch_game_logic( id: String, app: tauri::AppHandle, ) -> Result { @@ -121,25 +136,31 @@ pub 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 = make_request(&client, &["/api/v1/game/", &id], &[], |r| { - r.header("Authorization", generate_authorization_header()) - })? - .send()?; + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; if response.status() == 404 { return Err(RemoteAccessError::GameNotFound); } if response.status() != 200 { - let err = response.json().unwrap(); - warn!("{:?}", err); - return Err(RemoteAccessError::InvalidResponse(err)); + return Err(RemoteAccessError::InvalidCodeError( + response.status().into(), + )); } - let game: Game = response.json()?; + let game = response.json::()?; state_handle.games.insert(id.clone(), game.clone()); - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .applications @@ -158,62 +179,95 @@ pub fn fetch_game_logic( Ok(data) } -pub fn fetch_game_verion_options_logic( - game_id: String, - state: tauri::State<'_, Mutex>, -) -> Result, RemoteAccessError> { - let client = reqwest::blocking::Client::new(); +#[tauri::command] +pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result { + let result = fetch_game_logic(game_id, app); - 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 { - let err = response.json().unwrap(); - warn!("{:?}", err); - return Err(RemoteAccessError::InvalidResponse(err)); + if result.is_err() { + return Err(result.err().unwrap().to_string()); } - let data: Vec = response.json()?; + 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>( + 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(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; + + if response.status() != 200 { + return Err(RemoteAccessError::InvalidCodeError( + response.status().into(), + )); + } + + let data = response.json::>()?; let state_lock = state.lock().unwrap(); let process_manager_lock = state_lock.process_manager.lock().unwrap(); - let data: Vec = data + let data = data .into_iter() .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) - .collect(); + .collect::>(); drop(process_manager_lock); drop(state_lock); Ok(data) } -pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { - println!("triggered uninstall for agent"); - let mut db_handle = borrow_db_mut_checked(); +#[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(); db_handle .applications .transient_statuses .entry(meta.clone()) .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {}); - + push_game_update( app_handle, - &meta.id, + &meta, (None, Some(ApplicationTransientStatus::Uninstalling {})), ); let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); if previous_state.is_none() { - warn!("uninstall job doesn't have previous state, failing silently"); + info!("uninstall job doesn't have previous state, failing silently"); return; } let previous_state = previous_state.unwrap(); - if let Some((_, install_dir)) = match previous_state { + if let Some((version_name, install_dir)) = match previous_state { GameDownloadStatus::Installed { version_name, install_dir, @@ -237,7 +291,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) error!("{}", e); } Ok(_) => { - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle.applications.transient_statuses.remove(&meta); db_handle .applications @@ -245,26 +299,29 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) .entry(meta.id.clone()) .and_modify(|e| *e = GameDownloadStatus::Remote {}); drop(db_handle); - save_db(); + DB.save().unwrap(); - debug!("uninstalled game id {}", &meta.id); + info!("uninstalled game id {}", &meta.id); - push_game_update( - &app_handle, - &meta.id, - (Some(GameDownloadStatus::Remote {}), None), - ); + push_game_update(&app_handle, &meta, (Some(GameDownloadStatus::Remote {}), None)); } }); } } -pub fn get_current_meta(game_id: &String) -> Option { - borrow_db_checked() - .applications - .installed_game_version - .get(game_id) - .cloned() +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 on_game_complete( @@ -273,27 +330,28 @@ pub fn on_game_complete( app_handle: &AppHandle, ) -> Result<(), RemoteAccessError> { // Fetch game version information from remote - if meta.version.is_none() { - return Err(RemoteAccessError::GameNotFound); - } + let base_url = DB.fetch_base_url(); + 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 = make_request( - &client, - &["/api/v1/client/metadata/version"], - &[ - ("id", &meta.id), - ("version", meta.version.as_ref().unwrap()), - ], - |f| f.header("Authorization", header), - )? - .send()?; + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; - let data: GameVersion = response.json()?; + let data = response.json::()?; - let mut handle = borrow_db_mut_checked(); + let mut handle = DB.borrow_data_mut().unwrap(); handle .applications .game_versions @@ -306,7 +364,7 @@ pub fn on_game_complete( .insert(meta.id.clone(), meta.clone()); drop(handle); - save_db(); + DB.save().unwrap(); let status = if data.setup_command.is_empty() { GameDownloadStatus::Installed { @@ -320,13 +378,13 @@ pub fn on_game_complete( } }; - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .applications .game_statuses .insert(meta.id.clone(), status.clone()); drop(db_handle); - save_db(); + DB.save().unwrap(); app_handle .emit( &format!("update_game/{}", meta.id), @@ -340,14 +398,14 @@ pub fn on_game_complete( Ok(()) } -pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) { +pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) { app_handle .emit( - &format!("update_game/{}", game_id), + &format!("update_game/{}", meta.id), GameUpdateEvent { - game_id: game_id.clone(), + game_id: meta.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 65c5c6b..e49a4ef 100644 --- a/src-tauri/src/games/mod.rs +++ b/src-tauri/src/games/mod.rs @@ -1,4 +1,3 @@ -pub mod commands; pub mod downloads; pub mod library; -pub mod state; +pub mod state; \ No newline at end of file diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs index 19b1769..778da84 100644 --- a/src-tauri/src/games/state.rs +++ b/src-tauri/src/games/state.rs @@ -1,16 +1,19 @@ -use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus}; +use crate::{ + db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, DB +}; -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 = borrow_db_checked(); + let db_lock = DB.borrow_data().unwrap(); 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(); @@ -26,4 +29,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 1e374eb..5bc1538 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,54 +1,50 @@ -mod database; +mod auth; +mod db; mod games; mod autostart; mod cleanup; -mod commands; -mod download_manager; -mod error; +mod debug; mod process; mod remote; +mod tools; +pub mod download_manager; +#[cfg(test)] +mod tests; -use crate::database::db::DatabaseImpls; -use autostart::{get_autostart_enabled, toggle_autostart}; +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 cleanup::{cleanup_and_exit, quit}; -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 db::{ + add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus, + DATA_ROOT_DIR, }; +use debug::fetch_system_data; use download_manager::download_manager::DownloadManager; use download_manager::download_manager_builder::DownloadManagerBuilder; -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 games::downloads::download_commands::{cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads}; 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::commands::{kill_game, launch_game}; +use process::compat::CompatibilityManager; +use process::process_commands::{kill_game, launch_game}; use process::process_manager::ProcessManager; -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 remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; -use std::env; +use tauri_plugin_dialog::DialogExt; use std::path::Path; -use std::str::FromStr; use std::sync::Arc; use std::{ collections::HashMap, @@ -56,9 +52,8 @@ use std::{ }; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIconBuilder; -use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; +use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; -use tauri_plugin_dialog::DialogExt; #[derive(Clone, Copy, Serialize)] pub enum AppStatus { @@ -91,25 +86,29 @@ 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}:{L} - {m}{n}", - ))) + .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {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}:{L} - {m}{n}", - ))) + .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {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)), @@ -118,7 +117,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { .build( Root::builder() .appenders(vec!["logfile", "console"]) - .build(LevelFilter::from_str(&log_level).expect("Invalid log level")), + .build(LevelFilter::Info), ) .unwrap(); @@ -127,8 +126,9 @@ 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"); - // TODO: Account for possible failure - let (app_status, user) = auth::setup(); + let (app_status, user) = auth::setup().unwrap(); - let db_handle = borrow_db_checked(); + let db_handle = DB.borrow_data().unwrap(); 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 { - database::db::GameDownloadStatus::Remote {} => {} - database::db::GameDownloadStatus::SetupRequired { + db::GameDownloadStatus::Remote {} => {} + db::GameDownloadStatus::SetupRequired { version_name: _, install_dir, } => { @@ -161,7 +161,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { missing_games.push(game_id); } } - database::db::GameDownloadStatus::Installed { + 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 = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); for game_id in missing_games { db_handle .applications @@ -186,11 +186,12 @@ fn setup(handle: AppHandle) -> AppState<'static> { drop(db_handle); - debug!("finished setup!"); + + info!("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 { @@ -199,6 +200,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { games, download_manager, process_manager, + compat_manager, } } @@ -225,9 +227,6 @@ pub fn run() { fetch_state, quit, fetch_system_data, - // User utils - update_settings, - fetch_settings, // Auth auth_initiate, retry_connect, @@ -246,9 +245,9 @@ pub fn run() { fetch_game_verion_options, // Downloads download_game, - move_download_in_queue, - pause_downloads, - resume_downloads, + move_game_in_queue, + pause_game_downloads, + resume_game_downloads, cancel_game, uninstall_game, // Processes @@ -266,14 +265,14 @@ pub fn run() { .setup(|app| { let handle = app.handle().clone(); let state = setup(handle); - debug!("initialized drop client"); + info!("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()?; - debug!("registered all pre-defined deep links"); + info!("registered all pre-defined deep links"); } let handle = app.handle().clone(); @@ -293,7 +292,7 @@ pub fn run() { .unwrap(); app.deep_link().on_open_url(move |event| { - debug!("handling drop:// url"); + info!("handling drop:// url"); let binding = event.urls(); let url = binding.first().unwrap(); if url.host_str().unwrap() == "handshake" { @@ -323,50 +322,48 @@ pub fn run() { app.webview_windows().get("main").unwrap().show().unwrap(); } "quit" => { - cleanup_and_exit(app, &app.state()); + cleanup_and_exit(app); } _ => { - 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 = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); 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 = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| { - f.header("Authorization", header) - }) - .unwrap() - .send(); + let response = client + .get(object_url.to_string()) + .header("Authorization", header) + .send(); if response.is_err() { warn!( "failed to fetch object with error: {}", @@ -395,7 +392,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 deleted file mode 100644 index 8a213a4..0000000 --- a/src-tauri/src/process/commands.rs +++ /dev/null @@ -1,40 +0,0 @@ -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 2b4f307..65e8ece 100644 --- a/src-tauri/src/process/compat.rs +++ b/src-tauri/src/process/compat.rs @@ -1,13 +1,51 @@ -// 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 std::{ + fs::create_dir_all, + path::PathBuf, + sync::atomic::{AtomicBool, Ordering}, +}; -// Option 1: Delete the file -// Delete src-tauri/src/process/compat.rs +use crate::db::DATA_ROOT_DIR; + +pub struct CompatibilityManager { + compat_tools_path: PathBuf, + prefixes_path: PathBuf, + created_paths: AtomicBool, +} -// Option 2: Add TODO comment /* -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. +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. */ +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 6a1aed5..85692c9 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 new file mode 100644 index 0000000..c74ff8b --- /dev/null +++ b/src-tauri/src/process/process_commands.rs @@ -0,0 +1,40 @@ +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 4d8ae12..c376037 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -1,33 +1,27 @@ use std::{ collections::HashMap, fs::{File, OpenOptions}, - io::{self, Error}, + io, path::{Path, PathBuf}, process::{Child, Command, ExitStatus}, sync::{Arc, Mutex}, thread::spawn, }; -use log::{debug, info, warn}; +use log::{info, warn}; use serde::{Deserialize, Serialize}; use shared_child::SharedChild; use tauri::{AppHandle, Manager}; use umu_wrapper_lib::command_builder::UmuCommandBuilder; use crate::{ - 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, + db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, games::library::push_game_update, games::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)>, } @@ -66,8 +60,13 @@ impl ProcessManager<'_> { } } - fn process_command(&self, install_dir: &String, command: Vec) -> (PathBuf, Vec) { - let root = &command[0]; + // 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; let install_dir = Path::new(install_dir); let absolute_exe = install_dir.join(root); @@ -80,40 +79,34 @@ impl ProcessManager<'_> { */ (absolute_exe, Vec::new()) } - pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> { - match self.processes.get(&game_id) { + pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> { + return match self.processes.get(&meta) { 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, game_id: String, result: Result) { - if !self.processes.contains_key(&game_id) { + fn on_process_finish(&mut self, meta: DownloadableMetadata, result: Result) { + if !self.processes.contains_key(&meta) { warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result); return; } - debug!("process for {:?} exited with {:?}", &game_id, result); + info!("process for {:?} exited with {:?}", meta, result); - self.processes.remove(&game_id); + self.processes.remove(&meta); - let mut db_handle = borrow_db_mut_checked(); - let meta = db_handle - .applications - .installed_game_version - .get(&game_id) - .cloned() - .unwrap(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle.applications.transient_statuses.remove(&meta); - let current_state = db_handle.applications.game_statuses.get(&game_id).cloned(); + let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); if let Some(saved_state) = current_state { if let GameDownloadStatus::SetupRequired { version_name, @@ -123,7 +116,7 @@ impl ProcessManager<'_> { if let Ok(exit_code) = result { if exit_code.success() { db_handle.applications.game_statuses.insert( - game_id.clone(), + meta.id.clone(), GameDownloadStatus::Installed { version_name: version_name.to_string(), install_dir: install_dir.to_string(), @@ -135,9 +128,9 @@ impl ProcessManager<'_> { } drop(db_handle); - let status = GameStatusManager::fetch_state(&game_id); + let status = GameStatusManager::fetch_state(&meta.id); - push_game_update(&self.app_handle, &game_id, status); + push_game_update(&self.app_handle, &meta, status); // TODO better management } @@ -149,86 +142,59 @@ impl ProcessManager<'_> { .contains_key(&(current.clone(), platform.clone()))) } - pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> { - if self.processes.contains_key(&game_id) { - return Err(ProcessError::AlreadyRunning); + 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()); } - 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 mut db_lock = DB.borrow_data_mut().unwrap(); + info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions); let game_status = db_lock .applications .game_statuses - .get(&game_id) - .ok_or(ProcessError::NotInstalled)?; + .get(&meta.id) + .ok_or("Game not installed")?; - let (version_name, install_dir) = match game_status { + let status_metadata: Option<(&String, &String)> = match game_status { GameDownloadStatus::Installed { version_name, install_dir, - } => (version_name, install_dir), + } => Some((version_name, install_dir)), GameDownloadStatus::SetupRequired { version_name, install_dir, - } => (version_name, install_dir), - _ => return Err(ProcessError::NotDownloaded), + } => Some((version_name, install_dir)), + _ => None, }; + 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(&game_id) - .ok_or(ProcessError::InvalidID)? + .get(&meta.id) + .ok_or("Invalid game ID".to_owned())? .get(version_name) - .ok_or(ProcessError::InvalidVersion)?; + .ok_or("Invalid version name".to_owned())?; - let mut command: Vec = Vec::new(); - - match game_status { + let raw_command: String = match game_status { GameDownloadStatus::Installed { version_name: _, install_dir: _, - } => { - command.extend([game_version.launch_command.clone()]); - command.extend(game_version.launch_args.clone()); - }, + } => game_version.launch_command.clone(), GameDownloadStatus::SetupRequired { version_name: _, install_dir: _, - } => { - command.extend([game_version.setup_command.clone()]); - command.extend(game_version.setup_args.clone()); - }, + } => game_version.setup_command.clone(), _ => panic!("unreachable code"), }; - info!("Command: {:?}", &command); - let (command, args) = self.process_command(install_dir, command); + let (command, args) = self.process_command(install_dir, raw_command); let target_current_dir = command.parent().unwrap().to_str().unwrap(); @@ -244,13 +210,11 @@ impl ProcessManager<'_> { .truncate(true) .read(true) .create(true) - .open(self.log_output_dir.join(format!( - "{}-{}-{}.log", - &game_id, - &version, - current_time.timestamp() - ))) - .map_err(ProcessError::IOError)?; + .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())?; let error_file = OpenOptions::new() .write(true) @@ -259,11 +223,11 @@ impl ProcessManager<'_> { .create(true) .open(self.log_output_dir.join(format!( "{}-{}-{}-error.log", - &game_id, - &version, + meta.id.clone(), + meta.version.clone().unwrap_or_default(), current_time.timestamp() ))) - .map_err(ProcessError::IOError)?; + .map_err(|v| v.to_string())?; let current_platform = self.current_platform.clone(); let target_platform = game_version.platform.clone(); @@ -271,21 +235,20 @@ impl ProcessManager<'_> { let game_launcher = self .game_launchers .get(&(current_platform, target_platform)) - .ok_or(ProcessError::InvalidPlatform)?; + .ok_or("Invalid version for this platform.") + .map_err(|e| e.to_string())?; - 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 = game_launcher.launch_process( + &meta, + command.to_str().unwrap().to_owned(), + args, + &target_current_dir.to_string(), + log_file, + error_file, + )?; let launch_process_handle = - Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?); + Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?); db_lock .applications @@ -294,7 +257,7 @@ impl ProcessManager<'_> { push_game_update( &self.app_handle, - &meta.id, + &meta, (None, Some(ApplicationTransientStatus::Running {})), ); @@ -309,7 +272,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.id, result); + process_manager_handle.on_process_finish(wait_thread_game_id, result); // As everything goes out of scope, they should get dropped // But just to explicit about it @@ -317,7 +280,10 @@ impl ProcessManager<'_> { drop(app_state_handle); }); - self.processes.insert(meta.id, wait_thread_handle); + self.processes.insert(meta, wait_thread_handle); + + info!("finished spawning process"); + Ok(()) } } @@ -332,31 +298,32 @@ pub trait ProcessHandler: Send + 'static { fn launch_process( &self, meta: &DownloadableMetadata, - launch_command: String, - game_version: &GameVersion, - current_dir: &str, + command: String, + args: Vec, + current_dir: &String, log_file: File, error_file: File, - ) -> Result; + ) -> Result; } struct NativeGameLauncher; impl ProcessHandler for NativeGameLauncher { fn launch_process( &self, - _meta: &DownloadableMetadata, - launch_command: String, - game_version: &GameVersion, - current_dir: &str, + meta: &DownloadableMetadata, + command: String, + args: Vec, + current_dir: &String, log_file: File, error_file: File, - ) -> Result { - Command::new(PathBuf::from(launch_command)) + ) -> Result { + Command::new(command) .current_dir(current_dir) .stdout(log_file) .stderr(error_file) - .args(game_version.launch_args.clone()) + .args(args) .spawn() + .map_err(|v| v.to_string()) } } @@ -365,23 +332,18 @@ struct UMULauncher; impl ProcessHandler for UMULauncher { fn launch_process( &self, - _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()) + 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) .build() .spawn() + .map_err(|x| x.to_string()) } } diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs new file mode 100644 index 0000000..04c56db --- /dev/null +++ b/src-tauri/src/remote.rs @@ -0,0 +1,156 @@ +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/remote/commands.rs b/src-tauri/src/remote/commands.rs deleted file mode 100644 index 82781c8..0000000 --- a/src-tauri/src/remote/commands.rs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index a2eb666..0000000 --- a/src-tauri/src/remote/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 01f2c2b..0000000 --- a/src-tauri/src/remote/remote.rs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 44cdc83..0000000 --- a/src-tauri/src/remote/requests.rs +++ /dev/null @@ -1,23 +0,0 @@ -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 new file mode 100644 index 0000000..401ca7e --- /dev/null +++ b/src-tauri/src/tests/mod.rs @@ -0,0 +1 @@ +mod progress_tests; diff --git a/src-tauri/src/tests/progress_tests.rs b/src-tauri/src/tests/progress_tests.rs new file mode 100644 index 0000000..228b22b --- /dev/null +++ b/src-tauri/src/tests/progress_tests.rs @@ -0,0 +1,29 @@ +/* +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 new file mode 100644 index 0000000..3644319 --- /dev/null +++ b/src-tauri/src/tools/compatibility_layer.rs @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..0279e82 --- /dev/null +++ b/src-tauri/src/tools/mod.rs @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..e69de29 diff --git a/src-tauri/src/tools/registry.rs b/src-tauri/src/tools/registry.rs new file mode 100644 index 0000000..fa50426 --- /dev/null +++ b/src-tauri/src/tools/registry.rs @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..43fc9f0 --- /dev/null +++ b/src-tauri/src/tools/tool.rs @@ -0,0 +1,54 @@ +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 1dead92..0efb8e0 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.2.0-beta", + "version": "0.1.0", "identifier": "dev.drop.app", "build": { "beforeDevCommand": "yarn dev --port 1432", @@ -23,7 +23,7 @@ }, "bundle": { "active": true, - "targets": ["nsis", "deb", "rpm", "dmg", "appimage"], + "targets": ["nsis", "deb", "rpm", "dmg"], "windows": { "nsis": { "installMode": "both" diff --git a/types.ts b/types.ts index 60a5b23..720ec28 100644 --- a/types.ts +++ b/types.ts @@ -71,9 +71,4 @@ export type DownloadableMetadata = { id: string, version: string, downloadType: DownloadableType -} - -export type Settings = { - autostart: boolean, - maxDownloadThreads: number, } \ No newline at end of file diff --git a/composables/generateGameMeta.ts b/utils/generateGameMeta.ts similarity index 100% rename from composables/generateGameMeta.ts rename to utils/generateGameMeta.ts