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/README.md b/README.md index 903f4ca..b0df333 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,6 @@ Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI. -## Running -Before setting up the drop app, be sure that you have a server set up. -The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html) - -## Current features -Currently supported are the following features: -- Signin (with custom server) -- Database registering & recovery -- Dynamic library fetching from server -- Installing & uninstalling games -- Download progress monitoring -- Launching / playing games - ## Development Install dependencies with `yarn` @@ -23,7 +10,7 @@ Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use she To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`: -e.g. `RUST_LOG=debug yarn tauri dev` +e.g. `RUST_LOG=debug yarn taudi dev` ## Contributing Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines. \ No newline at end of file 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/auth/failed.vue b/pages/auth/failed.vue index 1eca3c0..db8a6e5 100644 --- a/pages/auth/failed.vue +++ b/pages/auth/failed.vue @@ -26,7 +26,7 @@ import { XCircleIcon } from "@heroicons/vue/16/solid"; const route = useRoute(); -const message = route.query.error ?? "An unknown error occurred"; +const message = route.query.error ?? "An unknown error occurred."; definePageMeta({ layout: "mini", diff --git a/pages/index.vue b/pages/index.vue index 52bc1df..72433ae 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,7 +1,4 @@ \ No newline at end of file diff --git a/pages/settings/interface.vue b/pages/settings/interface.vue index 3df236b..27e0f69 100644 --- a/pages/settings/interface.vue +++ b/pages/settings/interface.vue @@ -1,7 +1,3 @@ - - + \ No newline at end of file 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..ff38943 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -255,29 +255,12 @@ dependencies = [ "system-deps", ] -[[package]] -name = "atomic-instant-full" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe" - [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auto-launch" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" -dependencies = [ - "dirs 4.0.0", - "thiserror 1.0.69", - "winreg 0.10.1", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -887,16 +870,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys 0.3.7", + "dirs-sys", ] [[package]] @@ -905,18 +879,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", + "dirs-sys", ] [[package]] @@ -1006,9 +969,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 +980,6 @@ dependencies = [ "log4rs", "md5", "openssl", - "parking_lot 0.12.3", "rayon", "reqwest", "rustbreak", @@ -1026,18 +987,14 @@ dependencies = [ "serde", "serde-binary", "serde_json", - "serde_with", "shared_child", - "slice-deque", "tauri", "tauri-build", - "tauri-plugin-autostart", "tauri-plugin-deep-link", "tauri-plugin-dialog", "tauri-plugin-os", "tauri-plugin-shell", "tauri-plugin-single-instance", - "throttle_my_fn", "tokio", "umu-wrapper-lib", "url", @@ -1090,7 +1047,7 @@ dependencies = [ "rustc_version", "toml 0.8.2", "vswhom", - "winreg 0.52.0", + "winreg", ] [[package]] @@ -2351,7 +2308,7 @@ dependencies = [ "log", "log-mdc", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "rand 0.8.5", "serde", "serde-value", @@ -2369,15 +2326,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 +2925,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 +2932,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 +2943,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -3455,15 +3378,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" @@ -3910,9 +3824,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", @@ -3928,9 +3842,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", "proc-macro2", @@ -4051,17 +3965,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 +3997,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 +4055,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 +4208,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "raw-window-handle", "scopeguard", "tao-macros", @@ -4342,7 +4245,7 @@ checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c" dependencies = [ "anyhow", "bytes", - "dirs 5.0.1", + "dirs", "dunce", "embed_plist", "futures-util", @@ -4392,7 +4295,7 @@ checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01" dependencies = [ "anyhow", "cargo_toml", - "dirs 5.0.1", + "dirs", "glob", "heck 0.5.0", "json-patch", @@ -4464,20 +4367,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-plugin-autostart" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c13f843e5e5df3eed270fc42b02923cc1a6b5c7e56b0f3ac1d858ab2c8b5fb" -dependencies = [ - "auto-launch", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.9", -] - [[package]] name = "tauri-plugin-deep-link" version = "2.2.0" @@ -4766,18 +4655,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" @@ -4994,7 +4871,7 @@ checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" dependencies = [ "core-graphics", "crossbeam-channel", - "dirs 5.0.1", + "dirs", "libappindicator", "muda", "objc2", @@ -5900,15 +5777,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.52.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f9f24c3..e8bc070 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" @@ -26,6 +26,7 @@ tauri-build = { version = "2.0.0", features = [] } [dependencies] tauri-plugin-shell = "2.0.0" +serde = { version = "1", features = ["derive", "rc"] } serde_json = "1" serde-binary = "0.5.0" rayon = "1.10.0" @@ -43,13 +44,7 @@ chrono = "0.4.38" tauri-plugin-os = "2" boxcar = "0.2.7" 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" @@ -88,10 +83,6 @@ features = [] # You can also use "yaml_enc" or "bin_enc" version = "0.12" features = ["json", "blocking"] -[dependencies.serde] -version = "1" -features = ["derive", "rc"] - [profile.release] lto = true codegen-units = 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 62% rename from src-tauri/src/remote/auth.rs rename to src-tauri/src/auth.rs index d2594de..24c8b57 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,38 +64,40 @@ 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> { let path_chunks: Vec<&str> = path.split("/").collect(); if path_chunks.len() != 3 { app.emit("auth/failed", ()).unwrap(); - return Err(RemoteAccessError::HandshakeFailed( - "failed to parse token".to_string(), - )); + return Err(RemoteAccessError::InvalidResponse); } let base_url = { - let handle = borrow_db_checked(); + let handle = DB.borrow_data().unwrap(); Url::parse(handle.base_url.as_str())? }; @@ -114,18 +111,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 +135,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 +155,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 +171,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 +180,53 @@ 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)) } diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs deleted file mode 100644 index dec9148..0000000 --- a/src-tauri/src/autostart.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; -use log::debug; -use tauri::AppHandle; -use tauri_plugin_autostart::ManagerExt; - -pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), String> { - let manager = app.autolaunch(); - if enabled { - manager.enable().map_err(|e| e.to_string())?; - debug!("enabled autostart"); - } else { - manager.disable().map_err(|e| e.to_string())?; - debug!("eisabled autostart"); - } - - // Store the state in DB - let mut db_handle = borrow_db_mut_checked(); - db_handle.settings.autostart = enabled; - drop(db_handle); - save_db(); - - Ok(()) -} - -pub fn get_autostart_enabled_logic(app: AppHandle) -> Result { - // First check DB state - let db_handle = borrow_db_checked(); - let db_state = db_handle.settings.autostart; - drop(db_handle); - - // Get actual system state - let manager = app.autolaunch(); - let system_state = manager.is_enabled()?; - - // If they don't match, sync to DB state - if db_state != system_state { - if db_state { - manager.enable()?; - } else { - manager.disable()?; - } - } - - Ok(db_state) -} - -// New function to sync state on startup -pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> { - let db_handle = borrow_db_checked(); - let should_be_enabled = db_handle.settings.autostart; - drop(db_handle); - - let manager = app.autolaunch(); - let current_state = manager.is_enabled().map_err(|e| e.to_string())?; - - if current_state != should_be_enabled { - if should_be_enabled { - manager.enable().map_err(|e| e.to_string())?; - debug!("synced autostart: enabled"); - } else { - manager.disable().map_err(|e| e.to_string())?; - debug!("synced autostart: disabled"); - } - } - - Ok(()) -} -#[tauri::command] -pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> { - toggle_autostart_logic(app, enabled) -} - -#[tauri::command] -pub fn get_autostart_enabled(app: AppHandle) -> Result { - get_autostart_enabled_logic(app) -} diff --git a/src-tauri/src/cleanup.rs b/src-tauri/src/cleanup.rs index 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/db.rs b/src-tauri/src/database/db.rs deleted file mode 100644 index b92de3d..0000000 --- a/src-tauri/src/database/db.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::{ - collections::HashMap, - fs::{self, create_dir_all}, - hash::Hash, - path::{Path, PathBuf}, - sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, -}; - -use chrono::Utc; -use directories::BaseDirs; -use log::{debug, error, info}; -use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_with::serde_as; -use tauri::AppHandle; -use url::Url; - -use crate::{ - database::settings::Settings, - download_manager::downloadable_metadata::DownloadableMetadata, - games::{library::push_game_update, state::GameStatusManager}, - process::process_manager::Platform, - DB, -}; - -#[derive(serde::Serialize, Clone, Deserialize)] -pub struct DatabaseAuth { - pub private: String, - pub cert: String, - pub client_id: String, -} - -// Strings are version names for a particular game -#[derive(Serialize, Clone, Deserialize)] -#[serde(tag = "type")] -pub enum GameDownloadStatus { - Remote {}, - SetupRequired { - version_name: String, - install_dir: String, - }, - Installed { - version_name: String, - install_dir: String, - }, -} - -// Stuff that shouldn't be synced to disk -#[derive(Clone, Serialize)] -pub enum ApplicationTransientStatus { - Downloading { version_name: String }, - Uninstalling {}, - Updating { version_name: String }, - Running {}, -} - -#[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, -} - -#[serde_as] -#[derive(Serialize, Clone, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct DatabaseApplications { - 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>, - pub installed_game_version: HashMap, - - #[serde(skip)] - pub transient_statuses: HashMap, -} - -#[derive(Serialize, Deserialize, Clone, Default)] -pub struct Database { - #[serde(default)] - pub settings: Settings, - pub auth: Option, - pub base_url: String, - pub applications: DatabaseApplications, - pub prev_database: Option, -} -impl Database { - fn new>(games_base_dir: T, prev_database: Option) -> Self { - Self { - applications: DatabaseApplications { - install_dirs: vec![games_base_dir.into()], - game_statuses: HashMap::new(), - game_versions: HashMap::new(), - installed_game_version: HashMap::new(), - transient_statuses: HashMap::new(), - }, - prev_database, - base_url: "".to_owned(), - auth: None, - settings: Settings { - autostart: false, - max_download_threads: 4, - }, - } - } -} -pub static DATA_ROOT_DIR: LazyLock> = - LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); - -// Custom JSON serializer to support everything we need -#[derive(Debug, Default, Clone)] -pub struct DropDatabaseSerializer; - -impl DeSerializer for DropDatabaseSerializer { - fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> { - serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string())) - } - - fn deserialize(&self, s: R) -> rustbreak::error::DeSerResult { - serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string())) - } -} - -pub type DatabaseInterface = - rustbreak::Database; - -pub trait DatabaseImpls { - fn set_up_database() -> DatabaseInterface; - fn database_is_set_up(&self) -> bool; - fn fetch_base_url(&self) -> Url; -} -impl DatabaseImpls for DatabaseInterface { - fn set_up_database() -> DatabaseInterface { - let data_root_dir = DATA_ROOT_DIR.lock().unwrap(); - let db_path = data_root_dir.join("drop.db"); - 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); - create_dir_all(data_root_dir.clone()).unwrap(); - create_dir_all(games_base_dir.clone()).unwrap(); - 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), - }, - false => { - let default = Database::new(games_base_dir, None); - debug!( - "Creating database at path {}", - db_path.as_os_str().to_str().unwrap() - ); - PathDatabase::create_at_path(db_path, default) - .expect("Database could not be created") - } - } - } - - fn database_is_set_up(&self) -> bool { - !self.borrow_data().unwrap().base_url.is_empty() - } - - fn fetch_base_url(&self) -> Url { - let handle = self.borrow_data().unwrap(); - Url::parse(&handle.base_url).unwrap() - } -} - -pub fn set_game_status, &DownloadableMetadata)>( - app_handle: &AppHandle, - meta: DownloadableMetadata, - setter: F, -) { - let mut db_handle = borrow_db_mut_checked(); - setter(&mut db_handle, &meta); - drop(db_handle); - save_db(); - - let status = GameStatusManager::fetch_state(&meta.id); - - push_game_update(app_handle, &meta.id, status); -} -// TODO: Make the error relelvant rather than just assume that it's a Deserialize error -fn handle_invalid_database( - _e: RustbreakError, - db_path: PathBuf, - games_base_dir: PathBuf, -) -> rustbreak::Database { - let new_path = { - let time = Utc::now().timestamp(); - let mut base = db_path.clone(); - base.set_file_name(format!("drop.db.backup-{}", time)); - base - }; - info!( - "old database stored at: {}", - new_path.to_string_lossy().to_string() - ); - fs::rename(&db_path, &new_path).unwrap(); - - let db = Database::new( - games_base_dir.into_os_string().into_string().unwrap(), - Some(new_path), - ); - - PathDatabase::create_at_path(db_path, db).expect("Database could not be created") -} - -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) - } - } -} 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/db.rs b/src-tauri/src/db.rs new file mode 100644 index 0000000..bfad526 --- /dev/null +++ b/src-tauri/src/db.rs @@ -0,0 +1,226 @@ +use std::{ + collections::HashMap, + fs::{self, create_dir_all}, + path::{Path, PathBuf}, + sync::{LazyLock, Mutex, RwLockWriteGuard}, +}; + +use directories::BaseDirs; +use log::debug; +use rustbreak::{DeSerError, DeSerializer, PathDatabase}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tauri::AppHandle; +use url::Url; + +use crate::{library::push_application_update, process::process_manager::Platform, state::DownloadStatusManager, DB}; + +#[derive(serde::Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseAuth { + pub private: String, + pub cert: String, + pub client_id: String, +} + +pub struct GameStatusData { + version_name: String, + install_dir: String, +} + +// Strings are version names for a particular game +#[derive(Serialize, Clone, Deserialize)] +#[serde(tag = "type")] +pub enum ApplicationStatus { + Remote {}, + SetupRequired { + version_name: String, + install_dir: String, + }, + Installed { + version_name: String, + install_dir: String, + }, +} + +// Stuff that shouldn't be synced to disk +#[derive(Clone, Serialize)] +pub enum ApplicationTransientStatus { + Downloading { version_name: String }, + Uninstalling {}, + Updating { version_name: String }, + Running {}, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ApplicationVersion { + pub version_index: usize, + pub version_name: String, + pub launch_command: String, + pub setup_command: String, + pub platform: Platform, +} + +#[derive(Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseApplications { + pub install_dirs: Vec, + // Guaranteed to exist if the game also exists in the app state map + pub statuses: HashMap, + pub versions: HashMap>, + + #[serde(skip)] + pub transient_statuses: HashMap, +} + +#[derive(Serialize, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Database { + pub auth: Option, + pub base_url: String, + pub applications: DatabaseApplications, +} +pub static DATA_ROOT_DIR: LazyLock> = + LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); + +// Custom JSON serializer to support everything we need +#[derive(Debug, Default, Clone)] +pub struct DropDatabaseSerializer; + +impl DeSerializer for DropDatabaseSerializer { + fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> { + serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string())) + } + + fn deserialize(&self, s: R) -> rustbreak::error::DeSerResult { + serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string())) + } +} + +pub type DatabaseInterface = + rustbreak::Database; + +pub trait DatabaseImpls { + fn set_up_database() -> DatabaseInterface; + fn database_is_set_up(&self) -> bool; + fn fetch_base_url(&self) -> Url; +} +impl DatabaseImpls for DatabaseInterface { + fn set_up_database() -> DatabaseInterface { + let data_root_dir = DATA_ROOT_DIR.lock().unwrap(); + let db_path = data_root_dir.join("drop.db"); + 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); + 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(); + + #[allow(clippy::let_and_return)] + let exists = fs::exists(db_path.clone()).unwrap(); + + match exists { + true => PathDatabase::load_from_path(db_path).expect("Database loading failed"), + false => { + let default = Database { + auth: None, + base_url: "".to_string(), + applications: DatabaseApplications { + install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], + statuses: HashMap::new(), + transient_statuses: HashMap::new(), + versions: HashMap::new(), + }, + }; + debug!( + "Creating database at path {}", + db_path.as_os_str().to_str().unwrap() + ); + PathDatabase::create_at_path(db_path, default) + .expect("Database could not be created") + } + } + } + + fn database_is_set_up(&self) -> bool { + !self.borrow_data().unwrap().base_url.is_empty() + } + + fn fetch_base_url(&self) -> Url { + let handle = self.borrow_data().unwrap(); + Url::parse(&handle.base_url).unwrap() + } +} + +#[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_application_status, &String)>( + app_handle: &AppHandle, + id: String, + setter: F, +) { + let mut db_handle = DB.borrow_data_mut().unwrap(); + setter(&mut db_handle, &id); + drop(db_handle); + DB.save().unwrap(); + + let status = DownloadStatusManager::fetch_state(&id); + + push_application_update(app_handle, id, status); +} 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..064d57e 100644 --- a/src-tauri/src/download_manager/download_manager.rs +++ b/src-tauri/src/download_manager/download_manager.rs @@ -4,29 +4,31 @@ 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 crate::downloads::download_agent::GameDownloadAgent; -use super::{ - download_manager_builder::{CurrentProgressObject, DownloadAgent}, - downloadable_metadata::DownloadableMetadata, - queue::Queue, -}; +use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent, DownloadableQueueStandin}, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, queue::Queue}; +pub enum DownloadType { + Game, + Tool, + DLC, + Mod +} pub enum DownloadManagerSignal { /// Resumes (or starts) the DownloadManager Go, /// Pauses the DownloadManager Stop, /// Called when a DownloadAgent has fully completed a download. - Completed(DownloadableMetadata), + Completed(String), /// Generates and appends a DownloadAgent /// to the registry and queue Queue(DownloadAgent), @@ -34,10 +36,10 @@ pub enum DownloadManagerSignal { /// download, sync everything to disk, and /// then exit Finish, - /// Stops, removes, and tells a download to cleanup - Cancel(DownloadableMetadata), + /// Stops (but doesn't remove) current download + Cancel, /// Removes a given application - Remove(DownloadableMetadata), + Remove(String), /// Any error which occurs in the agent Error(ApplicationDownloadError), /// Pushes UI update @@ -45,7 +47,7 @@ pub enum DownloadManagerSignal { UpdateUIStats(usize, usize), //kb/s and seconds /// Uninstall download /// Takes download ID - Uninstall(DownloadableMetadata), + Uninstall(String), } #[derive(Debug, Clone)] @@ -84,11 +86,27 @@ 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, } +impl From> for DownloadableQueueStandin { + fn from(value: Arc) -> Self { + Self { + id: value.id(), + status: Mutex::from(DownloadStatus::Queued), + progress: value.progress().clone(), + } + } +} +impl Debug for DownloadableQueueStandin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DownloadableQueueStandin") + .field("id", &self.id) + .finish() + } +} #[allow(dead_code)] impl DownloadManager { @@ -99,7 +117,7 @@ impl DownloadManager { command_sender: Sender, ) -> Self { Self { - terminator: Mutex::new(Some(terminator)), + terminator, download_queue, progress, command_sender, @@ -108,35 +126,40 @@ impl DownloadManager { pub fn queue_download( &self, - download: DownloadAgent, + id: String, + version: String, + target_download_dir: usize, ) -> Result<(), SendError> { - info!("creating download with meta {:?}", download.metadata()); - self.command_sender - .send(DownloadManagerSignal::Queue(download))?; + info!("Adding download id {}", id); + self.command_sender.send(DownloadManagerSignal::Queue( + id, + version, + target_download_dir, + ))?; self.command_sender.send(DownloadManagerSignal::Go) } - pub fn edit(&self) -> MutexGuard<'_, VecDeque> { + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.download_queue.edit() } - pub fn read_queue(&self) -> VecDeque { + pub fn read_queue(&self) -> VecDeque> { self.download_queue.read() } pub fn get_current_download_progress(&self) -> Option { let progress_object = (*self.progress.lock().unwrap()).clone()?; Some(progress_object.get_progress()) } - pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) { + pub fn rearrange_string(&self, id: String, new_index: usize) { let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, meta).unwrap(); + let current_index = get_index_from_id(&mut queue, id).unwrap(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); self.command_sender .send(DownloadManagerSignal::UpdateUIQueue) .unwrap(); } - pub fn cancel(&self, meta: DownloadableMetadata) { + pub fn cancel(&self, id: String) { self.command_sender - .send(DownloadManagerSignal::Cancel(meta)) + .send(DownloadManagerSignal::Remove(id)) .unwrap(); } pub fn rearrange(&self, current_index: usize, new_index: usize) { @@ -147,18 +170,16 @@ impl DownloadManager { let needs_pause = current_index == 0 || new_index == 0; if needs_pause { self.command_sender - .send(DownloadManagerSignal::Stop) + .send(DownloadManagerSignal::Cancel) .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 +188,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,30 +197,26 @@ 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) { + pub fn uninstall_application(&self, id: String) { self.command_sender - .send(DownloadManagerSignal::Uninstall(meta)) + .send(DownloadManagerSignal::Uninstall(id)) .unwrap(); } - pub fn get_sender(&self) -> Sender { - self.command_sender.clone() - } } /// Takes in the locked value from .edit() and attempts to /// get the index of whatever id is passed in fn get_index_from_id( - queue: &mut MutexGuard<'_, VecDeque>, - meta: &DownloadableMetadata, + queue: &mut MutexGuard<'_, VecDeque>>, + id: String, ) -> Option { queue .iter() - .position(|download_agent| download_agent == meta) + .position(|download_agent| download_agent.id == id) } diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs index dd43dc2..fe0da0d 100644 --- a/src-tauri/src/download_manager/download_manager_builder.rs +++ b/src-tauri/src/download_manager/download_manager_builder.rs @@ -1,29 +1,30 @@ 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}, + db::{set_application_status, ApplicationStatus, ApplicationTransientStatus, Database}, download_manager::{download_manager::{DownloadStatus, DownloadType}, generate_downloadable::generate_downloadable}, downloads::download_agent::{self, GameDownloadAgent}, library::{ + on_game_complete, push_application_update, QueueUpdateEvent, + QueueUpdateEventQueueData, StatsUpdateEvent, + }, state::DownloadStatusManager, DB }; -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 struct DownloadableQueueStandin { + pub id: String, + pub status: Mutex, + pub progress: Arc, +} pub type DownloadAgent = Arc>; pub type CurrentProgressObject = Arc>>>; @@ -56,7 +57,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. @@ -65,7 +66,7 @@ Behold, my madness - quexeky */ pub struct DownloadManagerBuilder { - download_agent_registry: HashMap, + download_agent_registry: HashMap, DownloadAgent>, download_queue: Queue, command_receiver: Receiver, sender: Sender, @@ -103,13 +104,247 @@ impl DownloadManagerBuilder { DownloadManager::new(terminator, queue, active_progress, command_sender) } - fn set_status(&self, status: DownloadManagerStatus) { - *self.status.lock().unwrap() = status; + fn manage_queue(mut self) -> Result<(), ()> { + loop { + let signal = match self.command_receiver.recv() { + Ok(signal) => signal, + Err(_) => return Err(()), + }; + + match signal { + DownloadManagerSignal::Go => { + self.manage_go_signal(); + } + DownloadManagerSignal::Stop => { + self.manage_stop_signal(); + } + DownloadManagerSignal::Completed(id) => { + self.manage_completed_signal(id); + } + DownloadManagerSignal::Queue(download_agent) => { + self.manage_queue_signal(download_agent); + } + DownloadManagerSignal::Error(e) => { + self.manage_error_signal(e); + } + DownloadManagerSignal::Cancel => { + self.manage_cancel_signal(); + } + DownloadManagerSignal::UpdateUIQueue => { + self.push_ui_queue_update(); + } + DownloadManagerSignal::UpdateUIStats(kbs, time) => { + self.push_ui_stats_update(kbs, time); + } + DownloadManagerSignal::Finish => { + self.stop_and_wait_current_download(); + return Ok(()); + } + DownloadManagerSignal::Remove(id) => { + self.manage_remove_download_from_queue(id); + } + DownloadManagerSignal::Uninstall(id) => { + self.uninstall_application(id); + } + }; + } + } + fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { + info!("Got signal Queue"); + let meta = download_agent.metadata(); + + if self.download_queue.exists(meta.clone()) { + info!("Download with same ID already exists"); + return; + } + + let download_agent = generate_downloadable(meta.clone()); + download_agent.on_initialised(&self.app_handle); + self.download_queue.append(meta.clone()); + self.download_agent_registry.insert(meta, download_agent); + + self.sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap(); + } +} +/* +// Refactored to consolidate this type. It's a monster. +pub type DownloadAgent = Arc>>; + + +impl DownloadManagerBuilder { + fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) { + info!("Got signal Queue"); + + if let Some(index) = self.download_queue.get_by_id(id.clone()) { + // Should always give us a value + if let Some(download_agent) = self.download_agent_registry.get(&id) { + let download_agent_handle = download_agent.lock().unwrap(); + if download_agent_handle.version() == version { + info!("Application with same version already queued, skipping"); + return; + } + // If it's not the same, we want to cancel the current one, and then add the new one + drop(download_agent_handle); + + self.manage_remove_download_from_queue(id.clone()); + } + } + + let download_agent = Arc::new(Mutex::new(DownloadType::Game.generate( + id.clone(), + version, + target_download_dir, + self.sender.clone(), + ))); + let download_agent_lock = download_agent.lock().unwrap(); + + let agent_status = DownloadStatus::Queued; + let interface_data = DownloadableQueueStandin { + id: id.clone(), + status: Mutex::new(agent_status), + progress: download_agent_lock.progress() + }; + let version_name = download_agent_lock.version().clone(); + + drop(download_agent_lock); + + self.download_agent_registry + .insert(interface_data.id.clone(), download_agent); + self.download_queue.append(interface_data); + + self.set_application_status(id, |db, id| { + db.applications.transient_statuses.insert( + id.to_string(), + ApplicationTransientStatus::Downloading { version_name }, + ); + }); + self.sender + .send(DownloadManagerSignal::UpdateUIQueue) + .unwrap(); + } + fn manage_go_signal(&mut self) { + if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) { + return; + } + + if self.current_download_agent.is_some() { + info!("skipping go signal due to existing download job"); + return; + } + + info!("current download queue: {:?}", self.download_queue.read()); + let agent_data = self.download_queue.read().front().unwrap().clone(); + info!("starting download for {}", agent_data.id.clone()); + let download_agent = self + .download_agent_registry + .get(&agent_data.id) + .unwrap() + .clone(); + let download_agent_lock = download_agent.lock().unwrap(); + self.current_download_agent = Some(agent_data); + // Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData + let agent_data = self.current_download_agent.clone().unwrap(); + + let version_name = download_agent_lock.version().clone(); + + let progress_object = download_agent_lock.progress(); + *self.progress.lock().unwrap() = Some(progress_object); + + let active_control_flag = download_agent_lock.control_flag(); + self.active_control_flag = Some(active_control_flag.clone()); + + let sender = self.sender.clone(); + + drop(download_agent_lock); + + info!("Spawning download"); + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + *download_thread_lock = Some(spawn(move || { + let mut download_agent_lock = download_agent.lock().unwrap(); + match download_agent_lock.download() { + // Returns once we've exited the download + // (not necessarily completed) + // The download agent will fire the completed event for us + Ok(_) => {} + // If an error occurred while *starting* the download + Err(err) => { + error!("error while managing download: {}", err); + sender.send(DownloadManagerSignal::Error(err)).unwrap(); + } + }; + drop(download_agent_lock); + })); + + // Set status for applications + for queue_application in self.download_queue.read() { + let mut status_handle = queue_application.status.lock().unwrap(); + if queue_application.id == agent_data.id { + *status_handle = DownloadStatus::Downloading; + } else { + *status_handle = DownloadStatus::Queued; + } + drop(status_handle); + } + + // Set flags for download manager + active_control_flag.set(DownloadThreadControlFlag::Go); + self.set_status(DownloadManagerStatus::Downloading); + self.set_application_status(agent_data.id.clone(), |db, id| { + db.applications.transient_statuses.insert( + id.to_string(), + ApplicationTransientStatus::Downloading { version_name }, + ); + }); + + self.sender + .send(DownloadManagerSignal::UpdateUIQueue) + .unwrap(); } - fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent { + fn push_ui_stats_update(&self, kbs: usize, time: usize) { + let event_data = StatsUpdateEvent { speed: kbs, time }; + + self.app_handle.emit("update_stats", event_data).unwrap(); + } + + fn push_ui_queue_update(&self) { + let queue = self.download_queue.read(); + let queue_objs: Vec = queue + .iter() + .map(|interface| QueueUpdateEventQueueData { + id: interface.id.clone(), + status: interface.status.lock().unwrap().clone(), + progress: interface.progress.get_progress(), + }) + .collect(); + + let status_handle = self.status.lock().unwrap(); + let status = status_handle.clone(); + drop(status_handle); + + let event_data = QueueUpdateEvent { + queue: queue_objs, + status, + }; + self.app_handle.emit("update_queue", event_data).unwrap(); + } + + fn stop_and_wait_current_download(&self) { + self.set_status(DownloadManagerStatus::Paused); + if let Some(current_flag) = &self.active_control_flag { + current_flag.set(DownloadThreadControlFlag::Stop); + } + + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + if let Some(current_download_thread) = download_thread_lock.take() { + current_download_thread.join().unwrap(); + } + drop(download_thread_lock); + } + + fn remove_and_cleanup_front_download(&mut self, id: &String) -> DownloadAgent { self.download_queue.pop_front(); - let download_agent = self.download_agent_registry.remove(meta).unwrap(); + let download_agent = self.download_agent_registry.remove(id).unwrap(); self.cleanup_current_download(); download_agent } @@ -126,18 +361,6 @@ impl DownloadManagerBuilder { drop(download_thread_lock); } - fn stop_and_wait_current_download(&self) { - self.set_status(DownloadManagerStatus::Paused); - if let Some(current_flag) = &self.active_control_flag { - current_flag.set(DownloadThreadControlFlag::Stop); - } - - let mut download_thread_lock = self.current_download_thread.lock().unwrap(); - if let Some(current_download_thread) = download_thread_lock.take() { - current_download_thread.join().unwrap(); - } - } - fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.command_receiver.recv() { @@ -152,15 +375,18 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Stop => { self.manage_stop_signal(); } - DownloadManagerSignal::Completed(meta) => { - self.manage_completed_signal(meta); + DownloadManagerSignal::Completed(id) => { + self.manage_completed_signal(id); } - DownloadManagerSignal::Queue(download_agent) => { - self.manage_queue_signal(download_agent); + DownloadManagerSignal::Queue(id, version, target_download_dir) => { + self.manage_queue_signal(id, version, target_download_dir); } DownloadManagerSignal::Error(e) => { self.manage_error_signal(e); } + DownloadManagerSignal::Cancel => { + self.manage_cancel_signal(); + } DownloadManagerSignal::UpdateUIQueue => { self.push_ui_queue_update(); } @@ -171,194 +397,255 @@ impl DownloadManagerBuilder { self.stop_and_wait_current_download(); return Ok(()); } - DownloadManagerSignal::Cancel(meta) => { - self.manage_cancel_signal(&meta); + DownloadManagerSignal::Remove(id) => { + self.manage_remove_download_from_queue(id); + } + DownloadManagerSignal::Uninstall(id) => { + self.uninstall_application(id); } - _ => {} }; } } - fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { - debug!("got signal Queue"); - let meta = download_agent.metadata(); - debug!("queue metadata: {:?}", meta); + fn uninstall_application(&mut self, id: String) { + // Removes the download if it's in the queue + self.manage_remove_download_from_queue(id.clone()); - if self.download_queue.exists(meta.clone()) { - warn!("download with same ID already exists"); + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle + .applications + .transient_statuses + .entry(id.clone()) + .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {}); + push_application_update( + &self.app_handle, + id.clone(), + (None, Some(ApplicationTransientStatus::Uninstalling {})), + ); + + let previous_state = db_handle.applications.statuses.get(&id).cloned(); + if previous_state.is_none() { + info!("uninstall job doesn't have previous state, failing silently"); + return; + } + let previous_state = previous_state.unwrap(); + if let Some((_version_name, install_dir)) = match previous_state { + ApplicationStatus::Installed { + version_name, + install_dir, + } => Some((version_name, install_dir)), + ApplicationStatus::SetupRequired { + version_name, + install_dir, + } => Some((version_name, install_dir)), + _ => None, + } { + db_handle + .applications + .transient_statuses + .entry(id.clone()) + .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {}); + drop(db_handle); + + let sender = self.sender.clone(); + let app_handle = self.app_handle.clone(); + spawn(move || match remove_dir_all(install_dir) { + Err(e) => { + sender + .send(DownloadManagerSignal::Error(ApplicationDownloadError::IoError( + e.kind(), + ))) + .unwrap(); + } + Ok(_) => { + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle.applications.transient_statuses.remove(&id); + db_handle + .applications + .statuses + .entry(id.clone()) + .and_modify(|e| *e = ApplicationStatus::Remote {}); + drop(db_handle); + DB.save().unwrap(); + + info!("uninstalled {}", id); + + push_application_update(&app_handle, id, (Some(ApplicationStatus::Remote {}), None)); + } + }); + } + } + + fn manage_remove_download_from_queue(&mut self, id: DownloadableMetadata) { + if let Some(current_download) = &self.current_download_agent { + if current_download.lock().unwrap().metadata() == id { + self.manage_cancel_signal(); + } + } + + let index = self.download_queue.get_by_id(id.clone()); + if let Some(index) = index { + let mut queue_handle = self.download_queue.edit(); + queue_handle.remove(index); + set_application_status(&self.app_handle, id, |db_handle, id| { + db_handle.applications.transient_statuses.remove(id); + }); + drop(queue_handle); + } + + if self.current_download_agent.is_none() { + self.manage_go_signal(); + } + + self.push_ui_queue_update(); + } + + fn manage_stop_signal(&mut self) { + info!("Got signal 'Stop'"); + self.set_status(DownloadManagerStatus::Paused); + if let Some(active_control_flag) = self.active_control_flag.clone() { + active_control_flag.set(DownloadThreadControlFlag::Stop); + } + } + + fn manage_completed_signal(&mut self, id: String) { + info!("Got signal 'Completed'"); + if let Some(interface) = &self.current_download_agent { + if interface.id == id { + info!("Popping consumed data"); + let download_agent = self.remove_and_cleanup_front_download(&id); + let download_agent_lock = download_agent.lock().unwrap(); + + drop(download_agent_lock); + + if let Err(error) = + self.current_download_agent.on_complete(&self.app_handle); + { + self.sender + .send(DownloadManagerSignal::Error( + ApplicationDownloadError::Communication(error), + )) + .unwrap(); + } + } + } + self.sender + .send(DownloadManagerSignal::UpdateUIQueue) + .unwrap(); + self.sender.send(DownloadManagerSignal::Go).unwrap(); + } + + + fn manage_go_signal(&mut self) { + if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) { return; } - download_agent.on_initialised(&self.app_handle); - self.download_queue.append(meta.clone()); - self.download_agent_registry.insert(meta, download_agent); + if self.current_download_agent.is_some() { + info!("skipping go signal due to existing download job"); + return; + } + + info!("current download queue: {:?}", self.download_queue.read()); + let agent_data = self.download_queue.read().front().unwrap().clone(); + info!("starting download for {}", agent_data.id.clone()); + let download_agent = self + .download_agent_registry + .get(&agent_data.id) + .unwrap() + .clone(); + let download_agent_lock = download_agent.lock().unwrap(); + self.current_download_agent = Some(agent_data); + // Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData + let agent_data = self.current_download_agent.clone().unwrap(); + + let version_name = download_agent_lock.version().clone(); + + let progress_object = download_agent_lock.progress(); + *self.progress.lock().unwrap() = Some(progress_object); + + let active_control_flag = download_agent_lock.control_flag(); + self.active_control_flag = Some(active_control_flag.clone()); + + let sender = self.sender.clone(); + + drop(download_agent_lock); + + info!("Spawning download"); + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + *download_thread_lock = Some(spawn(move || { + let mut download_agent_lock = download_agent.lock().unwrap(); + match download_agent_lock.download() { + // Returns once we've exited the download + // (not necessarily completed) + // The download agent will fire the completed event for us + Ok(_) => {} + // If an error occurred while *starting* the download + Err(err) => { + error!("error while managing download: {}", err); + sender.send(DownloadManagerSignal::Error(err)).unwrap(); + } + }; + drop(download_agent_lock); + })); + + // Set status for applications + for queue_application in self.download_queue.read() { + let mut status_handle = queue_application.status.lock().unwrap(); + if queue_application.id == agent_data.id { + *status_handle = DownloadStatus::Downloading; + } else { + *status_handle = DownloadStatus::Queued; + } + drop(status_handle); + } + + // Set flags for download manager + active_control_flag.set(DownloadThreadControlFlag::Go); + self.set_status(DownloadManagerStatus::Downloading); + self.set_application_status(agent_data.id.clone(), |db, id| { + db.applications.transient_statuses.insert( + id.to_string(), + ApplicationTransientStatus::Downloading { version_name }, + ); + }); 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; - } - - if self.current_download_agent.is_some() { - if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() { - debug!( - "Current download agent: {:?}", - self.current_download_agent.as_ref().unwrap().metadata() - ); - return; - } - } - - debug!("current download queue: {:?}", self.download_queue.read()); - - // Should always be Some if the above two statements keep going - let agent_data = self.download_queue.read().front().unwrap().clone(); - - info!("starting download for {:?}", agent_data); - - let download_agent = self - .download_agent_registry - .get(&agent_data) - .unwrap() - .clone(); - - self.active_control_flag = Some(download_agent.control_flag()); - self.current_download_agent = Some(download_agent.clone()); - - let sender = self.sender.clone(); - - let mut download_thread_lock = self.current_download_thread.lock().unwrap(); - let app_handle = self.app_handle.clone(); - - *download_thread_lock = Some(spawn(move || { - 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(); - } - // 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(); - })); - - self.set_status(DownloadManagerStatus::Downloading); - let active_control_flag = self.active_control_flag.clone().unwrap(); - active_control_flag.set(DownloadThreadControlFlag::Go); - } - fn manage_stop_signal(&mut self) { - debug!("got signal Stop"); - - if let Some(active_control_flag) = self.active_control_flag.clone() { - self.set_status(DownloadManagerStatus::Paused); - active_control_flag.set(DownloadThreadControlFlag::Stop); - } - } - fn manage_completed_signal(&mut self, meta: DownloadableMetadata) { - debug!("got signal Completed"); - if let Some(interface) = &self.current_download_agent { - if interface.metadata() == meta { - self.remove_and_cleanup_front_download(&meta); - } - } - self.push_ui_queue_update(); - 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()); + let current_status = self.current_download_agent.clone().unwrap(); - 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_status.id); // Remove all the locks and shit, and remove from queue + + self.app_handle + .emit("download_error", error.to_string()) + .unwrap(); + + let mut lock = current_status.status.lock().unwrap(); + *lock = DownloadStatus::Error; self.set_status(DownloadManagerStatus::Error(error)); + + let id = current_status.id.clone(); + self.set_application_status(id, |db_handle, id| { + db_handle.applications.transient_statuses.remove(id); + }); + + self.sender + .send(DownloadManagerSignal::UpdateUIQueue) + .unwrap(); } - fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { - debug!("got signal Cancel"); + fn manage_cancel_signal(&mut self) { + self.stop_and_wait_current_download(); - if let Some(current_download) = &self.current_download_agent { - if ¤t_download.metadata() == meta { - self.set_status(DownloadManagerStatus::Paused); - current_download.on_cancelled(&self.app_handle); - self.stop_and_wait_current_download(); + info!("cancel waited for download to finish"); - self.download_queue.pop_front(); - - self.cleanup_current_download(); - debug!("current download queue: {:?}", self.download_queue.read()); - } - // TODO: Collapse these two into a single if statement somehow - else if let Some(download_agent) = self.download_agent_registry.get(meta) { - 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() - ); - } - } - } 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(); + self.cleanup_current_download(); } - fn push_ui_stats_update(&self, kbs: usize, time: usize) { - let event_data = StatsUpdateEvent { speed: kbs, time }; - - self.app_handle.emit("update_stats", event_data).unwrap(); - } - fn push_ui_queue_update(&self) { - let queue = &self.download_queue.read(); - let queue_objs = queue - .iter() - .map(|key| { - let val = self.download_agent_registry.get(key).unwrap(); - QueueUpdateEventQueueData { - meta: DownloadableMetadata::clone(key), - status: val.status(), - progress: val.progress().get_progress(), - current: val.progress().sum(), - max: val.progress().get_max(), - } - }) - .collect(); - - let event_data = QueueUpdateEvent { queue: queue_objs }; - self.app_handle.emit("update_queue", event_data).unwrap(); + fn set_status(&self, status: DownloadManagerStatus) { + *self.status.lock().unwrap() = status; } } +*/ \ 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..693652b 100644 --- a/src-tauri/src/download_manager/downloadable.rs +++ b/src-tauri/src/download_manager/downloadable.rs @@ -2,22 +2,16 @@ use std::sync::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_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject }; pub trait Downloadable: Send + Sync { - fn download(&self, app_handle: &AppHandle) -> Result; + fn download(&mut self) -> Result<(), ApplicationDownloadError>; fn progress(&self) -> Arc; fn control_flag(&self) -> DownloadThreadControl; - fn status(&self) -> DownloadStatus; - fn metadata(&self) -> DownloadableMetadata; + fn metadata(&self) -> Arc; fn on_initialised(&self, app_handle: &AppHandle); - fn on_error(&self, app_handle: &AppHandle, error: ApplicationDownloadError); + fn on_error(&self, app_handle: &AppHandle); fn on_complete(&self, app_handle: &AppHandle); - fn on_incomplete(&self, app_handle: &AppHandle); - fn on_cancelled(&self, app_handle: &AppHandle); } diff --git a/src-tauri/src/download_manager/downloadable_metadata.rs b/src-tauri/src/download_manager/downloadable_metadata.rs index 790b669..7036c51 100644 --- a/src-tauri/src/download_manager/downloadable_metadata.rs +++ b/src-tauri/src/download_manager/downloadable_metadata.rs @@ -1,26 +1,4 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)] -pub enum DownloadType { - Game, - Tool, - DLC, - Mod, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DownloadableMetadata { - pub id: String, - pub version: Option, - pub download_type: DownloadType, -} -impl DownloadableMetadata { - pub fn new(id: String, version: Option, download_type: DownloadType) -> Self { - Self { - id, - version, - download_type, - } - } + id: String, } 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..254436f --- /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: Arc) -> 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..64a147e 100644 --- a/src-tauri/src/download_manager/queue.rs +++ b/src-tauri/src/download_manager/queue.rs @@ -7,35 +7,29 @@ use super::downloadable_metadata::DownloadableMetadata; #[derive(Clone)] pub struct Queue { - inner: Arc>>, + inner: Arc>>>, } #[allow(dead_code)] -impl Default for Queue { - fn default() -> Self { - Self::new() - } -} - impl Queue { pub fn new() -> Self { Self { inner: Arc::new(Mutex::new(VecDeque::new())), } } - pub fn read(&self) -> VecDeque { + pub fn read(&self) -> VecDeque> { self.inner.lock().unwrap().clone() } - pub fn edit(&self) -> MutexGuard<'_, VecDeque> { + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.inner.lock().unwrap() } - pub fn pop_front(&self) -> Option { + 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 { + pub fn exists(&self, meta: Arc) -> bool { self.read().contains(&meta) } /// Either inserts `interface` at the specified index, or appends to @@ -44,28 +38,30 @@ impl Queue { if self.read().len() > index { self.append(interface); } else { - self.edit().insert(index, interface); + self.edit().insert(index, Arc::new(interface)); } } - pub fn append(&self, interface: DownloadableMetadata) { + pub fn append(&self, interface: Arc) { self.edit().push_back(interface); } - pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option { + pub fn pop_front_if_equal( + &self, + meta: &Arc, + ) -> 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(); } None } - pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option { + pub fn get_by_meta(&self, meta: &Arc) -> 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: &Arc, 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/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs new file mode 100644 index 0000000..f91372d --- /dev/null +++ b/src-tauri/src/downloads/download_agent.rs @@ -0,0 +1,323 @@ +use crate::auth::generate_authorization_header; +use crate::db::DatabaseImpls; +use crate::download_manager::application_download_error::ApplicationDownloadError; +use crate::download_manager::download_manager::DownloadManagerSignal; +use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; +use crate::download_manager::downloadable::Downloadable; +use crate::download_manager::progress_object::{ProgressHandle, ProgressObject}; +use crate::downloads::manifest::{DropDownloadContext, DropManifest}; +use crate::remote::RemoteAccessError; +use crate::DB; +use log::{debug, error, info}; +use rayon::ThreadPoolBuilder; +use std::collections::VecDeque; +use std::fs::{create_dir_all, File}; +use std::path::Path; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; +use std::time::Instant; +use urlencoding::encode; + +#[cfg(target_os = "linux")] +use rustix::fs::{fallocate, FallocateFlags}; + +use super::download_logic::download_game_chunk; +use super::stored_manifest::StoredManifest; + +pub struct GameDownloadAgent { + pub id: String, + pub version: String, + pub control_flag: DownloadThreadControl, + contexts: Vec, + completed_contexts: VecDeque, + pub manifest: Mutex>, + pub progress: Arc, + sender: Sender, + pub stored_manifest: StoredManifest, +} + + + +impl GameDownloadAgent { + pub fn new( + id: String, + version: String, + target_download_dir: usize, + sender: Sender, + ) -> Self { + // Don't run by default + let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); + + let db_lock = DB.borrow_data().unwrap(); + let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); + drop(db_lock); + + let base_dir_path = Path::new(&base_dir); + let data_base_dir_path = base_dir_path.join(id.clone()); + + let stored_manifest = + StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone()); + + Self { + id, + version, + control_flag, + manifest: Mutex::new(None), + contexts: Vec::new(), + completed_contexts: VecDeque::new(), + progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), + sender, + stored_manifest, + } + } + + // Blocking + pub fn setup_download(&mut self) -> Result<(), ApplicationDownloadError> { + self.ensure_manifest_exists()?; + info!("Ensured manifest exists"); + + self.ensure_contexts()?; + info!("Ensured contexts exists"); + + self.control_flag.set(DownloadThreadControlFlag::Go); + + Ok(()) + } + + // Blocking + pub fn download(&mut self) -> Result<(), ApplicationDownloadError> { + self.setup_download()?; + self.set_progress_object_params(); + let timer = Instant::now(); + self.run().map_err(|_| ApplicationDownloadError::DownloadError)?; + + info!( + "{} took {}ms to download", + self.id, + timer.elapsed().as_millis() + ); + Ok(()) + } + + pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> { + if self.manifest.lock().unwrap().is_some() { + return Ok(()); + } + + self.download_manifest() + } + + fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { + let base_url = DB.fetch_base_url(); + let manifest_url = base_url + .join( + format!( + "/api/v1/client/metadata/manifest?id={}&version={}", + self.id, + encode(&self.version) + ) + .as_str(), + ) + .unwrap(); + + let header = generate_authorization_header(); + let client = reqwest::blocking::Client::new(); + let response = client + .get(manifest_url.to_string()) + .header("Authorization", header) + .send() + .unwrap(); + + if response.status() != 200 { + return Err(ApplicationDownloadError::Communication( + RemoteAccessError::ManifestDownloadFailed( + response.status(), + response.text().unwrap(), + ), + )); + } + + let manifest_download = response.json::().unwrap(); + + if let Ok(mut manifest) = self.manifest.lock() { + *manifest = Some(manifest_download); + return Ok(()); + } + + Err(ApplicationDownloadError::Lock) + } + + fn set_progress_object_params(&self) { + // Avoid re-setting it + if self.progress.get_max() != 0 { + return; + } + + let length = self.contexts.len(); + + let chunk_count = self.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(); + } + + pub fn ensure_contexts(&mut self) -> Result<(), ApplicationDownloadError> { + if !self.contexts.is_empty() { + return Ok(()); + } + + self.generate_contexts()?; + Ok(()) + } + + pub fn generate_contexts(&mut self) -> Result<(), ApplicationDownloadError> { + let manifest = self.manifest.lock().unwrap().clone().unwrap(); + let game_id = self.id.clone(); + + let mut contexts = Vec::new(); + let base_path = Path::new(&self.stored_manifest.base_path); + create_dir_all(base_path).unwrap(); + + self.completed_contexts.clear(); + self.completed_contexts + .extend(self.stored_manifest.get_completed_contexts()); + + for (raw_path, chunk) in manifest { + let path = base_path.join(Path::new(&raw_path)); + + let container = path.parent().unwrap(); + create_dir_all(container).unwrap(); + + let file = File::create(path.clone()).unwrap(); + let mut running_offset = 0; + + for (index, length) in chunk.lengths.iter().enumerate() { + contexts.push(DropDownloadContext { + file_name: raw_path.to_string(), + version: chunk.version_name.to_string(), + offset: running_offset, + index, + game_id: game_id.to_string(), + path: path.clone(), + checksum: chunk.checksums[index].clone(), + length: *length, + permissions: chunk.permissions, + }); + running_offset += *length as u64; + } + + #[cfg(target_os = "linux")] + if running_offset > 0 { + let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); + } + } + self.contexts = contexts; + + Ok(()) + } + + pub fn run(&mut self) -> Result<(), ()> { + info!("downloading game: {}", self.id); + const DOWNLOAD_MAX_THREADS: usize = 1; + + let pool = ThreadPoolBuilder::new() + .num_threads(DOWNLOAD_MAX_THREADS) + .build() + .unwrap(); + + let completed_indexes = Arc::new(boxcar::Vec::new()); + let completed_indexes_loop_arc = completed_indexes.clone(); + + pool.scope(|scope| { + for (index, context) in self.contexts.iter().enumerate() { + let completed_indexes = completed_indexes_loop_arc.clone(); + + let progress = self.progress.get(index); // Clone arcs + let progress_handle = ProgressHandle::new(progress, self.progress.clone()); + // If we've done this one already, skip it + if self.completed_contexts.contains(&index) { + progress_handle.add(context.length); + continue; + } + + let context = context.clone(); + let control_flag = self.control_flag.clone(); // Clone arcs + + let sender = self.sender.clone(); + + scope.spawn(move |_| { + match download_game_chunk(context.clone(), control_flag, progress_handle) { + Ok(res) => { + if res { + completed_indexes.push(index); + } + } + Err(e) => { + error!("{}", e); + sender.send(DownloadManagerSignal::Error(e)).unwrap(); + } + } + }); + } + }); + + let newly_completed = completed_indexes.to_owned(); + + let completed_lock_len = { + for (item, _) in newly_completed.iter() { + self.completed_contexts.push_front(item); + } + + self.completed_contexts.len() + }; + + // If we're not out of contexts, we're not done, so we don't fire completed + if completed_lock_len != self.contexts.len() { + info!("da for {} exited without completing", self.id.clone()); + self.stored_manifest + .set_completed_contexts(&self.completed_contexts.clone().into()); + info!("Setting completed contexts"); + self.stored_manifest.write(); + info!("Wrote completed contexts"); + return Ok(()); + } + + // We've completed + self.sender + .send(DownloadManagerSignal::Completed(self.id.clone())) + .unwrap(); + + Ok(()) + } +} + +impl Downloadable for GameDownloadAgent { + + fn download(&mut self) -> Result<(), ApplicationDownloadError> { + self.download() + } + + fn progress(&self) -> Arc { + self.progress.clone() + } + + fn control_flag(&self) -> DownloadThreadControl { + self.control_flag.clone() + } + + fn metadata(&self) -> crate::download_manager::downloadable_metadata::DownloadableMetadata { + todo!() + } + + fn on_error(&self) { + todo!() + } + + fn on_complete(&self, app_handle: &AppHandle>) { + on_game_complete(id, version, install_dir, app_handle) + } +} \ No newline at end of file diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs new file mode 100644 index 0000000..4ac3554 --- /dev/null +++ b/src-tauri/src/downloads/download_commands.rs @@ -0,0 +1,62 @@ +use std::sync::Mutex; + +use crate::AppState; + +#[tauri::command] +pub fn download_game( + game_id: String, + game_version: String, + install_dir: usize, + state: tauri::State<'_, Mutex>, +) -> Result<(), String> { + state + .lock() + .unwrap() + .download_manager + .queue_download(game_id, game_version, install_dir) + .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>, game_id: String) { + state.lock().unwrap().download_manager.cancel(game_id) +} + +/* +#[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/downloads/download_logic.rs similarity index 71% rename from src-tauri/src/games/downloads/download_logic.rs rename to src-tauri/src/downloads/download_logic.rs index 70e5b7e..d064060 100644 --- a/src-tauri/src/games/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -1,16 +1,17 @@ -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 crate::downloads::manifest::DropDownloadContext; +use crate::remote::RemoteAccessError; +use crate::DB; use log::warn; use md5::{Context, Digest}; -use reqwest::blocking::{RequestBuilder, Response}; +use reqwest::blocking::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::{ @@ -18,6 +19,7 @@ use std::{ io::{self, BufWriter, Seek, SeekFrom, Write}, path::PathBuf, }; +use urlencoding::encode; pub struct DropWriter { hasher: Context, @@ -39,17 +41,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() } } @@ -60,18 +64,18 @@ impl Seek for DropWriter { } } -pub struct DropDownloadPipeline<'a, R: Read, W: Write> { +pub struct DropDownloadPipeline { pub source: R, pub destination: DropWriter, - pub control_flag: &'a DownloadThreadControl, + pub control_flag: DownloadThreadControl, pub progress: ProgressHandle, pub size: usize, } -impl<'a> DropDownloadPipeline<'a, Response, File> { +impl DropDownloadPipeline { fn new( source: Response, destination: DropWriter, - control_flag: &'a DownloadThreadControl, + control_flag: DownloadThreadControl, progress: ProgressHandle, size: usize, ) -> Self { @@ -116,10 +120,9 @@ impl<'a> DropDownloadPipeline<'a, Response, File> { } pub fn download_game_chunk( - ctx: &DropDownloadContext, - control_flag: &DownloadThreadControl, + ctx: DropDownloadContext, + control_flag: DownloadThreadControl, progress: ProgressHandle, - request: RequestBuilder, ) -> Result { // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { @@ -127,14 +130,32 @@ pub fn download_game_chunk( return Ok(false); } - let response = request + let base_url = DB.fetch_base_url(); + + let client = reqwest::blocking::Client::new(); + let chunk_url = base_url + .join(&format!( + "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}", + // Encode the parts we don't trust + ctx.game_id, + encode(&ctx.version), + encode(&ctx.file_name), + ctx.index + )) + .unwrap(); + + let header = generate_authorization_header(); + + let response = client + .get(chunk_url) + .header("Authorization", header) .send() .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 +169,8 @@ pub fn download_game_chunk( let content_length = response.content_length(); if content_length.is_none() { - warn!("recieved 0 length content from server"); return Err(ApplicationDownloadError::Communication( - RemoteAccessError::InvalidResponse(response.json().unwrap()), + RemoteAccessError::InvalidResponse, )); } @@ -173,17 +193,19 @@ pub fn download_game_chunk( #[cfg(unix)] { let permissions = Permissions::from_mode(ctx.permissions); - set_permissions(ctx.path.clone(), permissions).unwrap(); + set_permissions(ctx.path, 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/manifest.rs b/src-tauri/src/downloads/manifest.rs similarity index 100% rename from src-tauri/src/games/downloads/manifest.rs rename to src-tauri/src/downloads/manifest.rs diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/downloads/mod.rs similarity index 74% rename from src-tauri/src/games/downloads/mod.rs rename to src-tauri/src/downloads/mod.rs index c9b3cd4..8709ca6 100644 --- a/src-tauri/src/games/downloads/mod.rs +++ b/src-tauri/src/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; diff --git a/src-tauri/src/games/downloads/stored_manifest.rs b/src-tauri/src/downloads/stored_manifest.rs similarity index 93% rename from src-tauri/src/games/downloads/stored_manifest.rs rename to src-tauri/src/downloads/stored_manifest.rs index fdc232b..bb5e6b9 100644 --- a/src-tauri/src/games/downloads/stored_manifest.rs +++ b/src-tauri/src/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/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 deleted file mode 100644 index d4fb1cd..0000000 --- a/src-tauri/src/games/downloads/download_agent.rs +++ /dev/null @@ -1,407 +0,0 @@ -use crate::auth::generate_authorization_header; -use crate::database::db::{ - borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls, - GameDownloadStatus, -}; -use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; -use crate::download_manager::download_thread_control_flag::{ - DownloadThreadControl, DownloadThreadControlFlag, -}; -use crate::download_manager::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::DB; -use log::{debug, error, info}; -use rayon::ThreadPoolBuilder; -use slice_deque::SliceDeque; -use std::fs::{create_dir_all, File}; -use std::path::Path; -use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex}; -use std::time::Instant; -use tauri::{AppHandle, Emitter}; -use urlencoding::encode; - -#[cfg(target_os = "linux")] -use rustix::fs::{fallocate, FallocateFlags}; - -use super::download_logic::download_game_chunk; -use super::stored_manifest::StoredManifest; - -pub struct GameDownloadAgent { - pub id: String, - pub version: String, - pub control_flag: DownloadThreadControl, - contexts: Mutex>, - completed_contexts: Mutex>, - pub manifest: Mutex>, - pub progress: Arc, - sender: Sender, - pub stored_manifest: StoredManifest, - status: Mutex, -} - -impl GameDownloadAgent { - pub fn new( - id: String, - version: String, - target_download_dir: usize, - sender: Sender, - ) -> Self { - // Don't run by default - let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); - - let db_lock = borrow_db_checked(); - let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); - drop(db_lock); - - let base_dir_path = Path::new(&base_dir); - let data_base_dir_path = base_dir_path.join(id.clone()); - - let stored_manifest = - StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone()); - - Self { - id, - version, - control_flag, - manifest: Mutex::new(None), - contexts: Mutex::new(Vec::new()), - completed_contexts: Mutex::new(SliceDeque::new()), - progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), - sender, - stored_manifest, - status: Mutex::new(DownloadStatus::Queued), - } - } - - // Blocking - pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> { - self.ensure_manifest_exists()?; - - self.ensure_contexts()?; - - self.control_flag.set(DownloadThreadControlFlag::Go); - - Ok(()) - } - - // Blocking - pub fn download(&self, app_handle: &AppHandle) -> Result { - self.setup_download()?; - self.set_progress_object_params(); - 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); - - debug!( - "{} took {}ms to download", - self.id, - timer.elapsed().as_millis() - ); - res - } - - pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> { - if self.manifest.lock().unwrap().is_some() { - return Ok(()); - } - - self.download_manifest() - } - - fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { - 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()))?; - - if response.status() != 200 { - return Err(ApplicationDownloadError::Communication( - RemoteAccessError::ManifestDownloadFailed( - response.status(), - response.text().unwrap(), - ), - )); - } - - let manifest_download: DropManifest = response.json().unwrap(); - - if let Ok(mut manifest) = self.manifest.lock() { - *manifest = Some(manifest_download); - return Ok(()); - } - - Err(ApplicationDownloadError::Lock) - } - - fn set_progress_object_params(&self) { - // Avoid re-setting it - if self.progress.get_max() != 0 { - return; - } - - let contexts = self.contexts.lock().unwrap(); - - let length = contexts.len(); - - let chunk_count = contexts.iter().map(|chunk| chunk.length).sum(); - - self.progress.set_max(chunk_count); - self.progress.set_size(length); - self.progress.set_time_now(); - } - - pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> { - if !self.contexts.lock().unwrap().is_empty() { - return Ok(()); - } - - self.generate_contexts()?; - Ok(()) - } - - pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> { - let manifest = self.manifest.lock().unwrap().clone().unwrap(); - let game_id = self.id.clone(); - - let mut contexts = Vec::new(); - let base_path = Path::new(&self.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()); - } - - for (raw_path, chunk) in manifest { - let path = base_path.join(Path::new(&raw_path)); - - let container = path.parent().unwrap(); - create_dir_all(container).unwrap(); - - let file = File::create(path.clone()).unwrap(); - let mut running_offset = 0; - - for (index, length) in chunk.lengths.iter().enumerate() { - contexts.push(DropDownloadContext { - file_name: raw_path.to_string(), - version: chunk.version_name.to_string(), - offset: running_offset, - index, - game_id: game_id.to_string(), - path: path.clone(), - checksum: chunk.checksums[index].clone(), - length: *length, - permissions: chunk.permissions, - }); - running_offset += *length as u64; - } - - #[cfg(target_os = "linux")] - if running_offset > 0 { - let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); - } - } - *self.contexts.lock().unwrap() = contexts; - - Ok(()) - } - - // TODO: Change return value on Err - pub fn run(&self) -> Result { - let max_download_threads = borrow_db_checked().settings.max_download_threads; - - debug!( - "downloading game: {} with {} threads", - self.id, max_download_threads - ); - let pool = ThreadPoolBuilder::new() - .num_threads(max_download_threads) - .build() - .unwrap(); - - let completed_indexes = Arc::new(boxcar::Vec::new()); - let completed_indexes_loop_arc = completed_indexes.clone(); - - let contexts = self.contexts.lock().unwrap(); - pool.scope(|scope| { - let client = &reqwest::blocking::Client::new(); - for (index, context) in contexts.iter().enumerate() { - let client = client.clone(); - let completed_indexes = completed_indexes_loop_arc.clone(); - - let progress = self.progress.get(index); - let progress_handle = ProgressHandle::new(progress, self.progress.clone()); - - // If we've done this one already, skip it - if self.completed_contexts.lock().unwrap().contains(&index) { - progress_handle.skip(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; - }, - }; - - scope.spawn(move |_| { - match download_game_chunk(context, &self.control_flag, progress_handle, request) - { - Ok(res) => { - if res { - completed_indexes.push(index); - } - } - Err(e) => { - error!("{}", e); - sender.send(DownloadManagerSignal::Error(e)).unwrap(); - } - } - }); - } - }); - - 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); - } - - completed_contexts_lock.len() - }; - - // 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(), - ); - self.stored_manifest - .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice()); - self.stored_manifest.write(); - return Ok(false); - } - - // We've completed - self.sender - .send(DownloadManagerSignal::Completed(self.metadata())) - .unwrap(); - - Ok(true) - } -} - -impl Downloadable for GameDownloadAgent { - fn download(&self, app_handle: &AppHandle) -> Result { - *self.status.lock().unwrap() = DownloadStatus::Downloading; - self.download(app_handle) - } - - fn progress(&self) -> Arc { - self.progress.clone() - } - - fn control_flag(&self) -> DownloadThreadControl { - self.control_flag.clone() - } - - fn metadata(&self) -> DownloadableMetadata { - DownloadableMetadata { - id: self.id.clone(), - version: Some(self.version.clone()), - download_type: DownloadType::Game, - } - } - - fn on_initialised(&self, _app_handle: &tauri::AppHandle) { - *self.status.lock().unwrap() = DownloadStatus::Queued; - } - - fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) { - *self.status.lock().unwrap() = DownloadStatus::Error; - app_handle - .emit("download_error", error.to_string()) - .unwrap(); - - error!("error while managing download: {}", error); - - 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(); - } - - // TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly - fn on_incomplete(&self, app_handle: &tauri::AppHandle) { - let meta = self.metadata(); - *self.status.lock().unwrap() = DownloadStatus::Queued; - app_handle - .emit( - &format!("update_game/{}", meta.id), - GameUpdateEvent { - game_id: meta.id.clone(), - status: (Some(GameDownloadStatus::Remote {}), None), - }, - ) - .unwrap(); - } - - fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {} - - fn status(&self) -> DownloadStatus { - self.status.lock().unwrap().clone() - } -} diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs deleted file mode 100644 index b2f2fe2..0000000 --- a/src-tauri/src/games/library.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::fs::remove_dir_all; -use std::sync::Mutex; -use std::thread::spawn; - -use log::{debug, error, warn}; -use serde::{Deserialize, Serialize}; -use tauri::Emitter; -use tauri::{AppHandle, Manager}; - -use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion}; -use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus}; -use crate::download_manager::download_manager::DownloadStatus; -use crate::download_manager::downloadable_metadata::DownloadableMetadata; -use crate::error::remote_access_error::RemoteAccessError; -use crate::games::state::{GameStatusManager, GameStatusWithTransient}; -use crate::remote::auth::generate_authorization_header; -use crate::remote::requests::make_request; -use crate::AppState; - -#[derive(serde::Serialize)] -pub struct FetchGameStruct { - game: Game, - status: GameStatusWithTransient, -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Game { - id: String, - m_name: String, - m_short_description: String, - m_description: String, - // mDevelopers - // mPublishers - m_icon_id: String, - m_banner_id: String, - m_cover_id: String, - m_image_library: Vec, -} -#[derive(serde::Serialize, Clone)] -pub struct GameUpdateEvent { - pub game_id: String, - pub status: ( - Option, - Option, - ), -} - -#[derive(Serialize, Clone)] -pub struct QueueUpdateEventQueueData { - pub meta: DownloadableMetadata, - pub status: DownloadStatus, - pub progress: f64, - pub current: usize, - pub max: usize, -} - -#[derive(serde::Serialize, Clone)] -pub struct QueueUpdateEvent { - pub queue: Vec, -} - -#[derive(serde::Serialize, Clone)] -pub struct StatsUpdateEvent { - pub speed: usize, - pub time: usize, -} - -pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { - 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()?; - - if response.status() != 200 { - let err = response.json().unwrap(); - warn!("{:?}", err); - return Err(RemoteAccessError::InvalidResponse(err)); - } - - let games: Vec = response.json()?; - - let state = app.state::>(); - let mut handle = state.lock().unwrap(); - - let mut db_handle = borrow_db_mut_checked(); - - for game in games.iter() { - handle.games.insert(game.id.clone(), game.clone()); - if !db_handle.applications.game_statuses.contains_key(&game.id) { - db_handle - .applications - .game_statuses - .insert(game.id.clone(), GameDownloadStatus::Remote {}); - } - } - - drop(handle); - - Ok(games) -} - -pub fn fetch_game_logic( - id: String, - app: tauri::AppHandle, -) -> Result { - let state = app.state::>(); - let mut state_handle = state.lock().unwrap(); - - let game = state_handle.games.get(&id); - if let Some(game) = game { - let status = GameStatusManager::fetch_state(&id); - - let data = FetchGameStruct { - game: game.clone(), - status, - }; - - return Ok(data); - } - let client = reqwest::blocking::Client::new(); - let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| { - r.header("Authorization", generate_authorization_header()) - })? - .send()?; - - if response.status() == 404 { - return Err(RemoteAccessError::GameNotFound); - } - if response.status() != 200 { - let err = response.json().unwrap(); - warn!("{:?}", err); - return Err(RemoteAccessError::InvalidResponse(err)); - } - - let game: Game = response.json()?; - state_handle.games.insert(id.clone(), game.clone()); - - let mut db_handle = borrow_db_mut_checked(); - - db_handle - .applications - .game_statuses - .entry(id.clone()) - .or_insert(GameDownloadStatus::Remote {}); - drop(db_handle); - - let status = GameStatusManager::fetch_state(&id); - - let data = FetchGameStruct { - game: game.clone(), - status, - }; - - Ok(data) -} - -pub fn fetch_game_verion_options_logic( - game_id: String, - state: tauri::State<'_, Mutex>, -) -> Result, RemoteAccessError> { - let client = reqwest::blocking::Client::new(); - - 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)); - } - - let data: Vec = response.json()?; - - let state_lock = state.lock().unwrap(); - let process_manager_lock = state_lock.process_manager.lock().unwrap(); - let data: Vec = data - .into_iter() - .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) - .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(); - db_handle - .applications - .transient_statuses - .entry(meta.clone()) - .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {}); - - push_game_update( - app_handle, - &meta.id, - (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"); - return; - } - let previous_state = previous_state.unwrap(); - if let Some((_, install_dir)) = match previous_state { - GameDownloadStatus::Installed { - version_name, - install_dir, - } => Some((version_name, install_dir)), - GameDownloadStatus::SetupRequired { - version_name, - install_dir, - } => Some((version_name, install_dir)), - _ => None, - } { - db_handle - .applications - .transient_statuses - .entry(meta.clone()) - .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {}); - drop(db_handle); - - let app_handle = app_handle.clone(); - spawn(move || match remove_dir_all(install_dir) { - Err(e) => { - error!("{}", e); - } - Ok(_) => { - let mut db_handle = borrow_db_mut_checked(); - db_handle.applications.transient_statuses.remove(&meta); - db_handle - .applications - .game_statuses - .entry(meta.id.clone()) - .and_modify(|e| *e = GameDownloadStatus::Remote {}); - drop(db_handle); - save_db(); - - debug!("uninstalled game id {}", &meta.id); - - push_game_update( - &app_handle, - &meta.id, - (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 on_game_complete( - meta: &DownloadableMetadata, - install_dir: String, - app_handle: &AppHandle, -) -> Result<(), RemoteAccessError> { - // Fetch game version information from remote - if meta.version.is_none() { - return Err(RemoteAccessError::GameNotFound); - } - - 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 data: GameVersion = response.json()?; - - let mut handle = borrow_db_mut_checked(); - handle - .applications - .game_versions - .entry(meta.id.clone()) - .or_default() - .insert(meta.version.clone().unwrap(), data.clone()); - handle - .applications - .installed_game_version - .insert(meta.id.clone(), meta.clone()); - - drop(handle); - save_db(); - - let status = if data.setup_command.is_empty() { - GameDownloadStatus::Installed { - version_name: meta.version.clone().unwrap(), - install_dir, - } - } else { - GameDownloadStatus::SetupRequired { - version_name: meta.version.clone().unwrap(), - install_dir, - } - }; - - let mut db_handle = borrow_db_mut_checked(); - db_handle - .applications - .game_statuses - .insert(meta.id.clone(), status.clone()); - drop(db_handle); - save_db(); - app_handle - .emit( - &format!("update_game/{}", meta.id), - GameUpdateEvent { - game_id: meta.id.clone(), - status: (Some(status), None), - }, - ) - .unwrap(); - - Ok(()) -} - -pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameStatusWithTransient) { - app_handle - .emit( - &format!("update_game/{}", game_id), - GameUpdateEvent { - game_id: game_id.clone(), - status, - }, - ) - .unwrap(); -} diff --git a/src-tauri/src/games/mod.rs b/src-tauri/src/games/mod.rs deleted file mode 100644 index 65c5c6b..0000000 --- a/src-tauri/src/games/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod commands; -pub mod downloads; -pub mod library; -pub mod state; diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs deleted file mode 100644 index 19b1769..0000000 --- a/src-tauri/src/games/state.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus}; - -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 online_state = match db_lock.applications.installed_game_version.get(game_id) { - Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(), - None => None, - }; - let offline_state = db_lock.applications.game_statuses.get(game_id).cloned(); - drop(db_lock); - - if online_state.is_some() { - return (None, online_state); - } - - if offline_state.is_some() { - return (offline_state, None); - } - - (None, None) - } -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1e374eb..2875944 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,54 +1,44 @@ -mod database; -mod games; +mod auth; +mod db; +mod downloads; +mod library; -mod autostart; mod cleanup; -mod commands; -mod download_manager; -mod error; mod process; mod remote; +mod state; +mod tools; +pub mod download_manager; +#[cfg(test)] +mod tests; -use crate::database::db::DatabaseImpls; -use autostart::{get_autostart_enabled, toggle_autostart}; +use crate::db::DatabaseImpls; +use auth::{auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake, retry_connect}; 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, ApplicationStatus, + DATA_ROOT_DIR, }; 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 downloads::download_commands::*; use http::Response; use http::{header::*, response::Builder as ResponseBuilder}; +use 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 std::path::Path; -use std::str::FromStr; use std::sync::Arc; use std::{ collections::HashMap, @@ -58,7 +48,6 @@ use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::tray::TrayIconBuilder; use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; -use tauri_plugin_dialog::DialogExt; #[derive(Clone, Copy, Serialize)] pub enum AppStatus { @@ -91,25 +80,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 +111,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 +120,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 +131,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(); + let statuses = db_handle.applications.statuses.clone(); drop(db_handle); for (game_id, status) in statuses.into_iter() { match status { - database::db::GameDownloadStatus::Remote {} => {} - database::db::GameDownloadStatus::SetupRequired { + db::ApplicationStatus::Remote {} => {} + db::ApplicationStatus::SetupRequired { version_name: _, install_dir, } => { @@ -161,7 +155,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { missing_games.push(game_id); } } - database::db::GameDownloadStatus::Installed { + db::ApplicationStatus::Installed { version_name: _, install_dir, } => { @@ -175,23 +169,16 @@ 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 - .game_statuses - .entry(game_id) - .and_modify(|v| *v = GameDownloadStatus::Remote {}); + .statuses + .entry(game_id.to_string()) + .and_modify(|v| *v = ApplicationStatus::Remote {}); } - drop(db_handle); - - debug!("finished setup!"); - - // Sync autostart state - if let Err(e) = autostart::sync_autostart_on_startup(&handle) { - warn!("failed to sync autostart state: {}", e); - } + info!("finished setup!"); AppState { status: app_status, @@ -199,6 +186,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { games, download_manager, process_manager, + compat_manager, } } @@ -224,15 +212,10 @@ pub fn run() { // Core utils fetch_state, quit, - fetch_system_data, - // User utils - update_settings, - fetch_settings, // Auth auth_initiate, retry_connect, manual_recieve_handshake, - sign_out, // Remote use_remote, gen_drop_url, @@ -246,34 +229,28 @@ 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 launch_game, - kill_game, - toggle_autostart, - get_autostart_enabled, + kill_game ]) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_autostart::init( - tauri_plugin_autostart::MacosLauncher::LaunchAgent, - Some(vec!["--minimize"]), - )) .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 +270,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 +300,36 @@ 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(); - if let Some(original) = db_handle.prev_database.take() { - 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(), - ) - .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 +358,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/library.rs b/src-tauri/src/library.rs new file mode 100644 index 0000000..9795547 --- /dev/null +++ b/src-tauri/src/library.rs @@ -0,0 +1,333 @@ +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; +use tauri::Emitter; +use tauri::{AppHandle, Manager}; +use urlencoding::encode; + +use crate::db::DatabaseImpls; +use crate::db::ApplicationVersion; +use crate::db::ApplicationStatus; +use crate::download_manager::download_manager::{DownloadManagerStatus, DownloadStatus}; +use crate::process::process_manager::Platform; +use crate::remote::RemoteAccessError; +use crate::state::{DownloadStatusManager, ApplicationStatusWithTransient}; +use crate::{auth::generate_authorization_header, AppState, DB}; + +#[derive(serde::Serialize)] +pub struct FetchGameStruct { + game: Game, + status: ApplicationStatusWithTransient, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Game { + id: String, + m_name: String, + m_short_description: String, + m_description: String, + // mDevelopers + // mPublishers + m_icon_id: String, + m_banner_id: String, + m_cover_id: String, + m_image_library: Vec, +} +#[derive(serde::Serialize, Clone)] +pub struct GameUpdateEvent { + pub game_id: String, + pub status: ApplicationStatusWithTransient, +} + +#[derive(Serialize, Clone)] +pub struct QueueUpdateEventQueueData { + pub id: String, + pub status: DownloadStatus, + pub progress: f64, +} + +#[derive(serde::Serialize, Clone)] +pub struct QueueUpdateEvent { + pub queue: Vec, + pub status: DownloadManagerStatus, +} + +#[derive(serde::Serialize, Clone)] +pub struct StatsUpdateEvent { + pub speed: usize, + pub time: usize, +} + +// Game version with some fields missing and size information +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GameVersionOption { + version_index: usize, + version_name: String, + platform: Platform, + setup_command: String, + launch_command: String, + delta: bool, + umu_id_override: Option, + // total_size: usize, +} + +fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { + let base_url = DB.fetch_base_url(); + let library_url = base_url.join("/api/v1/client/user/library")?; + + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(library_url.to_string()) + .header("Authorization", header) + .send()?; + + if response.status() != 200 { + return Err(response.status().as_u16().into()); + } + + let games: Vec = response.json::>()?; + + let state = app.state::>(); + let mut handle = state.lock().unwrap(); + + let mut db_handle = DB.borrow_data_mut().unwrap(); + + for game in games.iter() { + handle.games.insert(game.id.clone(), game.clone()); + if !db_handle.applications.statuses.contains_key(&game.id) { + db_handle + .applications + .statuses + .insert(game.id.clone(), ApplicationStatus::Remote {}); + } + } + + drop(handle); + + Ok(games) +} + +#[tauri::command] +pub fn fetch_library(app: AppHandle) -> Result, String> { + fetch_library_logic(app).map_err(|e| e.to_string()) +} + +fn fetch_game_logic( + id: String, + app: tauri::AppHandle, +) -> Result { + let state = app.state::>(); + let mut state_handle = state.lock().unwrap(); + + let game = state_handle.games.get(&id); + if let Some(game) = game { + let status = DownloadStatusManager::fetch_state(&id); + + let data = FetchGameStruct { + game: game.clone(), + status, + }; + + return Ok(data); + } + + let base_url = DB.fetch_base_url(); + + let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?; + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; + + if response.status() == 404 { + return Err(RemoteAccessError::GameNotFound); + } + if response.status() != 200 { + return Err(RemoteAccessError::InvalidCodeError( + response.status().into(), + )); + } + + let game = response.json::()?; + state_handle.games.insert(id.clone(), game.clone()); + + let mut db_handle = DB.borrow_data_mut().unwrap(); + + db_handle + .applications + .statuses + .entry(id.clone()) + .or_insert(ApplicationStatus::Remote {}); + drop(db_handle); + + let status = DownloadStatusManager::fetch_state(&id); + + let data = FetchGameStruct { + game: game.clone(), + status, + }; + + Ok(data) +} + +#[tauri::command] +pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result { + let result = fetch_game_logic(id, app); + + if result.is_err() { + return Err(result.err().unwrap().to_string()); + } + + Ok(result.unwrap()) +} + +#[tauri::command] +pub fn fetch_game_status(id: String) -> Result { + let status = DownloadStatusManager::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 = data + .into_iter() + .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) + .collect::>(); + drop(process_manager_lock); + drop(state_lock); + + Ok(data) +} + +#[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()) +} + +#[tauri::command] +pub fn uninstall_game( + game_id: String, + state: tauri::State<'_, Mutex>, +) -> Result<(), String> { + let state_lock = state.lock().unwrap(); + state_lock.download_manager.uninstall_application(game_id); + drop(state_lock); + + Ok(()) +} + +pub fn push_application_update(app_handle: &AppHandle, id: String, status: ApplicationStatusWithTransient) { + app_handle + .emit( + &format!("update_game/{}", id), + GameUpdateEvent { + game_id: id, + status, + }, + ) + .unwrap(); +} + +pub fn on_game_complete( + game_id: String, + version_name: String, + install_dir: String, + app_handle: &AppHandle, +) -> Result<(), RemoteAccessError> { + // Fetch game version information from remote + let base_url = DB.fetch_base_url(); + + let endpoint = base_url.join( + format!( + "/api/v1/client/metadata/version?id={}&version={}", + game_id, + encode(&version_name) + ) + .as_str(), + )?; + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; + + let data = response.json::()?; + + let mut handle = DB.borrow_data_mut().unwrap(); + handle + .applications + .versions + .entry(game_id.clone()) + .or_default() + .insert(version_name.clone(), data.clone()); + drop(handle); + DB.save().unwrap(); + + let status = if data.setup_command.is_empty() { + ApplicationStatus::Installed { + version_name, + install_dir, + } + } else { + ApplicationStatus::SetupRequired { + version_name, + install_dir, + } + }; + + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle + .applications + .statuses + .insert(game_id.clone(), status.clone()); + drop(db_handle); + DB.save().unwrap(); + app_handle + .emit( + &format!("update_game/{}", game_id), + GameUpdateEvent { + game_id, + status: (Some(status), None), + }, + ) + .unwrap(); + + Ok(()) +} 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/commands.rs b/src-tauri/src/process/process_commands.rs similarity index 51% rename from src-tauri/src/process/commands.rs rename to src-tauri/src/process/process_commands.rs index 8a213a4..74d7dfd 100644 --- a/src-tauri/src/process/commands.rs +++ b/src-tauri/src/process/process_commands.rs @@ -1,25 +1,16 @@ use std::sync::Mutex; -use crate::{error::process_error::ProcessError, AppState}; +use crate::AppState; #[tauri::command] pub fn launch_game( - id: String, + game_id: String, state: tauri::State<'_, Mutex>, -) -> Result<(), ProcessError> { +) -> Result<(), String> { 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), - }; + process_manager_lock.launch_process(game_id)?; drop(process_manager_lock); drop(state_lock); @@ -31,10 +22,8 @@ pub fn launch_game( pub fn kill_game( game_id: String, state: tauri::State<'_, Mutex>, -) -> Result<(), ProcessError> { +) -> Result<(), String> { 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) -} + process_manager_lock.kill_game(game_id).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..b6da0f0 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -1,26 +1,23 @@ 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}, + db::{ApplicationStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, + library::push_application_update, + state::DownloadStatusManager, AppState, DB, }; @@ -66,8 +63,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); @@ -81,17 +83,17 @@ impl ProcessManager<'_> { (absolute_exe, Vec::new()) } pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> { - match self.processes.get(&game_id) { + return match self.processes.get(&game_id) { Some(child) => { child.kill()?; child.wait()?; Ok(()) - } + }, None => Err(io::Error::new( io::ErrorKind::NotFound, "Game ID not running", )), - } + }; } fn on_process_finish(&mut self, game_id: String, result: Result) { @@ -100,31 +102,25 @@ impl ProcessManager<'_> { return; } - debug!("process for {:?} exited with {:?}", &game_id, result); + info!("process for {} exited with {:?}", game_id, result); self.processes.remove(&game_id); - let mut db_handle = borrow_db_mut_checked(); - let meta = db_handle - .applications - .installed_game_version - .get(&game_id) - .cloned() - .unwrap(); - db_handle.applications.transient_statuses.remove(&meta); + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle.applications.transient_statuses.remove(&game_id); - let current_state = db_handle.applications.game_statuses.get(&game_id).cloned(); + let current_state = db_handle.applications.statuses.get(&game_id).cloned(); if let Some(saved_state) = current_state { - if let GameDownloadStatus::SetupRequired { + if let ApplicationStatus::SetupRequired { version_name, install_dir, } = saved_state { if let Ok(exit_code) = result { if exit_code.success() { - db_handle.applications.game_statuses.insert( + db_handle.applications.statuses.insert( game_id.clone(), - GameDownloadStatus::Installed { + ApplicationStatus::Installed { version_name: version_name.to_string(), install_dir: install_dir.to_string(), }, @@ -135,9 +131,9 @@ impl ProcessManager<'_> { } drop(db_handle); - let status = GameStatusManager::fetch_state(&game_id); + let status = DownloadStatusManager::fetch_state(&game_id); - push_game_update(&self.app_handle, &game_id, status); + push_application_update(&self.app_handle, game_id.clone(), status); // TODO better management } @@ -149,86 +145,57 @@ impl ProcessManager<'_> { .contains_key(&(current.clone(), platform.clone()))) } - pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> { + pub fn launch_process(&mut self, game_id: String) -> Result<(), String> { if self.processes.contains_key(&game_id) { - return Err(ProcessError::AlreadyRunning); + 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(); let game_status = db_lock .applications - .game_statuses + .statuses .get(&game_id) - .ok_or(ProcessError::NotInstalled)?; + .ok_or("Game not installed")?; - let (version_name, install_dir) = match game_status { - GameDownloadStatus::Installed { + let status_metadata: Option<(&String, &String)> = match game_status { + ApplicationStatus::Installed { version_name, install_dir, - } => (version_name, install_dir), - GameDownloadStatus::SetupRequired { + } => Some((version_name, install_dir)), + ApplicationStatus::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 + .versions .get(&game_id) - .ok_or(ProcessError::InvalidID)? + .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 { - GameDownloadStatus::Installed { + let raw_command: String = match game_status { + ApplicationStatus::Installed { version_name: _, install_dir: _, - } => { - command.extend([game_version.launch_command.clone()]); - command.extend(game_version.launch_args.clone()); - }, - GameDownloadStatus::SetupRequired { + } => game_version.launch_command.clone(), + ApplicationStatus::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 +211,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", game_id, current_time.timestamp())), + ) + .map_err(|v| v.to_string())?; let error_file = OpenOptions::new() .write(true) @@ -258,12 +223,11 @@ impl ProcessManager<'_> { .read(true) .create(true) .open(self.log_output_dir.join(format!( - "{}-{}-{}-error.log", - &game_id, - &version, + "{}-{}-error.log", + game_id, 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,36 +235,36 @@ 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( + &game_id, + version_name, + 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 .transient_statuses - .insert(meta.clone(), ApplicationTransientStatus::Running {}); + .insert(game_id.clone(), ApplicationTransientStatus::Running {}); - push_game_update( + push_application_update( &self.app_handle, - &meta.id, + game_id.clone(), (None, Some(ApplicationTransientStatus::Running {})), ); let wait_thread_handle = launch_process_handle.clone(); let wait_thread_apphandle = self.app_handle.clone(); - let wait_thread_game_id = meta.clone(); + let wait_thread_game_id = game_id.clone(); spawn(move || { let result: Result = launch_process_handle.wait(); @@ -309,7 +273,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 +281,10 @@ impl ProcessManager<'_> { drop(app_state_handle); }); - self.processes.insert(meta.id, wait_thread_handle); + self.processes.insert(game_id, wait_thread_handle); + + info!("finished spawning process"); + Ok(()) } } @@ -331,32 +298,35 @@ pub enum Platform { pub trait ProcessHandler: Send + 'static { fn launch_process( &self, - meta: &DownloadableMetadata, - launch_command: String, - game_version: &GameVersion, - current_dir: &str, + game_id: &String, + version_name: &String, + 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, + game_id: &String, + version_name: &String, + 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 +335,19 @@ 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()) + game_id: &String, + version_name: &String, + command: String, + args: Vec, + current_dir: &String, + log_file: File, + error_file: File, + ) -> Result { + UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command) + .game_id(game_id.into()) + .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..1559155 --- /dev/null +++ b/src-tauri/src/remote.rs @@ -0,0 +1,153 @@ +use std::{ + error::Error, + fmt::{Display, Formatter}, + sync::{Arc, Mutex}, +}; + +use http::StatusCode; +use log::{info, warn}; +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, + InvalidRedirect, + ManifestDownloadFailed(StatusCode, String), + OutOfSync, +} + +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 => write!(f, "Server returned an invalid response"), + 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."), + } + } +} + +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)] +#[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/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..dd95868 --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,27 @@ + +use crate::{ + db::{ApplicationStatus, ApplicationTransientStatus}, + DB, +}; + +pub type ApplicationStatusWithTransient = (Option, Option); +pub struct DownloadStatusManager {} + +impl DownloadStatusManager { + pub fn fetch_state(id: &String) -> ApplicationStatusWithTransient { + let db_lock = DB.borrow_data().unwrap(); + let offline_state = db_lock.applications.statuses.get(id).cloned(); + let online_state = db_lock.applications.transient_statuses.get(id).cloned(); + drop(db_lock); + + if online_state.is_some() { + return (None, online_state); + } + + if offline_state.is_some() { + return (offline_state, None); + } + + (None, None) + } +} 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..f3a0de0 --- /dev/null +++ b/src-tauri/src/tools/tool.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; + +use crate::download_manager::{download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, progress_object::ProgressObject}; + +pub struct ToolDownloadAgent { + id: String, + version: String, + location: String, + control_flag: DownloadThreadControl, + progress: Arc, +} +impl Downloadable for ToolDownloadAgent { + fn download(&mut self) -> Result<(), crate::download_manager::application_download_error::ApplicationDownloadError> { + todo!() + } + + fn progress(&self) -> Arc { + todo!() + } + + fn control_flag(&self) -> DownloadThreadControl { + todo!() + } + + fn metadata(&self) -> crate::download_manager::downloadable_metadata::DownloadableMetadata { + todo!() + } + + fn on_error(&self) { + todo!() + } + + fn on_complete(&self) { + 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..63e2f59 100644 --- a/types.ts +++ b/types.ts @@ -59,21 +59,3 @@ export type GameStatus = { type: GameStatusEnum; version_name?: string; }; - -export enum DownloadableType { - Game = "Game", - Tool = "Tool", - DLC = "DLC", - Mod = "Mod" -} - -export type DownloadableMetadata = { - id: string, - version: string, - downloadType: DownloadableType -} - -export type Settings = { - autostart: boolean, - maxDownloadThreads: number, -} \ No newline at end of file