+
-
+
-
- {{ nav.label }}
-
+
-
+
diff --git a/pages/library/index.vue b/pages/library/index.vue
index dd245b6..61c7765 100644
--- a/pages/library/index.vue
+++ b/pages/library/index.vue
@@ -1,9 +1,3 @@
-
-
- Library Failed to update
-
-
+
\ No newline at end of file
diff --git a/pages/queue.vue b/pages/queue.vue
index 8d46bea..907ab2d 100644
--- a/pages/queue.vue
+++ b/pages/queue.vue
@@ -1,46 +1,30 @@
-
-
-
{{ formatKilobytes(stats.speed) }}/s
-
{{ formatTime(stats.time) }} left
+
+
+ {{ formatKilobytes(stats.speed) }}
+ {{ formatTime(stats.time) }} left
+
-
+
-
+
-
+
- {{ games[element.meta.id].game.mName }}
+ {{ games[element.id].game.mName }}
- {{ games[element.meta.id].game.mShortDescription }}
+ {{ games[element.id].game.mShortDescription }}
@@ -49,74 +33,53 @@
{{ element.status }}
-
-
+
-
{{
- formatKilobytes(element.current / 1000)
- }}
- /
- {{ formatKilobytes(element.max / 1000) }}
- cancelGame(element.meta)" class="group">
-
+ cancelGame(element.id)" class="group">
+
Loading...
-
diff --git a/pages/settings/compatibility.vue b/pages/settings/compatibility.vue
new file mode 100644
index 0000000..27e0f69
--- /dev/null
+++ b/pages/settings/compatibility.vue
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/pages/settings/debug.vue b/pages/settings/debug.vue
deleted file mode 100644
index 7a549ca..0000000
--- a/pages/settings/debug.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
-
-
- Debug Information
-
-
- Technical information about your Drop client installation, helpful for
- debugging.
-
-
-
-
-
-
-
- Client ID
-
-
-
- {{ clientId || "Not signed in" }}
-
-
-
-
-
-
-
- Platform
-
-
-
- {{ platformInfo }}
-
-
-
-
-
-
-
- Server URL
-
-
-
- {{ baseUrl || "Not connected" }}
-
-
-
-
-
-
-
- Data Directory
-
-
-
- {{ dataDir || "Unknown" }}
-
-
-
-
- openDataDir()"
- type="button"
- class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
- >
-
- Open Data Directory
-
- openLogFile()"
- type="button"
- class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
- >
-
- Open Log File
-
-
-
-
-
-
-
-
diff --git a/pages/settings/downloads.vue b/pages/settings/downloads.vue
index c03fa78..d7e7d53 100644
--- a/pages/settings/downloads.vue
+++ b/pages/settings/downloads.vue
@@ -59,54 +59,6 @@
-
-
- Download Settings
-
-
- Configure how Drop downloads games and other content.
-
-
-
-
- Maximum Download Threads
-
-
-
-
-
- The maximum number of concurrent download threads. Higher values may
- download faster but use more system resources. Default is 4.
-
-
-
-
-
- {{ saveState.success ? 'Saved' : 'Save Changes' }}
-
-
-
@@ -220,7 +172,6 @@ import {
} from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core";
-import { type Settings } from "~/types";
const open = ref(false);
const currentDirectory = ref(undefined);
@@ -229,14 +180,6 @@ const createDirectoryLoading = ref(false);
const dirs = ref>([]);
-const settings = await invoke("fetch_settings");
-const downloadThreads = ref(settings?.maxDownloadThreads ?? 4);
-
-const saveState = reactive({
- loading: false,
- success: false
-});
-
async function updateDirs() {
const newDirs = await invoke>("fetch_download_dir_stats");
dirs.value = newDirs;
@@ -270,7 +213,7 @@ async function submitDirectory() {
try {
error.value = undefined;
if (!currentDirectory.value)
- throw new Error("Please select a directory first");
+ throw new Error("Please select a directory first.");
createDirectoryLoading.value = true;
// Add directory
@@ -292,42 +235,4 @@ async function deleteDirectory(index: number) {
await invoke("delete_download_dir", { index });
await updateDirs();
}
-
-async function saveDownloadThreads() {
- try {
- saveState.loading = true;
- await invoke("update_settings", {
- newSettings: { maxDownloadThreads: downloadThreads.value },
- });
-
- // Show success state
- saveState.success = true;
-
- // Reset back to normal state after 2 seconds
- setTimeout(() => {
- saveState.success = false;
- }, 2000);
-
- } catch (error) {
- console.error('Failed to save settings:', error);
- } finally {
- saveState.loading = false;
- }
-}
-
-function validateNumberInput(event: KeyboardEvent) {
- // Allow only numbers and basic control keys
- if (!/^\d$/.test(event.key) &&
- !['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
- event.preventDefault();
- }
-}
-
-function validatePaste(event: ClipboardEvent) {
- // Prevent paste if content contains non-numeric characters
- const pastedData = event.clipboardData?.getData('text');
- if (pastedData && !/^\d+$/.test(pastedData)) {
- event.preventDefault();
- }
-}
diff --git a/pages/settings/index.vue b/pages/settings/index.vue
index 241c840..27e0f69 100644
--- a/pages/settings/index.vue
+++ b/pages/settings/index.vue
@@ -1,60 +1,3 @@
-
-
-
General
-
- Configure basic application settings
-
-
-
-
-
-
Start with system
-
- Drop will automatically start when you log into your computer
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
\ 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.ts b/plugins.ts
new file mode 100644
index 0000000..e65d2d4
--- /dev/null
+++ b/plugins.ts
@@ -0,0 +1 @@
+import 'tauri-plugin-gamepad-api'
\ 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..dfed7f9 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,20 +987,15 @@ 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-gamepad",
"tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
- "throttle_my_fn",
"tokio",
- "umu-wrapper-lib",
"url",
"urlencoding",
"uuid",
@@ -1090,7 +1046,7 @@ dependencies = [
"rustc_version",
"toml 0.8.2",
"vswhom",
- "winreg 0.52.0",
+ "winreg",
]
[[package]]
@@ -1114,6 +1070,26 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+[[package]]
+name = "enum-iterator"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635"
+dependencies = [
+ "enum-iterator-derive",
+]
+
+[[package]]
+name = "enum-iterator-derive"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.91",
+]
+
[[package]]
name = "enumflags2"
version = "0.7.10"
@@ -1520,6 +1496,40 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
+[[package]]
+name = "gilrs"
+version = "0.11.0"
+source = "git+https://github.com/eugenehp/gilrs.git#cbcff6a4cd722a132bf2ca3c55fa9147775c9fe9"
+dependencies = [
+ "fnv",
+ "gilrs-core",
+ "log",
+ "serde",
+ "uuid",
+ "vec_map",
+]
+
+[[package]]
+name = "gilrs-core"
+version = "0.6.0"
+source = "git+https://github.com/eugenehp/gilrs.git#cbcff6a4cd722a132bf2ca3c55fa9147775c9fe9"
+dependencies = [
+ "core-foundation 0.10.0",
+ "inotify",
+ "io-kit-sys",
+ "js-sys",
+ "libc",
+ "libudev-sys",
+ "log",
+ "nix 0.29.0",
+ "serde",
+ "uuid",
+ "vec_map",
+ "wasm-bindgen",
+ "web-sys",
+ "windows",
+]
+
[[package]]
name = "gimli"
version = "0.31.1"
@@ -2085,6 +2095,26 @@ dependencies = [
"cfb",
]
+[[package]]
+name = "inotify"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
+dependencies = [
+ "bitflags 2.6.0",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "instant"
version = "0.1.13"
@@ -2094,6 +2124,16 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "io-kit-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
+dependencies = [
+ "core-foundation-sys",
+ "mach2",
+]
+
[[package]]
name = "ipnet"
version = "2.10.1"
@@ -2298,6 +2338,16 @@ dependencies = [
"libc",
]
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@@ -2351,7 +2401,7 @@ dependencies = [
"log",
"log-mdc",
"once_cell",
- "parking_lot 0.12.3",
+ "parking_lot",
"rand 0.8.5",
"serde",
"serde-value",
@@ -2370,10 +2420,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
-name = "mach"
-version = "0.3.2"
+name = "mach2"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
dependencies = [
"libc",
]
@@ -2540,6 +2590,18 @@ dependencies = [
"memoffset",
]
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -2977,17 +3039,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 +3046,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 +3057,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall 0.5.8",
+ "redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
@@ -3455,15 +3492,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 +3938,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 +3956,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 +4079,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 +4111,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 +4169,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 +4322,7 @@ dependencies = [
"ndk-sys",
"objc",
"once_cell",
- "parking_lot 0.12.3",
+ "parking_lot",
"raw-window-handle",
"scopeguard",
"tao-macros",
@@ -4342,7 +4359,7 @@ checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c"
dependencies = [
"anyhow",
"bytes",
- "dirs 5.0.1",
+ "dirs",
"dunce",
"embed_plist",
"futures-util",
@@ -4392,7 +4409,7 @@ checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
dependencies = [
"anyhow",
"cargo_toml",
- "dirs 5.0.1",
+ "dirs",
"glob",
"heck 0.5.0",
"json-patch",
@@ -4464,20 +4481,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"
@@ -4539,6 +4542,21 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "tauri-plugin-gamepad"
+version = "0.0.5"
+source = "git+https://github.com/quexeky/tauri-plugin-gamepad.git#92cafc0aae66a3de01c252ae2ed599b329b0c686"
+dependencies = [
+ "enum-iterator",
+ "gilrs",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.9",
+ "uuid",
+]
+
[[package]]
name = "tauri-plugin-os"
version = "2.2.0"
@@ -4766,18 +4784,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 +5000,7 @@ checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b"
dependencies = [
"core-graphics",
"crossbeam-channel",
- "dirs 5.0.1",
+ "dirs",
"libappindicator",
"muda",
"objc2",
@@ -5051,12 +5057,6 @@ dependencies = [
"winapi",
]
-[[package]]
-name = "umu-wrapper-lib"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baa36636bef667cee9eb4f497c24279182b8b9f098fd04b0b8c5d2ebc4e451f1"
-
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -5208,6 +5208,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -5900,15 +5906,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"
@@ -6052,7 +6049,7 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
- "nix",
+ "nix 0.27.1",
"ordered-stream",
"rand 0.8.5",
"serde",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f9f24c3..6df35ed 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"
@@ -42,14 +43,7 @@ md5 = "0.7.0"
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"
+tauri-plugin-gamepad = {git = "https://github.com/quexeky/tauri-plugin-gamepad.git", version = "0.0.5"}
[dependencies.tauri]
version = "2.1.1"
@@ -88,10 +82,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/capabilities/default.json b/src-tauri/capabilities/default.json
index 1b818b3..bb5ce00 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -14,6 +14,7 @@
"core:window:allow-close",
"deep-link:default",
"dialog:default",
- "os:default"
+ "os:default",
+ "gamepad:default"
]
}
\ No newline at end of file
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 63%
rename from src-tauri/src/remote/auth.rs
rename to src-tauri/src/auth.rs
index d2594de..9d6afb5 100644
--- a/src-tauri/src/remote/auth.rs
+++ b/src-tauri/src/auth.rs
@@ -1,22 +1,21 @@
-use std::{env, sync::Mutex};
+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 +38,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 +53,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 +67,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 +114,18 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?;
- 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();
}
{
@@ -152,9 +152,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 +168,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 +177,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..662d943
--- /dev/null
+++ b/src-tauri/src/db.rs
@@ -0,0 +1,210 @@
+use std::{
+ collections::HashMap,
+ fs::{self, create_dir_all},
+ path::{Path, PathBuf},
+ sync::{LazyLock, Mutex},
+};
+
+use directories::BaseDirs;
+use log::debug;
+use rustbreak::{DeSerError, DeSerializer, PathDatabase};
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use url::Url;
+
+use crate::{process::process_manager::Platform, 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 GameStatus {
+ 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 GameTransientStatus {
+ Downloading { version_name: String },
+ Uninstalling {},
+ Updating { version_name: String },
+ Running {},
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct GameVersion {
+ 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 DatabaseGames {
+ 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 games: DatabaseGames,
+}
+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(),
+ games: DatabaseGames {
+ 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.games.install_dirs.contains(&new_dir) {
+ return Err("Download directory already used".to_string());
+ }
+ lock.games.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.games.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.games.install_dirs.clone();
+ drop(lock);
+
+ Ok(directories)
+}
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_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs
deleted file mode 100644
index dd43dc2..0000000
--- a/src-tauri/src/download_manager/download_manager_builder.rs
+++ /dev/null
@@ -1,364 +0,0 @@
-use std::{
- collections::HashMap,
- sync::{
- mpsc::{channel, Receiver, Sender},
- Arc, Mutex,
- },
- thread::{spawn, JoinHandle},
-};
-
-use log::{debug, error, info, warn};
-use tauri::{AppHandle, Emitter};
-
-use crate::{
- error::application_download_error::ApplicationDownloadError,
- games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
-};
-
-use super::{
- download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
- download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
- downloadable::Downloadable,
- downloadable_metadata::DownloadableMetadata,
- progress_object::ProgressObject,
- queue::Queue,
-};
-
-pub type DownloadAgent = Arc>;
-pub type CurrentProgressObject = Arc>>>;
-
-/*
-
-Welcome to the download manager, the most overengineered, glorious piece of bullshit.
-
-The download manager takes a queue of ids and their associated
-DownloadAgents, and then, one-by-one, executes them. It provides an interface
-to interact with the currently downloading agent, and manage the queue.
-
-When the DownloadManager is initialised, it is designed to provide a reference
-which can be used to provide some instructions (the DownloadManagerInterface),
-but other than that, it runs without any sort of interruptions.
-
-It does this by opening up two data structures. Primarily is the command_receiver,
-and mpsc (multi-channel-single-producer) which allows commands to be sent from
-the Interface, and queued up for the Manager to process.
-
-These have been mapped in the DownloadManagerSignal docs.
-
-The other way to interact with the DownloadManager is via the donwload_queue,
-which is just a collection of ids which may be rearranged to suit
-whichever download queue order is required.
-
-+----------------------------------------------------------------------------+
-| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
-| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
-| WHICH HAS NOT BEEN ACCOUNTED FOR |
-+----------------------------------------------------------------------------+
-
-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
-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.
-
-Behold, my madness - quexeky
-
-*/
-
-pub struct DownloadManagerBuilder {
- download_agent_registry: HashMap,
- download_queue: Queue,
- command_receiver: Receiver,
- sender: Sender,
- progress: CurrentProgressObject,
- status: Arc>,
- app_handle: AppHandle,
-
- current_download_agent: Option, // Should be the only download agent in the map with the "Go" flag
- current_download_thread: Mutex>>,
- active_control_flag: Option,
-}
-impl DownloadManagerBuilder {
- pub fn build(app_handle: AppHandle) -> DownloadManager {
- let queue = Queue::new();
- let (command_sender, command_receiver) = channel();
- let active_progress = Arc::new(Mutex::new(None));
- let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
-
- let manager = Self {
- download_agent_registry: HashMap::new(),
- download_queue: queue.clone(),
- command_receiver,
- status: status.clone(),
- sender: command_sender.clone(),
- progress: active_progress.clone(),
- app_handle,
-
- current_download_agent: None,
- current_download_thread: Mutex::new(None),
- active_control_flag: None,
- };
-
- let terminator = spawn(|| manager.manage_queue());
-
- DownloadManager::new(terminator, queue, active_progress, command_sender)
- }
-
- fn set_status(&self, status: DownloadManagerStatus) {
- *self.status.lock().unwrap() = status;
- }
-
- fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
- self.download_queue.pop_front();
- let download_agent = self.download_agent_registry.remove(meta).unwrap();
- self.cleanup_current_download();
- download_agent
- }
-
- // CAREFUL WITH THIS FUNCTION
- // Make sure the download thread is terminated
- fn cleanup_current_download(&mut self) {
- self.active_control_flag = None;
- *self.progress.lock().unwrap() = None;
- self.current_download_agent = None;
-
- let mut download_thread_lock = self.current_download_thread.lock().unwrap();
- *download_thread_lock = None;
- drop(download_thread_lock);
- }
-
- fn stop_and_wait_current_download(&self) {
- 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() {
- Ok(signal) => signal,
- Err(_) => return Err(()),
- };
-
- match signal {
- DownloadManagerSignal::Go => {
- self.manage_go_signal();
- }
- DownloadManagerSignal::Stop => {
- self.manage_stop_signal();
- }
- DownloadManagerSignal::Completed(meta) => {
- self.manage_completed_signal(meta);
- }
- DownloadManagerSignal::Queue(download_agent) => {
- self.manage_queue_signal(download_agent);
- }
- DownloadManagerSignal::Error(e) => {
- self.manage_error_signal(e);
- }
- 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::Cancel(meta) => {
- self.manage_cancel_signal(&meta);
- }
- _ => {}
- };
- }
- }
- fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
- debug!("got signal Queue");
- let meta = download_agent.metadata();
-
- debug!("queue metadata: {:?}", meta);
-
- if self.download_queue.exists(meta.clone()) {
- warn!("download with same ID already exists");
- return;
- }
-
- 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();
- }
-
- 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());
-
- self.stop_and_wait_current_download();
- self.remove_and_cleanup_front_download(¤t_agent.metadata());
- }
- self.set_status(DownloadManagerStatus::Error(error));
- }
- fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
- debug!("got signal Cancel");
-
- 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();
-
- 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();
- }
- 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();
- }
-}
diff --git a/src-tauri/src/download_manager/downloadable.rs b/src-tauri/src/download_manager/downloadable.rs
deleted file mode 100644
index 181b329..0000000
--- a/src-tauri/src/download_manager/downloadable.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-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,
-};
-
-pub trait Downloadable: Send + Sync {
- fn download(&self, app_handle: &AppHandle) -> Result;
- fn progress(&self) -> Arc;
- fn control_flag(&self) -> DownloadThreadControl;
- fn status(&self) -> DownloadStatus;
- fn metadata(&self) -> DownloadableMetadata;
- fn on_initialised(&self, app_handle: &AppHandle);
- fn on_error(&self, app_handle: &AppHandle, error: ApplicationDownloadError);
- 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
deleted file mode 100644
index 790b669..0000000
--- a/src-tauri/src/download_manager/downloadable_metadata.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-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")]
-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,
- }
- }
-}
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
deleted file mode 100644
index 0bac198..0000000
--- a/src-tauri/src/download_manager/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-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;
diff --git a/src-tauri/src/download_manager/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs
deleted file mode 100644
index fc0907b..0000000
--- a/src-tauri/src/download_manager/progress_object.rs
+++ /dev/null
@@ -1,155 +0,0 @@
-use std::{
- sync::{
- atomic::{AtomicUsize, Ordering},
- mpsc::Sender,
- Arc, Mutex,
- },
- time::{Duration, Instant},
-};
-
-use atomic_instant_full::AtomicInstant;
-use throttle_my_fn::throttle;
-
-use super::{
- download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow,
-};
-
-#[derive(Clone)]
-pub struct ProgressObject {
- max: Arc>,
- progress_instances: Arc>>>,
- start: Arc>,
- sender: Sender,
- //last_update: Arc>,
- last_update_time: Arc,
- bytes_last_update: Arc,
- rolling: RollingProgressWindow<250>,
-}
-
-pub struct ProgressHandle {
- progress: Arc,
- progress_object: Arc,
-}
-
-impl ProgressHandle {
- pub fn new(progress: Arc, progress_object: Arc) -> Self {
- Self {
- progress,
- progress_object,
- }
- }
- pub fn set(&self, amount: usize) {
- self.progress.store(amount, Ordering::Relaxed);
- }
- 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
- }
-}
-
-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
- 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(),
- }
- }
-
- pub fn set_time_now(&self) {
- *self.start.lock().unwrap() = Instant::now();
- }
- pub fn sum(&self) -> usize {
- self.progress_instances
- .lock()
- .unwrap()
- .iter()
- .map(|instance| instance.load(Ordering::Relaxed))
- .sum()
- }
- pub fn get_max(&self) -> usize {
- *self.max.lock().unwrap()
- }
- pub fn set_max(&self, new_max: usize) {
- *self.max.lock().unwrap() = new_max;
- }
- pub fn set_size(&self, length: usize) {
- *self.progress_instances.lock().unwrap() =
- (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
- }
- pub fn get_progress(&self) -> f64 {
- self.sum() as f64 / self.get_max() as f64
- }
- 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
deleted file mode 100644
index f3e9493..0000000
--- a/src-tauri/src/download_manager/queue.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use std::{
- collections::VecDeque,
- sync::{Arc, Mutex, MutexGuard},
-};
-
-use super::downloadable_metadata::DownloadableMetadata;
-
-#[derive(Clone)]
-pub struct Queue {
- 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 {
- self.inner.lock().unwrap().clone()
- }
- pub fn edit(&self) -> MutexGuard<'_, VecDeque> {
- self.inner.lock().unwrap()
- }
- pub fn pop_front(&self) -> Option {
- self.edit().pop_front()
- }
- pub fn is_empty(&self) -> bool {
- self.inner.lock().unwrap().len() == 0
- }
- pub fn exists(&self, meta: DownloadableMetadata) -> bool {
- self.read().contains(&meta)
- }
- /// Either inserts `interface` at the specified index, or appends to
- /// the back of the deque if index is greater than the length of the deque
- pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
- if self.read().len() > index {
- self.append(interface);
- } else {
- self.edit().insert(index, interface);
- }
- }
- pub fn append(&self, interface: DownloadableMetadata) {
- self.edit().push_back(interface);
- }
- pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option {
- let mut queue = self.edit();
- let front = queue.front()?;
- if front == meta {
- return queue.pop_front();
- }
- None
- }
- pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option {
- self.read().iter().position(|data| data == meta)
- }
- pub fn move_to_index_by_meta(
- &self,
- meta: &DownloadableMetadata,
- new_index: usize,
- ) -> Result<(), ()> {
- let index = match self.get_by_meta(meta) {
- Some(index) => index,
- None => return Err(()),
- };
- let existing = match self.edit().remove(index) {
- Some(existing) => existing,
- None => return Err(()),
- };
- self.edit().insert(new_index, existing);
- Ok(())
- }
-}
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..8b070be
--- /dev/null
+++ b/src-tauri/src/downloads/download_agent.rs
@@ -0,0 +1,331 @@
+use crate::auth::generate_authorization_header;
+use crate::db::DatabaseImpls;
+use crate::downloads::manifest::{DropDownloadContext, DropManifest};
+use crate::downloads::progress_object::ProgressHandle;
+use crate::remote::RemoteAccessError;
+use crate::DB;
+use log::{debug, error, info};
+use rayon::ThreadPoolBuilder;
+use std::collections::VecDeque;
+use std::fmt::{Display, Formatter};
+use std::fs::{create_dir_all, File};
+use std::io;
+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::download_manager::DownloadManagerSignal;
+use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
+use super::progress_object::ProgressObject;
+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,
+}
+
+#[derive(Debug, Clone)]
+pub enum GameDownloadError {
+ Communication(RemoteAccessError),
+ Checksum,
+ Setup(SetupError),
+ Lock,
+ IoError(io::ErrorKind),
+ DownloadError,
+}
+
+#[derive(Debug, Clone)]
+pub enum SetupError {
+ Context,
+}
+
+impl Display for GameDownloadError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ GameDownloadError::Communication(error) => write!(f, "{}", error),
+ GameDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
+ GameDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
+ GameDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
+ GameDownloadError::IoError(error) => write!(f, "{}", error),
+ GameDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
+ }
+ }
+}
+
+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"),
+ }
+ }
+}
+
+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.games.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<(), GameDownloadError> {
+ 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<(), GameDownloadError> {
+ self.setup_download()?;
+ self.set_progress_object_params();
+ let timer = Instant::now();
+ self.run().map_err(|_| GameDownloadError::DownloadError)?;
+
+ info!(
+ "{} took {}ms to download",
+ self.id,
+ timer.elapsed().as_millis()
+ );
+ Ok(())
+ }
+
+ pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> {
+ if self.manifest.lock().unwrap().is_some() {
+ return Ok(());
+ }
+
+ self.download_manifest()
+ }
+
+ fn download_manifest(&self) -> Result<(), GameDownloadError> {
+ 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(GameDownloadError::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(GameDownloadError::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<(), GameDownloadError> {
+ if !self.contexts.is_empty() {
+ return Ok(());
+ }
+
+ self.generate_contexts()?;
+ Ok(())
+ }
+
+ pub fn generate_contexts(&mut self) -> Result<(), GameDownloadError> {
+ 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, item_ref) 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(())
+ }
+}
diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs
new file mode 100644
index 0000000..d3ca2e7
--- /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_game(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 66%
rename from src-tauri/src/games/downloads/download_logic.rs
rename to src-tauri/src/downloads/download_logic.rs
index 70e5b7e..420d0ef 100644
--- a/src-tauri/src/games/downloads/download_logic.rs
+++ b/src-tauri/src/downloads/download_logic.rs
@@ -1,16 +1,14 @@
-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::auth::generate_authorization_header;
+use crate::db::DatabaseImpls;
+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 +16,11 @@ use std::{
io::{self, BufWriter, Seek, SeekFrom, Write},
path::PathBuf,
};
+use urlencoding::encode;
+
+use super::download_agent::GameDownloadError;
+use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
+use super::progress_object::ProgressHandle;
pub struct DropWriter {
hasher: Context,
@@ -39,17 +42,19 @@ impl DropWriter {
// Write automatically pushes to file and hasher
impl Write for DropWriter {
fn write(&mut self, buf: &[u8]) -> io::Result {
+ /*
self.hasher.write_all(buf).map_err(|e| {
io::Error::new(
ErrorKind::Other,
format!("Unable to write to hasher: {}", e),
)
})?;
+ */
self.destination.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
- self.hasher.flush()?;
+ // self.hasher.flush()?;
self.destination.flush()
}
}
@@ -60,18 +65,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,25 +121,42 @@ 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 {
+) -> Result {
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0);
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()))?;
+ .map_err(|e| GameDownloadError::Communication(e.into()))?;
if response.status() != 200 {
- let err = response.json().unwrap();
- return Err(ApplicationDownloadError::Communication(
- RemoteAccessError::InvalidResponse(err),
+ warn!("{}", response.text().unwrap());
+ return Err(GameDownloadError::Communication(
+ RemoteAccessError::InvalidCodeError(400),
));
}
@@ -148,9 +170,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()),
+ return Err(GameDownloadError::Communication(
+ RemoteAccessError::InvalidResponse,
));
}
@@ -164,7 +185,7 @@ pub fn download_game_chunk(
let completed = pipeline
.copy()
- .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
+ .map_err(|e| GameDownloadError::IoError(e.kind()))?;
if !completed {
return Ok(false);
};
@@ -173,17 +194,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/download_manager/download_manager.rs b/src-tauri/src/downloads/download_manager.rs
similarity index 58%
rename from src-tauri/src/download_manager/download_manager.rs
rename to src-tauri/src/downloads/download_manager.rs
index b3dee68..55bfa83 100644
--- a/src-tauri/src/download_manager/download_manager.rs
+++ b/src-tauri/src/downloads/download_manager.rs
@@ -4,19 +4,18 @@ use std::{
fmt::Debug,
sync::{
mpsc::{SendError, Sender},
- Mutex, MutexGuard,
+ Arc, Mutex, MutexGuard,
},
thread::JoinHandle,
};
-use log::{debug, info};
+use log::info;
use serde::Serialize;
-use crate::error::application_download_error::ApplicationDownloadError;
-
use super::{
- download_manager_builder::{CurrentProgressObject, DownloadAgent},
- downloadable_metadata::DownloadableMetadata,
+ download_agent::{GameDownloadAgent, GameDownloadError},
+ download_manager_builder::CurrentProgressObject,
+ progress_object::ProgressObject,
queue::Queue,
};
@@ -25,27 +24,27 @@ pub enum DownloadManagerSignal {
Go,
/// Pauses the DownloadManager
Stop,
- /// Called when a DownloadAgent has fully completed a download.
- Completed(DownloadableMetadata),
- /// Generates and appends a DownloadAgent
+ /// Called when a GameDownloadAgent has fully completed a download.
+ Completed(String),
+ /// Generates and appends a GameDownloadAgent
/// to the registry and queue
- Queue(DownloadAgent),
+ Queue(String, String, usize),
/// Tells the Manager to stop the current
/// download, sync everything to disk, and
/// then exit
Finish,
- /// Stops, removes, and tells a download to cleanup
- Cancel(DownloadableMetadata),
- /// Removes a given application
- Remove(DownloadableMetadata),
+ /// Stops (but doesn't remove) current download
+ Cancel,
+ /// Removes a given game
+ Remove(String),
/// Any error which occurs in the agent
- Error(ApplicationDownloadError),
+ Error(GameDownloadError),
/// Pushes UI update
UpdateUIQueue,
UpdateUIStats(usize, usize), //kb/s and seconds
- /// Uninstall download
- /// Takes download ID
- Uninstall(DownloadableMetadata),
+ /// Uninstall game
+ /// Takes game ID
+ Uninstall(String),
}
#[derive(Debug, Clone)]
@@ -53,7 +52,7 @@ pub enum DownloadManagerStatus {
Downloading,
Paused,
Empty,
- Error(ApplicationDownloadError),
+ Error(GameDownloadError),
Finished,
}
@@ -66,8 +65,8 @@ impl Serialize for DownloadManagerStatus {
}
}
-#[derive(Serialize, Clone, Debug)]
-pub enum DownloadStatus {
+#[derive(Serialize, Clone)]
+pub enum GameDownloadStatus {
Queued,
Downloading,
Error,
@@ -84,11 +83,32 @@ 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,
}
+pub struct GameDownloadAgentQueueStandin {
+ pub id: String,
+ pub status: Mutex,
+ pub progress: Arc,
+}
+impl From> for GameDownloadAgentQueueStandin {
+ fn from(value: Arc) -> Self {
+ Self {
+ id: value.id.clone(),
+ status: Mutex::from(GameDownloadStatus::Queued),
+ progress: value.progress.clone(),
+ }
+ }
+}
+impl Debug for GameDownloadAgentQueueStandin {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("GameDownloadAgentQueueStandin")
+ .field("id", &self.id)
+ .finish()
+ }
+}
#[allow(dead_code)]
impl DownloadManager {
@@ -99,44 +119,49 @@ impl DownloadManager {
command_sender: Sender,
) -> Self {
Self {
- terminator: Mutex::new(Some(terminator)),
+ terminator,
download_queue,
progress,
command_sender,
}
}
- pub fn queue_download(
+ pub fn queue_game(
&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 game 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 {
+ pub fn get_current_game_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, game_id: String) {
self.command_sender
- .send(DownloadManagerSignal::Cancel(meta))
+ .send(DownloadManagerSignal::Remove(game_id))
.unwrap();
}
pub fn rearrange(&self, current_index: usize, new_index: usize) {
@@ -147,18 +172,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 +190,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 +199,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_game(&self, game_id: String) {
self.command_sender
- .send(DownloadManagerSignal::Uninstall(meta))
+ .send(DownloadManagerSignal::Uninstall(game_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
+/// get the index of whatever game_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/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs
new file mode 100644
index 0000000..91ac61c
--- /dev/null
+++ b/src-tauri/src/downloads/download_manager_builder.rs
@@ -0,0 +1,535 @@
+use std::{
+ collections::HashMap,
+ fs::remove_dir_all,
+ sync::{
+ mpsc::{channel, Receiver, Sender},
+ Arc, Mutex, RwLockWriteGuard,
+ },
+ thread::{spawn, JoinHandle},
+};
+
+use log::{error, info};
+use tauri::{AppHandle, Emitter};
+
+use crate::{
+ db::{Database, GameStatus, GameTransientStatus},
+ library::{
+ on_game_complete, push_game_update, QueueUpdateEvent,
+ QueueUpdateEventQueueData, StatsUpdateEvent,
+ },
+ state::GameStatusManager,
+ DB,
+};
+
+use super::{
+ download_agent::{GameDownloadAgent, GameDownloadError},
+ download_manager::{
+ DownloadManager, DownloadManagerSignal, DownloadManagerStatus,
+ GameDownloadAgentQueueStandin, GameDownloadStatus,
+ },
+ download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
+ progress_object::ProgressObject,
+ queue::Queue,
+};
+
+/*
+
+Welcome to the download manager, the most overengineered, glorious piece of bullshit.
+
+The download manager takes a queue of game_ids and their associated
+GameDownloadAgents, and then, one-by-one, executes them. It provides an interface
+to interact with the currently downloading agent, and manage the queue.
+
+When the DownloadManager is initialised, it is designed to provide a reference
+which can be used to provide some instructions (the DownloadManagerInterface),
+but other than that, it runs without any sort of interruptions.
+
+It does this by opening up two data structures. Primarily is the command_receiver,
+and mpsc (multi-channel-single-producer) which allows commands to be sent from
+the Interface, and queued up for the Manager to process.
+
+These have been mapped in the DownloadManagerSignal docs.
+
+The other way to interact with the DownloadManager is via the donwload_queue,
+which is just a collection of ids which may be rearranged to suit
+whichever download queue order is required.
+
++----------------------------------------------------------------------------+
+| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! |
+| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE |
+| WHICH HAS NOT BEEN ACCOUNTED FOR |
++----------------------------------------------------------------------------+
+
+This download queue does not actually own any of the GameDownloadAgents. It is
+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.
+
+Behold, my madness - quexeky
+
+*/
+
+// Refactored to consolidate this type. It's a monster.
+pub type CurrentProgressObject = Arc>>>;
+
+pub struct DownloadManagerBuilder {
+ download_agent_registry: HashMap>>,
+ download_queue: Queue,
+ command_receiver: Receiver,
+ sender: Sender,
+ progress: CurrentProgressObject,
+ status: Arc>,
+ app_handle: AppHandle,
+
+ current_download_agent: Option>, // Should be the only game download agent in the map with the "Go" flag
+ current_download_thread: Mutex>>,
+ active_control_flag: Option,
+}
+
+impl DownloadManagerBuilder {
+ pub fn build(app_handle: AppHandle) -> DownloadManager {
+ let queue = Queue::new();
+ let (command_sender, command_receiver) = channel();
+ let active_progress = Arc::new(Mutex::new(None));
+ let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
+
+ let manager = Self {
+ download_agent_registry: HashMap::new(),
+ download_queue: queue.clone(),
+ command_receiver,
+ status: status.clone(),
+ sender: command_sender.clone(),
+ progress: active_progress.clone(),
+ app_handle,
+
+ current_download_agent: None,
+ current_download_thread: Mutex::new(None),
+ active_control_flag: None,
+ };
+
+ let terminator = spawn(|| manager.manage_queue());
+
+ DownloadManager::new(terminator, queue, active_progress, command_sender)
+ }
+
+ fn set_game_status, &String)>(
+ &self,
+ 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 = GameStatusManager::fetch_state(&id);
+
+ push_game_update(&self.app_handle, id, status);
+ }
+
+ 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_game(&mut self, game_id: &String) -> Arc> {
+ self.download_queue.pop_front();
+ let download_agent = self.download_agent_registry.remove(game_id).unwrap();
+ self.cleanup_current_download();
+ download_agent
+ }
+
+ // CAREFUL WITH THIS FUNCTION
+ // Make sure the download thread is terminated
+ fn cleanup_current_download(&mut self) {
+ self.active_control_flag = None;
+ *self.progress.lock().unwrap() = None;
+ self.current_download_agent = None;
+
+ let mut download_thread_lock = self.current_download_thread.lock().unwrap();
+ *download_thread_lock = None;
+ drop(download_thread_lock);
+ }
+
+ 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(game_id) => {
+ self.manage_completed_signal(game_id);
+ }
+ DownloadManagerSignal::Queue(game_id, version, target_download_dir) => {
+ self.manage_queue_signal(game_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();
+ }
+ DownloadManagerSignal::UpdateUIStats(kbs, time) => {
+ self.push_ui_stats_update(kbs, time);
+ }
+ DownloadManagerSignal::Finish => {
+ self.stop_and_wait_current_download();
+ return Ok(());
+ }
+ DownloadManagerSignal::Remove(game_id) => {
+ self.manage_remove_game_queue(game_id);
+ }
+ DownloadManagerSignal::Uninstall(game_id) => {
+ self.uninstall_game(game_id);
+ }
+ };
+ }
+ }
+
+ fn uninstall_game(&mut self, game_id: String) {
+ // Removes the game if it's in the queue
+ self.manage_remove_game_queue(game_id.clone());
+
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ db_handle
+ .games
+ .transient_statuses
+ .entry(game_id.clone())
+ .and_modify(|v| *v = GameTransientStatus::Uninstalling {});
+ push_game_update(
+ &self.app_handle,
+ game_id.clone(),
+ (None, Some(GameTransientStatus::Uninstalling {})),
+ );
+
+ let previous_state = db_handle.games.statuses.get(&game_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 {
+ GameStatus::Installed {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ GameStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ _ => None,
+ } {
+ db_handle
+ .games
+ .transient_statuses
+ .entry(game_id.clone())
+ .and_modify(|v| *v = GameTransientStatus::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(GameDownloadError::IoError(
+ e.kind(),
+ )))
+ .unwrap();
+ }
+ Ok(_) => {
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ db_handle.games.transient_statuses.remove(&game_id);
+ db_handle
+ .games
+ .statuses
+ .entry(game_id.clone())
+ .and_modify(|e| *e = GameStatus::Remote {});
+ drop(db_handle);
+ DB.save().unwrap();
+
+ info!("uninstalled {}", game_id);
+
+ push_game_update(&app_handle, game_id, (Some(GameStatus::Remote {}), None));
+ }
+ });
+ }
+ }
+
+ fn manage_remove_game_queue(&mut self, game_id: String) {
+ if let Some(current_download) = &self.current_download_agent {
+ if current_download.id == game_id {
+ self.manage_cancel_signal();
+ }
+ }
+
+ let index = self.download_queue.get_by_id(game_id.clone());
+ if let Some(index) = index {
+ let mut queue_handle = self.download_queue.edit();
+ queue_handle.remove(index);
+ self.set_game_status(game_id, |db_handle, id| {
+ db_handle.games.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, game_id: String) {
+ info!("Got signal 'Completed'");
+ if let Some(interface) = &self.current_download_agent {
+ // When if let chains are stabilised, combine these two statements
+ if interface.id == game_id {
+ info!("Popping consumed data");
+ let download_agent = self.remove_and_cleanup_front_game(&game_id);
+ let download_agent_lock = download_agent.lock().unwrap();
+
+ let version = download_agent_lock.version.clone();
+ let install_dir = download_agent_lock
+ .stored_manifest
+ .base_path
+ .clone()
+ .to_string_lossy()
+ .to_string();
+
+ drop(download_agent_lock);
+
+ if let Err(error) =
+ on_game_complete(game_id, version, install_dir, &self.app_handle)
+ {
+ self.sender
+ .send(DownloadManagerSignal::Error(
+ GameDownloadError::Communication(error),
+ ))
+ .unwrap();
+ }
+ }
+ }
+ self.sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+ self.sender.send(DownloadManagerSignal::Go).unwrap();
+ }
+
+ 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!("game 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_game_queue(id.clone());
+ }
+ }
+
+ let download_agent = Arc::new(Mutex::new(GameDownloadAgent::new(
+ id.clone(),
+ version,
+ target_download_dir,
+ self.sender.clone(),
+ )));
+ let download_agent_lock = download_agent.lock().unwrap();
+
+ let agent_status = GameDownloadStatus::Queued;
+ let interface_data = GameDownloadAgentQueueStandin {
+ id: id.clone(),
+ status: Mutex::new(agent_status),
+ progress: download_agent_lock.progress.clone(),
+ };
+ 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_game_status(id, |db, id| {
+ db.games.transient_statuses.insert(
+ id.to_string(),
+ GameTransientStatus::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.clone();
+ *self.progress.lock().unwrap() = Some(progress_object);
+
+ let active_control_flag = download_agent_lock.control_flag.clone();
+ 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 games
+ for queue_game in self.download_queue.read() {
+ let mut status_handle = queue_game.status.lock().unwrap();
+ if queue_game.id == agent_data.id {
+ *status_handle = GameDownloadStatus::Downloading;
+ } else {
+ *status_handle = GameDownloadStatus::Queued;
+ }
+ drop(status_handle);
+ }
+
+ // Set flags for download manager
+ active_control_flag.set(DownloadThreadControlFlag::Go);
+ self.set_status(DownloadManagerStatus::Downloading);
+ self.set_game_status(agent_data.id.clone(), |db, id| {
+ db.games.transient_statuses.insert(
+ id.to_string(),
+ GameTransientStatus::Downloading { version_name },
+ );
+ });
+
+ self.sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+ }
+ fn manage_error_signal(&mut self, error: GameDownloadError) {
+ let current_status = self.current_download_agent.clone().unwrap();
+
+ self.stop_and_wait_current_download();
+ self.remove_and_cleanup_front_game(¤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 = GameDownloadStatus::Error;
+ self.set_status(DownloadManagerStatus::Error(error));
+
+ let game_id = current_status.id.clone();
+ self.set_game_status(game_id, |db_handle, id| {
+ db_handle.games.transient_statuses.remove(id);
+ });
+
+ self.sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+ }
+ fn manage_cancel_signal(&mut self) {
+ self.stop_and_wait_current_download();
+
+ info!("cancel waited for download to finish");
+
+ self.cleanup_current_download();
+ }
+ fn set_status(&self, status: DownloadManagerStatus) {
+ *self.status.lock().unwrap() = status;
+ }
+}
diff --git a/src-tauri/src/download_manager/download_thread_control_flag.rs b/src-tauri/src/downloads/download_thread_control_flag.rs
similarity index 100%
rename from src-tauri/src/download_manager/download_thread_control_flag.rs
rename to src-tauri/src/downloads/download_thread_control_flag.rs
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/downloads/mod.rs b/src-tauri/src/downloads/mod.rs
new file mode 100644
index 0000000..2694cd7
--- /dev/null
+++ b/src-tauri/src/downloads/mod.rs
@@ -0,0 +1,10 @@
+pub mod download_agent;
+pub mod download_commands;
+mod download_logic;
+pub mod download_manager;
+pub mod download_manager_builder;
+mod download_thread_control_flag;
+mod manifest;
+mod progress_object;
+pub mod queue;
+mod stored_manifest;
diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs
new file mode 100644
index 0000000..2e0a194
--- /dev/null
+++ b/src-tauri/src/downloads/progress_object.rs
@@ -0,0 +1,144 @@
+use std::{
+ sync::{
+ atomic::{AtomicUsize, Ordering},
+ mpsc::Sender,
+ Arc, Mutex, RwLock,
+ },
+ time::{Duration, Instant},
+};
+
+use log::info;
+
+use super::download_manager::DownloadManagerSignal;
+
+#[derive(Clone)]
+pub struct ProgressObject {
+ max: Arc>,
+ progress_instances: Arc>>>,
+ start: Arc>,
+ sender: Sender,
+
+ points_towards_update: Arc,
+ points_to_push_update: Arc,
+ last_update: Arc>,
+ amount_last_update: Arc,
+}
+
+pub struct ProgressHandle {
+ progress: Arc,
+ progress_object: Arc,
+}
+
+impl ProgressHandle {
+ pub fn new(progress: Arc, progress_object: Arc) -> Self {
+ Self {
+ progress,
+ progress_object,
+ }
+ }
+ pub fn set(&self, amount: usize) {
+ self.progress.store(amount, Ordering::Relaxed);
+ }
+ pub fn add(&self, amount: usize) {
+ self.progress
+ .fetch_add(amount, std::sync::atomic::Ordering::Relaxed);
+ 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,
+
+ 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();
+ }
+ }
+
+ pub fn set_time_now(&self) {
+ *self.start.lock().unwrap() = Instant::now();
+ }
+ pub fn sum(&self) -> usize {
+ self.progress_instances
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|instance| instance.load(Ordering::Relaxed))
+ .sum()
+ }
+ pub fn get_max(&self) -> usize {
+ *self.max.lock().unwrap()
+ }
+ 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() =
+ (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect();
+ }
+ pub fn get_progress(&self) -> f64 {
+ self.sum() as f64 / self.get_max() as f64
+ }
+ pub fn get(&self, index: usize) -> Arc {
+ self.progress_instances.lock().unwrap()[index].clone()
+ }
+}
diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs
new file mode 100644
index 0000000..0ea65ca
--- /dev/null
+++ b/src-tauri/src/downloads/queue.rs
@@ -0,0 +1,73 @@
+use std::{
+ collections::VecDeque,
+ sync::{Arc, Mutex, MutexGuard},
+};
+
+use super::download_manager::GameDownloadAgentQueueStandin;
+
+#[derive(Clone)]
+pub struct Queue {
+ inner: Arc>>>,
+}
+
+#[allow(dead_code)]
+impl Queue {
+ pub fn new() -> Self {
+ Self {
+ inner: Arc::new(Mutex::new(VecDeque::new())),
+ }
+ }
+ pub fn read(&self) -> VecDeque> {
+ self.inner.lock().unwrap().clone()
+ }
+ pub fn edit(&self) -> MutexGuard<'_, VecDeque>> {
+ self.inner.lock().unwrap()
+ }
+ pub fn pop_front(&self) -> Option> {
+ self.edit().pop_front()
+ }
+ pub fn empty(&self) -> bool {
+ self.inner.lock().unwrap().len() == 0
+ }
+ /// Either inserts `interface` at the specified index, or appends to
+ /// the back of the deque if index is greater than the length of the deque
+ pub fn insert(&self, interface: GameDownloadAgentQueueStandin, index: usize) {
+ if self.read().len() > index {
+ self.append(interface);
+ } else {
+ self.edit().insert(index, Arc::new(interface));
+ }
+ }
+ pub fn append(&self, interface: GameDownloadAgentQueueStandin) {
+ self.edit().push_back(Arc::new(interface));
+ }
+ pub fn pop_front_if_equal(
+ &self,
+ game_id: String,
+ ) -> Option> {
+ let mut queue = self.edit();
+ let front = match queue.front() {
+ Some(front) => front,
+ None => return None,
+ };
+ if front.id == game_id {
+ return queue.pop_front();
+ }
+ None
+ }
+ pub fn get_by_id(&self, game_id: String) -> Option {
+ self.read().iter().position(|data| data.id == game_id)
+ }
+ pub fn move_to_index_by_id(&self, game_id: String, new_index: usize) -> Result<(), ()> {
+ let index = match self.get_by_id(game_id) {
+ Some(index) => index,
+ None => return Err(()),
+ };
+ let existing = match self.edit().remove(index) {
+ Some(existing) => existing,
+ None => return Err(()),
+ };
+ self.edit().insert(new_index, existing);
+ Ok(())
+ }
+}
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/application_download_error.rs b/src-tauri/src/error/application_download_error.rs
deleted file mode 100644
index d68bd71..0000000
--- a/src-tauri/src/error/application_download_error.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use std::{
- fmt::{Display, Formatter},
- io,
-};
-
-use serde_with::SerializeDisplay;
-
-use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
-
-// TODO: Rename / separate from downloads
-#[derive(Debug, Clone, SerializeDisplay)]
-pub enum ApplicationDownloadError {
- Communication(RemoteAccessError),
- Checksum,
- Setup(SetupError),
- Lock,
- IoError(io::ErrorKind),
- DownloadError,
-}
-
-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::IoError(error) => write!(f, "{}", error),
- ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
- }
- }
-}
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/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs
deleted file mode 100644
index c9b3cd4..0000000
--- a/src-tauri/src/games/downloads/mod.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod commands;
-pub mod download_agent;
-mod download_logic;
-mod manifest;
-mod stored_manifest;
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..47214a8 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,54 +1,42 @@
-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;
+#[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, 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 db::{
+ add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameStatus,
+ DATA_ROOT_DIR,
};
-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 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 downloads::download_manager::DownloadManager;
+use downloads::download_manager_builder::DownloadManagerBuilder;
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::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 +46,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 +78,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 +109,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 +118,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 +129,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.games.statuses.clone();
drop(db_handle);
for (game_id, status) in statuses.into_iter() {
match status {
- database::db::GameDownloadStatus::Remote {} => {}
- database::db::GameDownloadStatus::SetupRequired {
+ db::GameStatus::Remote {} => {}
+ db::GameStatus::SetupRequired {
version_name: _,
install_dir,
} => {
@@ -161,7 +153,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id);
}
}
- database::db::GameDownloadStatus::Installed {
+ db::GameStatus::Installed {
version_name: _,
install_dir,
} => {
@@ -175,23 +167,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 {});
+ .games
+ .statuses
+ .entry(game_id.to_string())
+ .and_modify(|v| *v = GameStatus::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 +184,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
games,
download_manager,
process_manager,
+ compat_manager,
}
}
@@ -224,15 +210,9 @@ 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 +226,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,
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
- .plugin(tauri_plugin_autostart::init(
- tauri_plugin_autostart::MacosLauncher::LaunchAgent,
- Some(vec!["--minimize"]),
- ))
+ .plugin(tauri_plugin_gamepad::init())
.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 +267,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 +297,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: {}",
@@ -386,20 +346,16 @@ pub fn run() {
responder.respond(resp);
})
- .on_window_event(|window, event| {
- if let WindowEvent::CloseRequested { api, .. } = event {
- window.hide().unwrap();
- api.prevent_close();
- }
+ .on_window_event(|window, event| if let WindowEvent::CloseRequested { api, .. } = event {
+ window.hide().unwrap();
+ api.prevent_close();
})
.build(tauri::generate_context!())
.expect("error while running tauri application");
- app.run(|_app_handle, event| {
- if let RunEvent::ExitRequested { code, api, .. } = event {
- if code.is_none() {
- api.prevent_exit();
- }
+ 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..e5705a1
--- /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::GameVersion;
+use crate::db::GameStatus;
+use crate::downloads::download_manager::{DownloadManagerStatus, GameDownloadStatus};
+use crate::process::process_manager::Platform;
+use crate::remote::RemoteAccessError;
+use crate::state::{GameStatusManager, GameStatusWithTransient};
+use crate::{auth::generate_authorization_header, AppState, DB};
+
+#[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: GameStatusWithTransient,
+}
+
+#[derive(Serialize, Clone)]
+pub struct QueueUpdateEventQueueData {
+ pub id: String,
+ pub status: GameDownloadStatus,
+ 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.games.statuses.contains_key(&game.id) {
+ db_handle
+ .games
+ .statuses
+ .insert(game.id.clone(), GameStatus::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 = GameStatusManager::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
+ .games
+ .statuses
+ .entry(id.clone())
+ .or_insert(GameStatus::Remote {});
+ drop(db_handle);
+
+ let status = GameStatusManager::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