diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..4099407
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+23
diff --git a/README.md b/README.md
index b0df333..903f4ca 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,19 @@
Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It uses a Tauri base with Nuxt 3 + TailwindCSS on top of it, so we can re-use components from the web UI.
+## Running
+Before setting up the drop app, be sure that you have a server set up.
+The instructions for this can be found on the [Drop Wiki](https://wiki.droposs.org/guides/quickstart.html)
+
+## Current features
+Currently supported are the following features:
+- Signin (with custom server)
+- Database registering & recovery
+- Dynamic library fetching from server
+- Installing & uninstalling games
+- Download progress monitoring
+- Launching / playing games
+
## Development
Install dependencies with `yarn`
@@ -10,7 +23,7 @@ Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use she
To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`:
-e.g. `RUST_LOG=debug yarn taudi dev`
+e.g. `RUST_LOG=debug yarn tauri dev`
## Contributing
Check the original [Drop repo](https://github.com/Drop-OSS/drop/blob/main/CONTRIBUTING.md) for contributing guidelines.
\ No newline at end of file
diff --git a/app.vue b/app.vue
index b921045..f81c432 100644
--- a/app.vue
+++ b/app.vue
@@ -6,7 +6,7 @@
diff --git a/components/InitiateAuthModule.vue b/components/InitiateAuthModule.vue
index 5f7158c..1f9be6c 100644
--- a/components/InitiateAuthModule.vue
+++ b/components/InitiateAuthModule.vue
@@ -42,6 +42,31 @@
+
+
Having trouble?
+
+ You can manually enter the token from your web browser.
+
+
+
+ continueManual_wrapper()"
+ class="w-fit"
+ >
+ Submit
+
+
+
+
@@ -101,6 +126,10 @@ import { invoke } from "@tauri-apps/api/core";
const loading = ref(false);
const error = ref
();
+const offerManual = ref(false);
+const manualToken = ref("");
+const manualLoading = ref(false);
+
async function auth() {
await invoke("auth_initiate");
}
@@ -111,5 +140,23 @@ function authWrapper_wrapper() {
loading.value = false;
error.value = e;
});
+ setTimeout(() => {
+ offerManual.value = true;
+ }, 10000);
+}
+
+async function continueManual() {
+ await invoke("manual_recieve_handshake", { token: manualToken.value });
+}
+
+function continueManual_wrapper() {
+ loading.value = true;
+ continueManual()
+ .catch((e) => {
+ error.value = e;
+ })
+ .finally(() => {
+ loading.value = false;
+ });
}
diff --git a/components/PageWidget.vue b/components/PageWidget.vue
new file mode 100644
index 0000000..ad9428b
--- /dev/null
+++ b/components/PageWidget.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/composables/downloads.ts b/composables/downloads.ts
new file mode 100644
index 0000000..55de7fb
--- /dev/null
+++ b/composables/downloads.ts
@@ -0,0 +1,28 @@
+import { listen } from "@tauri-apps/api/event";
+import type { DownloadableMetadata } from "~/types";
+
+export type QueueState = {
+ queue: Array<{ meta: DownloadableMetadata; status: string; progress: number | null }>;
+ status: string;
+};
+
+export type StatsState = {
+ speed: number; // Bytes per second
+ time: number; // Seconds,
+};
+
+export const useQueueState = () =>
+ useState("queue", () => ({ queue: [], status: "Unknown" }));
+
+export const useStatsState = () =>
+ useState("stats", () => ({ speed: 0, time: 0 }));
+
+listen("update_queue", (event) => {
+ const queue = useQueueState();
+ queue.value = event.payload as QueueState;
+});
+
+listen("update_stats", (event) => {
+ const stats = useStatsState();
+ stats.value = event.payload as StatsState;
+});
\ No newline at end of file
diff --git a/composables/game.ts b/composables/game.ts
index 7f28435..0335a7a 100644
--- a/composables/game.ts
+++ b/composables/game.ts
@@ -12,7 +12,8 @@ export type SerializedGameStatus = [
OptionGameStatus | null
];
-const parseStatus = (status: SerializedGameStatus): GameStatus => {
+export const parseStatus = (status: SerializedGameStatus): GameStatus => {
+ console.log(status);
if (status[0]) {
return {
type: status[0].type,
@@ -28,28 +29,29 @@ const parseStatus = (status: SerializedGameStatus): GameStatus => {
}
};
-export const useGame = async (id: string) => {
- if (!gameRegistry[id]) {
+export const useGame = async (gameId: string) => {
+ if (!gameRegistry[gameId]) {
const data: { game: Game; status: SerializedGameStatus } = await invoke(
"fetch_game",
{
- id,
+ gameId,
}
);
- gameRegistry[id] = data.game;
- if (!gameStatusRegistry[id]) {
- gameStatusRegistry[id] = ref(parseStatus(data.status));
+ gameRegistry[gameId] = data.game;
+ if (!gameStatusRegistry[gameId]) {
+ gameStatusRegistry[gameId] = ref(parseStatus(data.status));
- listen(`update_game/${id}`, (event) => {
+ listen(`update_game/${gameId}`, (event) => {
const payload: {
status: SerializedGameStatus;
} = event.payload as any;
- gameStatusRegistry[id].value = parseStatus(payload.status);
+ console.log(payload.status);
+ gameStatusRegistry[gameId].value = parseStatus(payload.status);
});
}
}
- const game = gameRegistry[id];
- const status = gameStatusRegistry[id];
+ const game = gameRegistry[gameId];
+ const status = gameStatusRegistry[gameId];
return { game, status };
-};
+};
\ No newline at end of file
diff --git a/composables/generateGameMeta.ts b/composables/generateGameMeta.ts
new file mode 100644
index 0000000..fdfc4b7
--- /dev/null
+++ b/composables/generateGameMeta.ts
@@ -0,0 +1,9 @@
+import { type DownloadableMetadata, DownloadableType } from '~/types'
+
+export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata {
+ return {
+ id: gameId,
+ version,
+ downloadType: DownloadableType.Game
+ }
+}
\ No newline at end of file
diff --git a/composables/queue.ts b/composables/queue.ts
deleted file mode 100644
index 0487260..0000000
--- a/composables/queue.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { listen } from "@tauri-apps/api/event";
-
-export type QueueState = {
- queue: Array<{ id: string; status: string, progress: number | null }>;
-};
-
-export const useQueueState = () =>
- useState("queue", () => ({ queue: [] }));
-
-listen("update_queue", (event) => {
- const queue = useQueueState();
- queue.value = event.payload as QueueState;
-});
diff --git a/composables/state-navigation.ts b/composables/state-navigation.ts
index 790d258..f7a9b4b 100644
--- a/composables/state-navigation.ts
+++ b/composables/state-navigation.ts
@@ -1,4 +1,5 @@
import { listen } from "@tauri-apps/api/event";
+import { data } from "autoprefixer";
import { AppStatus, type AppState } from "~/types";
export function setupHooks() {
@@ -18,6 +19,20 @@ export function setupHooks() {
router.push("/store");
});
+ listen("download_error", (event) => {
+ createModal(
+ ModalType.Notification,
+ {
+ title: "Drop encountered an error while downloading",
+ description: `Drop encountered an error while downloading your game: "${(
+ event.payload as unknown as string
+ ).toString()}"`,
+ buttonText: "Close"
+ },
+ (e, c) => c()
+ );
+ });
+
/*
document.addEventListener("contextmenu", (event) => {
diff --git a/package.json b/package.json
index 7226f0f..c3f715d 100644
--- a/package.json
+++ b/package.json
@@ -18,9 +18,9 @@
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1",
+ "@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-shell": ">=2.0.0",
"markdown-it": "^14.1.0",
- "moment": "^2.30.1",
"nuxt": "^3.13.0",
"scss": "^0.2.4",
"vue": "latest",
diff --git a/pages/account.vue b/pages/account.vue
new file mode 100644
index 0000000..6a072de
--- /dev/null
+++ b/pages/account.vue
@@ -0,0 +1,72 @@
+
+
+
+
+ Account
+
+
+
+
+
+
+
+
+
+
Sign out
+
+ Sign out of your Drop account on this device
+
+
+
+ Sign out
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/library.vue b/pages/library.vue
index 8acd519..925addb 100644
--- a/pages/library.vue
+++ b/pages/library.vue
@@ -1,34 +1,31 @@
-
+
-
+
-
+
+ {{ nav.label }}
+
@@ -41,16 +38,26 @@
diff --git a/pages/queue.vue b/pages/queue.vue
index 6572203..d78967c 100644
--- a/pages/queue.vue
+++ b/pages/queue.vue
@@ -1,27 +1,32 @@
-
+
+
+
+ {{ formatKilobytes(stats.speed) }}
+ {{ formatTime(stats.time) }} left
+
+
+
+
-
+
-
+
-
+
- {{ games[element.id].game.mName }}
+ {{ games[element.meta.id].game.mName }}
- {{ games[element.id].game.mShortDescription }}
+ {{ games[element.meta.id].game.mShortDescription }}
@@ -30,31 +35,20 @@
{{ element.status }}
-
- cancelGame(element.id)" class="group">
-
+ cancelGame(element.meta)" class="group">
+
Loading...
-
@@ -63,23 +57,68 @@
diff --git a/pages/settings.vue b/pages/settings.vue
index 1e92a0f..3c39c65 100644
--- a/pages/settings.vue
+++ b/pages/settings.vue
@@ -9,25 +9,18 @@
-
+
-
+ ? 'text-zinc-100'
+ : 'text-zinc-400 group-hover:text-zinc-200',
+ 'transition h-6 w-6 shrink-0',
+ ]" aria-hidden="true" />
{{ item.label }}
@@ -43,13 +36,53 @@
diff --git a/pages/settings/debug.vue b/pages/settings/debug.vue
new file mode 100644
index 0000000..7a549ca
--- /dev/null
+++ b/pages/settings/debug.vue
@@ -0,0 +1,136 @@
+
+
+
+
+ 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 d7e7d53..e3a0fab 100644
--- a/pages/settings/downloads.vue
+++ b/pages/settings/downloads.vue
@@ -59,6 +59,44 @@
+
+
+ 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.
+
+
+
+
+
+ Save Changes
+
+
+
@@ -180,6 +218,9 @@ const createDirectoryLoading = ref(false);
const dirs = ref>([]);
+const downloadThreads = ref(4);
+const downloadThreadsChanged = computed(() => downloadThreads.value !== 4);
+
async function updateDirs() {
const newDirs = await invoke>("fetch_download_dir_stats");
dirs.value = newDirs;
@@ -235,4 +276,9 @@ async function deleteDirectory(index: number) {
await invoke("delete_download_dir", { index });
await updateDirs();
}
+
+async function saveDownloadThreads() {
+ //Would save download threads downloadThreads.value);
+ await invoke("update_settings", { newSettings: { maxDownloadThreads: downloadThreads.value } })
+}
diff --git a/pages/settings/index.vue b/pages/settings/index.vue
index 27e0f69..241c840 100644
--- a/pages/settings/index.vue
+++ b/pages/settings/index.vue
@@ -1,3 +1,60 @@
-
-
\ No newline at end of file
+
+
+
General
+
+ Configure basic application settings
+
+
+
+
+
+
Start with system
+
+ Drop will automatically start when you log into your computer
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/settings/interface.vue b/pages/settings/interface.vue
index 27e0f69..3df236b 100644
--- a/pages/settings/interface.vue
+++ b/pages/settings/interface.vue
@@ -1,3 +1,7 @@
-
\ No newline at end of file
+
+
+
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 74a6576..0c8123b 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -261,6 +261,17 @@ 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"
@@ -355,6 +366,12 @@ dependencies = [
"piper",
]
+[[package]]
+name = "boxcar"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
+
[[package]]
name = "brotli"
version = "7.0.0"
@@ -829,6 +846,37 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.91",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn 2.0.91",
+]
+
[[package]]
name = "derive_more"
version = "0.99.18"
@@ -864,7 +912,16 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
- "dirs-sys",
+ "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",
]
[[package]]
@@ -873,7 +930,18 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
- "dirs-sys",
+ "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",
]
[[package]]
@@ -965,14 +1033,18 @@ dependencies = [
name = "drop-app"
version = "0.1.0"
dependencies = [
+ "boxcar",
"chrono",
+ "derive_builder",
"directories",
"hex",
"http",
"log",
"log4rs",
"md5",
+ "merge-struct",
"openssl",
+ "parking_lot 0.12.3",
"rayon",
"reqwest",
"rustbreak",
@@ -980,13 +1052,21 @@ dependencies = [
"serde",
"serde-binary",
"serde_json",
+ "serde_merge",
+ "serde_with",
+ "shared_child",
+ "slice-deque",
"tauri",
"tauri-build",
+ "tauri-plugin-autostart",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
+ "tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
+ "throttle_my_fn",
"tokio",
+ "umu-wrapper-lib",
"url",
"urlencoding",
"uuid",
@@ -1037,7 +1117,7 @@ dependencies = [
"rustc_version",
"toml 0.8.2",
"vswhom",
- "winreg",
+ "winreg 0.52.0",
]
[[package]]
@@ -1435,6 +1515,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "gethostname"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
+dependencies = [
+ "rustix",
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -2288,7 +2378,7 @@ dependencies = [
"log",
"log-mdc",
"once_cell",
- "parking_lot",
+ "parking_lot 0.12.3",
"rand 0.8.5",
"serde",
"serde-value",
@@ -2306,6 +2396,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+[[package]]
+name = "mach"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -2356,6 +2455,16 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "merge-struct"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d82012d21e24135b839b6b9bebd622b7ff0cb40071498bc2d066d3a6d04dd4a"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -2853,6 +2962,17 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "os_info"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430"
+dependencies = [
+ "log",
+ "serde",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "os_pipe"
version = "1.2.1"
@@ -2894,6 +3014,17 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -2901,7 +3032,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.9.10",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
]
[[package]]
@@ -2912,7 +3057,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall",
+ "redox_syscall 0.5.8",
"smallvec",
"windows-targets 0.52.6",
]
@@ -3347,6 +3492,15 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.8"
@@ -3759,6 +3913,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_merge"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "606e91878516232ac3b16c12e063d4468d762f16d77e7aef14a1f2326c5f409b"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "serde_repr"
version = "0.1.19"
@@ -3793,9 +3958,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.11.0"
+version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
+checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -3811,9 +3976,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.11.0"
+version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
+checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
@@ -3934,6 +4099,17 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "slice-deque"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
+dependencies = [
+ "libc",
+ "mach",
+ "winapi",
+]
+
[[package]]
name = "smallvec"
version = "1.13.2"
@@ -3966,7 +4142,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
- "redox_syscall",
+ "redox_syscall 0.5.8",
"wasm-bindgen",
"web-sys",
"windows-sys 0.59.0",
@@ -4024,7 +4200,7 @@ checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
dependencies = [
"new_debug_unreachable",
"once_cell",
- "parking_lot",
+ "parking_lot 0.12.3",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
@@ -4107,6 +4283,15 @@ dependencies = [
"syn 2.0.91",
]
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "system-configuration"
version = "0.6.1"
@@ -4168,7 +4353,7 @@ dependencies = [
"ndk-sys",
"objc",
"once_cell",
- "parking_lot",
+ "parking_lot 0.12.3",
"raw-window-handle",
"scopeguard",
"tao-macros",
@@ -4205,7 +4390,7 @@ checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c"
dependencies = [
"anyhow",
"bytes",
- "dirs",
+ "dirs 5.0.1",
"dunce",
"embed_plist",
"futures-util",
@@ -4255,7 +4440,7 @@ checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
dependencies = [
"anyhow",
"cargo_toml",
- "dirs",
+ "dirs 5.0.1",
"glob",
"heck 0.5.0",
"json-patch",
@@ -4327,6 +4512,20 @@ 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"
@@ -4388,6 +4587,24 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "tauri-plugin-os"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
+dependencies = [
+ "gethostname",
+ "log",
+ "os_info",
+ "serde",
+ "serde_json",
+ "serialize-to-javascript",
+ "sys-locale",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.9",
+]
+
[[package]]
name = "tauri-plugin-shell"
version = "2.2.0"
@@ -4597,6 +4814,18 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "throttle_my_fn"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "482c185e5675626c9a130b3a8f362c322a239338c882f745a1d9a85838b987f0"
+dependencies = [
+ "parking_lot 0.11.2",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "time"
version = "0.3.37"
@@ -4813,7 +5042,7 @@ checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b"
dependencies = [
"core-graphics",
"crossbeam-channel",
- "dirs",
+ "dirs 5.0.1",
"libappindicator",
"muda",
"objc2",
@@ -4870,6 +5099,12 @@ 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"
@@ -5713,6 +5948,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "winreg"
version = "0.52.0"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 64f0f9b..c48f490 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "drop-app"
version = "0.1.0"
-description = "A Tauri App"
-authors = ["you"]
+description = "The client application for the open-source, self-hosted game distribution platform Drop"
+authors = ["Drop OSS"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,7 +26,6 @@ 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"
@@ -41,12 +40,22 @@ http = "1.1.0"
urlencoding = "2.1.3"
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"
+derive_builder = "0.20.2"
+throttle_my_fn = "0.2.6"
+parking_lot = "0.12.3"
+merge-struct = "0.1.0"
+serde_merge = "0.1.3"
[dependencies.tauri]
version = "2.1.1"
-features = [
- "tray-icon"
-]
+features = ["tray-icon"]
[dependencies.tokio]
@@ -81,6 +90,10 @@ 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 4bafeab..1b818b3 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -13,6 +13,7 @@
"core:window:allow-maximize",
"core:window:allow-close",
"deep-link:default",
- "dialog:default"
+ "dialog:default",
+ "os:default"
]
}
\ No newline at end of file
diff --git a/src-tauri/rust-toolchain.toml b/src-tauri/rust-toolchain.toml
new file mode 100644
index 0000000..271800c
--- /dev/null
+++ b/src-tauri/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
\ No newline at end of file
diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs
new file mode 100644
index 0000000..da0c1aa
--- /dev/null
+++ b/src-tauri/src/autostart.rs
@@ -0,0 +1,76 @@
+use crate::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!("Disabled autostart");
+ }
+
+ // Store the state in DB
+ let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
+ db_handle.settings.autostart = enabled;
+ drop(db_handle);
+ DB.save().map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+pub fn get_autostart_enabled_logic(app: AppHandle) -> Result {
+ // First check DB state
+ let db_handle = DB.borrow_data().unwrap();
+ 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 = DB.borrow_data().map_err(|e| e.to_string())?;
+ let should_be_enabled = db_handle.settings.autostart;
+ drop(db_handle);
+
+ let manager = app.autolaunch();
+ let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
+
+ if current_state != should_be_enabled {
+ if should_be_enabled {
+ manager.enable().map_err(|e| e.to_string())?;
+ 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 2fe2d2d..d8817ae 100644
--- a/src-tauri/src/cleanup.rs
+++ b/src-tauri/src/cleanup.rs
@@ -1,18 +1,13 @@
-use std::sync::Mutex;
-
-use log::info;
+use log::{debug, info};
use tauri::AppHandle;
-use crate::AppState;
-
#[tauri::command]
pub fn quit(app: tauri::AppHandle) {
cleanup_and_exit(&app);
}
-
-pub fn cleanup_and_exit(app: &AppHandle, ) {
- info!("exiting drop application...");
+pub fn cleanup_and_exit(app: &AppHandle) {
+ debug!("Cleaning up and exiting application");
app.exit(0);
}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
new file mode 100644
index 0000000..a0b646e
--- /dev/null
+++ b/src-tauri/src/commands.rs
@@ -0,0 +1,16 @@
+use tauri::AppHandle;
+
+use crate::{
+ autostart::{get_autostart_enabled_logic, toggle_autostart_logic},
+ AppState,
+};
+
+#[tauri::command]
+pub fn fetch_state(
+ state: tauri::State<'_, std::sync::Mutex>>,
+) -> Result {
+ let guard = state.lock().unwrap();
+ let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
+ drop(guard);
+ Ok(cloned_state)
+}
diff --git a/src-tauri/src/database/commands.rs b/src-tauri/src/database/commands.rs
new file mode 100644
index 0000000..b254e00
--- /dev/null
+++ b/src-tauri/src/database/commands.rs
@@ -0,0 +1,83 @@
+use std::{
+ fs::create_dir_all,
+ io::{Error, ErrorKind},
+ path::{Path, PathBuf},
+};
+
+use serde_json::Value;
+
+use crate::{database::settings::Settings, error::user_error::UserValue, DB};
+
+use super::{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 = DB.borrow_data().unwrap();
+ lock.applications.install_dirs.clone()
+}
+
+#[tauri::command]
+pub fn delete_download_dir(index: usize) {
+ let mut lock = DB.borrow_data_mut().unwrap();
+ lock.applications.install_dirs.remove(index);
+ drop(lock);
+ DB.save().unwrap();
+}
+
+#[tauri::command]
+pub fn add_download_dir(new_dir: PathBuf) -> UserValue<(), Error> {
+ // 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 UserValue::Err(Error::new(
+ ErrorKind::DirectoryNotEmpty,
+ "Selected directory cannot contain any existing files",
+ ));
+ }
+ } else {
+ create_dir_all(new_dir_path)?;
+ }
+
+ // Add it to the dictionary
+ let mut lock = DB.borrow_data_mut().unwrap();
+ if lock.applications.install_dirs.contains(&new_dir) {
+ return UserValue::Err(Error::new(
+ ErrorKind::AlreadyExists,
+ "Selected directory already exists in database",
+ ));
+ }
+ lock.applications.install_dirs.push(new_dir);
+ drop(lock);
+ DB.save().unwrap();
+
+ UserValue::Ok(())
+}
+
+#[tauri::command]
+pub fn update_settings(new_settings: Value) {
+ println!("{}", new_settings);
+ let mut db_lock = DB.borrow_data_mut().unwrap();
+ 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();
+ }
+ println!("New settings unset: {}", ¤t_settings);
+ 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_system_data() -> SystemData {
+ let db_handle = DB.borrow_data().unwrap();
+ SystemData::new(
+ db_handle.auth.as_ref().unwrap().client_id.clone(),
+ db_handle.base_url.clone(),
+ DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
+ std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
+ )
+}
diff --git a/src-tauri/src/db.rs b/src-tauri/src/database/db.rs
similarity index 53%
rename from src-tauri/src/db.rs
rename to src-tauri/src/database/db.rs
index 00e93ab..376bcb0 100644
--- a/src-tauri/src/db.rs
+++ b/src-tauri/src/database/db.rs
@@ -2,19 +2,27 @@ use std::{
collections::HashMap,
fs::{self, create_dir_all},
path::{Path, PathBuf},
- sync::{LazyLock, Mutex},
+ sync::{LazyLock, Mutex, RwLockWriteGuard},
};
+use chrono::Utc;
use directories::BaseDirs;
-use log::debug;
-use rustbreak::{DeSerError, DeSerializer, PathDatabase};
+use log::{debug, 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::{process::process_manager::Platform, DB};
+use crate::{
+ database::settings::Settings,
+ download_manager::downloadable_metadata::DownloadableMetadata,
+ games::{library::push_game_update, state::GameStatusManager},
+ process::process_manager::Platform,
+ DB,
+};
#[derive(serde::Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
@@ -24,7 +32,7 @@ pub struct DatabaseAuth {
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
-pub enum GameStatus {
+pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
@@ -38,13 +46,14 @@ pub enum GameStatus {
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize)]
-pub enum GameTransientStatus {
+pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
+ Running {},
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub version_index: usize,
@@ -54,24 +63,40 @@ pub struct GameVersion {
pub platform: Platform,
}
-#[derive(Serialize, Clone, Deserialize)]
+#[serde_as]
+#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
-pub struct DatabaseGames {
- pub install_dirs: Vec,
+pub struct DatabaseApplications {
+ pub install_dirs: Vec,
// Guaranteed to exist if the game also exists in the app state map
- pub statuses: HashMap,
- pub versions: HashMap>,
+ pub game_statuses: HashMap,
+ pub game_versions: HashMap>,
+ pub installed_game_version: HashMap,
#[serde(skip)]
- pub transient_statuses: HashMap,
+ pub transient_statuses: HashMap,
}
-#[derive(Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
+#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
+ #[serde(default)]
+ pub settings: Settings,
pub auth: Option,
pub base_url: String,
- pub games: DatabaseGames,
+ 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()],
+ ..Default::default()
+ },
+ prev_database,
+ ..Default::default()
+ }
+ }
}
pub static DATA_ROOT_DIR: LazyLock> =
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
@@ -107,27 +132,18 @@ impl DatabaseImpls for DatabaseInterface {
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"),
+ 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 {
- 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(),
- },
- };
+ let default = Database::new(games_base_dir, None);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
@@ -148,57 +164,39 @@ impl DatabaseImpls for DatabaseInterface {
}
}
-#[tauri::command]
-pub fn add_download_dir(new_dir: String) -> Result<(), String> {
- // Check the new directory is all good
- let new_dir_path = Path::new(&new_dir);
- if new_dir_path.exists() {
- let metadata = new_dir_path
- .metadata()
- .map_err(|e| format!("Unable to access file or directory: {}", e))?;
- if !metadata.is_dir() {
- return Err("Invalid path: not a directory".to_string());
- }
- let dir_contents = new_dir_path
- .read_dir()
- .map_err(|e| format!("Unable to check directory contents: {}", e))?;
- if dir_contents.count() != 0 {
- return Err("Directory is not empty".to_string());
- }
- } else {
- create_dir_all(new_dir_path)
- .map_err(|e| format!("Unable to create directories to path: {}", e))?;
- }
-
- // Add it to the dictionary
- let mut lock = DB.borrow_data_mut().unwrap();
- if lock.games.install_dirs.contains(&new_dir) {
- return Err("Download directory already used".to_string());
- }
- lock.games.install_dirs.push(new_dir);
- drop(lock);
+pub fn set_game_status, &DownloadableMetadata)>(
+ app_handle: &AppHandle,
+ meta: DownloadableMetadata,
+ setter: F,
+) {
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ setter(&mut db_handle, &meta);
+ drop(db_handle);
DB.save().unwrap();
- Ok(())
+ 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.to_string()));
+ base
+ };
+ info!("{:?}", new_path);
+ fs::rename(&db_path, &new_path).unwrap();
-#[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();
+ let db = Database::new(
+ games_base_dir.into_os_string().into_string().unwrap(),
+ Some(new_path.into()),
+ );
- 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)
+ PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
}
diff --git a/src-tauri/src/database/debug.rs b/src-tauri/src/database/debug.rs
new file mode 100644
index 0000000..8547b73
--- /dev/null
+++ b/src-tauri/src/database/debug.rs
@@ -0,0 +1,22 @@
+use crate::{DATA_ROOT_DIR, DB};
+use serde::Serialize;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SystemData {
+ client_id: String,
+ base_url: String,
+ data_dir: String,
+ log_level: String,
+}
+
+impl SystemData {
+ pub fn new(client_id: String, base_url: String, data_dir: String, log_level: String) -> Self {
+ Self {
+ client_id,
+ base_url,
+ data_dir,
+ log_level,
+ }
+ }
+}
diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs
new file mode 100644
index 0000000..2a4255b
--- /dev/null
+++ b/src-tauri/src/database/mod.rs
@@ -0,0 +1,4 @@
+pub mod commands;
+pub mod db;
+pub mod debug;
+pub mod settings;
diff --git a/src-tauri/src/database/settings.rs b/src-tauri/src/database/settings.rs
new file mode 100644
index 0000000..b8c1742
--- /dev/null
+++ b/src-tauri/src/database/settings.rs
@@ -0,0 +1,24 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct Settings {
+ pub autostart: bool,
+ pub max_download_threads: usize,
+ // ... other settings ...
+}
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ autostart: false,
+ max_download_threads: 4,
+ }
+ }
+}
+// Ideally use pointers instead of a macro to assign the settings
+// fn deserialize_into(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
+// where T: for<'a> Deserialize<'a>
+// {
+// *t = serde_json::from_value(v)?;
+// Ok(())
+// }
diff --git a/src-tauri/src/download_manager/commands.rs b/src-tauri/src/download_manager/commands.rs
new file mode 100644
index 0000000..aa69aae
--- /dev/null
+++ b/src-tauri/src/download_manager/commands.rs
@@ -0,0 +1,31 @@
+use std::sync::{mpsc::SendError, Arc, 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/downloads/download_manager.rs b/src-tauri/src/download_manager/download_manager.rs
similarity index 60%
rename from src-tauri/src/downloads/download_manager.rs
rename to src-tauri/src/download_manager/download_manager.rs
index 8676087..8e5474a 100644
--- a/src-tauri/src/downloads/download_manager.rs
+++ b/src-tauri/src/download_manager/download_manager.rs
@@ -4,7 +4,7 @@ use std::{
fmt::Debug,
sync::{
mpsc::{SendError, Sender},
- Arc, Mutex, MutexGuard,
+ MutexGuard,
},
thread::JoinHandle,
};
@@ -12,10 +12,11 @@ use std::{
use log::info;
use serde::Serialize;
+use crate::error::application_download_error::ApplicationDownloadError;
+
use super::{
- download_agent::{GameDownloadAgent, GameDownloadError},
- download_manager_builder::CurrentProgressObject,
- progress_object::ProgressObject,
+ download_manager_builder::{CurrentProgressObject, DownloadAgent},
+ downloadable_metadata::DownloadableMetadata,
queue::Queue,
};
@@ -24,35 +25,49 @@ pub enum DownloadManagerSignal {
Go,
/// Pauses the DownloadManager
Stop,
- /// Called when a GameDownloadAgent has fully completed a download.
- Completed(String),
- /// Generates and appends a GameDownloadAgent
+ /// Called when a DownloadAgent has fully completed a download.
+ Completed(DownloadableMetadata),
+ /// Generates and appends a DownloadAgent
/// to the registry and queue
- Queue(String, String, usize),
+ Queue(DownloadAgent),
/// Tells the Manager to stop the current
/// download, sync everything to disk, and
/// then exit
Finish,
- /// Stops (but doesn't remove) current download
- Cancel,
- /// Removes a given game
- Remove(String),
+ /// Stops, removes, and tells a download to cleanup
+ Cancel(DownloadableMetadata),
+ /// Removes a given application
+ Remove(DownloadableMetadata),
/// Any error which occurs in the agent
- Error(GameDownloadError),
+ Error(ApplicationDownloadError),
/// Pushes UI update
- Update,
+ UpdateUIQueue,
+ UpdateUIStats(usize, usize), //kb/s and seconds
+ /// Uninstall download
+ /// Takes download ID
+ Uninstall(DownloadableMetadata),
}
+#[derive(Debug, Clone)]
pub enum DownloadManagerStatus {
Downloading,
Paused,
Empty,
- Error(GameDownloadError),
+ Error(ApplicationDownloadError),
Finished,
}
-#[derive(Serialize, Clone)]
-pub enum GameDownloadStatus {
+impl Serialize for DownloadManagerStatus {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&format!["{:?}", self])
+ }
+}
+
+#[derive(Serialize, Clone, Debug)]
+pub enum DownloadStatus {
Queued,
Downloading,
Error,
@@ -74,27 +89,6 @@ pub struct DownloadManager {
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 {
@@ -112,42 +106,37 @@ impl DownloadManager {
}
}
- pub fn queue_game(
+ pub fn queue_download(
&self,
- id: String,
- version: String,
- target_download_dir: usize,
+ download: DownloadAgent,
) -> Result<(), SendError> {
- info!("Adding game id {}", id);
- self.command_sender.send(DownloadManagerSignal::Queue(
- id,
- version,
- target_download_dir,
- ))?;
+ info!("Adding download id {:?}", download.metadata());
+ self.command_sender
+ .send(DownloadManagerSignal::Queue(download))?;
self.command_sender.send(DownloadManagerSignal::Go)
}
- pub fn edit(&self) -> MutexGuard<'_, VecDeque>> {
+ 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_game_download_progress(&self) -> Option {
+ pub fn get_current_download_progress(&self) -> Option {
let progress_object = (*self.progress.lock().unwrap()).clone()?;
Some(progress_object.get_progress())
}
- pub fn rearrange_string(&self, id: String, new_index: usize) {
+ pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
let mut queue = self.edit();
- let current_index = get_index_from_id(&mut queue, id).unwrap();
+ let current_index = get_index_from_id(&mut queue, meta).unwrap();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
self.command_sender
- .send(DownloadManagerSignal::Update)
+ .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
- pub fn cancel(&self, game_id: String) {
+ pub fn cancel(&self, meta: DownloadableMetadata) {
self.command_sender
- .send(DownloadManagerSignal::Remove(game_id))
+ .send(DownloadManagerSignal::Cancel(meta))
.unwrap();
}
pub fn rearrange(&self, current_index: usize, new_index: usize) {
@@ -158,7 +147,7 @@ impl DownloadManager {
let needs_pause = current_index == 0 || new_index == 0;
if needs_pause {
self.command_sender
- .send(DownloadManagerSignal::Cancel)
+ .send(DownloadManagerSignal::Stop)
.unwrap();
}
@@ -174,7 +163,7 @@ impl DownloadManager {
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
self.command_sender
- .send(DownloadManagerSignal::Update)
+ .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
pub fn pause_downloads(&self) {
@@ -191,15 +180,23 @@ impl DownloadManager {
.unwrap();
self.terminator.join()
}
+ pub fn uninstall_application(&self, meta: DownloadableMetadata) {
+ self.command_sender
+ .send(DownloadManagerSignal::Uninstall(meta))
+ .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 game_id is passed in
+/// get the index of whatever id is passed in
fn get_index_from_id(
- queue: &mut MutexGuard<'_, VecDeque>>,
- id: String,
+ queue: &mut MutexGuard<'_, VecDeque>,
+ meta: &DownloadableMetadata,
) -> Option {
queue
.iter()
- .position(|download_agent| download_agent.id == id)
+ .position(|download_agent| download_agent == meta)
}
diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs
new file mode 100644
index 0000000..4738730
--- /dev/null
+++ b/src-tauri/src/download_manager/download_manager_builder.rs
@@ -0,0 +1,364 @@
+use std::{
+ collections::HashMap,
+ sync::{
+ mpsc::{channel, Receiver, Sender},
+ Arc, Mutex,
+ },
+ thread::{spawn, JoinHandle},
+};
+
+use log::{debug, error, info};
+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) {
+ info!("Got signal Queue");
+ let meta = download_agent.metadata();
+
+ info!("Meta: {:?}", meta);
+
+ if self.download_queue.exists(meta.clone()) {
+ info!("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) {
+ info!("Got signal Go");
+ if self.download_agent_registry.is_empty() {
+ info!(
+ "Download agent registry: {:?}",
+ self.download_agent_registry.len()
+ );
+ return;
+ }
+
+ if self.current_download_agent.is_some() {
+ info!(
+ "Current download agent: {:?}",
+ self.current_download_agent.as_ref().unwrap().metadata()
+ );
+ return;
+ }
+
+ info!("Current download queue: {:?}", self.download_queue.read());
+
+ // Should always be Some if the above two statements keep going
+ let agent_data = self.download_queue.read().front().unwrap().clone();
+
+ info!("Starting download for {:?}", agent_data);
+
+ 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) {
+ info!("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) {
+ info!("Got signal Completed");
+ if let Some(interface) = &self.current_download_agent {
+ if interface.metadata() == meta {
+ info!("Popping consumed data");
+ self.remove_and_cleanup_front_download(&meta);
+ }
+ }
+ self.push_ui_queue_update();
+ self.sender.send(DownloadManagerSignal::Go).unwrap();
+ }
+ fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
+ info!("Got signal Error");
+ let current_agent = self.current_download_agent.clone().unwrap();
+
+ current_agent.on_error(&self.app_handle, error.clone());
+
+ self.stop_and_wait_current_download();
+ self.remove_and_cleanup_front_download(¤t_agent.metadata());
+
+ self.set_status(DownloadManagerStatus::Error(error));
+ }
+ fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
+ info!("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();
+ info!("Current donwload queue: {:?}", self.download_queue.read());
+ }
+ // TODO: Collapse these two into a single if statement somehow
+ else if let Some(download_agent) = self.download_agent_registry.get(meta) {
+ info!("Object exists in registry");
+ let index = self.download_queue.get_by_meta(meta);
+ if let Some(index) = index {
+ download_agent.on_cancelled(&self.app_handle);
+ let _ = self.download_queue.edit().remove(index).unwrap();
+ let removed = self.download_agent_registry.remove(meta);
+ info!(
+ "Removed {:?} from queue {:?}",
+ removed.map(|x| x.metadata()),
+ self.download_queue.read()
+ );
+ }
+ }
+ } else if let Some(download_agent) = self.download_agent_registry.get(meta) {
+ info!("Object exists in registry");
+ let index = self.download_queue.get_by_meta(meta);
+ if let Some(index) = index {
+ download_agent.on_cancelled(&self.app_handle);
+ let _ = self.download_queue.edit().remove(index).unwrap();
+ let removed = self.download_agent_registry.remove(meta);
+ info!(
+ "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(),
+ }
+ })
+ .collect();
+
+ let event_data = QueueUpdateEvent { queue: queue_objs };
+ self.app_handle.emit("update_queue", event_data).unwrap();
+ }
+}
diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/download_manager/download_thread_control_flag.rs
similarity index 100%
rename from src-tauri/src/downloads/download_thread_control_flag.rs
rename to src-tauri/src/download_manager/download_thread_control_flag.rs
diff --git a/src-tauri/src/download_manager/downloadable.rs b/src-tauri/src/download_manager/downloadable.rs
new file mode 100644
index 0000000..181b329
--- /dev/null
+++ b/src-tauri/src/download_manager/downloadable.rs
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 0000000..790b669
--- /dev/null
+++ b/src-tauri/src/download_manager/downloadable_metadata.rs
@@ -0,0 +1,26 @@
+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/mod.rs b/src-tauri/src/download_manager/mod.rs
new file mode 100644
index 0000000..0d664dd
--- /dev/null
+++ b/src-tauri/src/download_manager/mod.rs
@@ -0,0 +1,8 @@
+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 progress_object;
+pub mod queue;
diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs
similarity index 54%
rename from src-tauri/src/downloads/progress_object.rs
rename to src-tauri/src/download_manager/progress_object.rs
index 143656d..5d65207 100644
--- a/src-tauri/src/downloads/progress_object.rs
+++ b/src-tauri/src/download_manager/progress_object.rs
@@ -2,12 +2,13 @@ use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::Sender,
- Arc, Mutex,
+ Arc, Mutex, RwLock,
},
- time::Instant,
+ time::{Duration, Instant},
};
use log::info;
+use throttle_my_fn::throttle;
use super::download_manager::DownloadManagerSignal;
@@ -17,9 +18,10 @@ pub struct ProgressObject {
progress_instances: Arc>>>,
start: Arc>,
sender: Sender,
-
points_towards_update: Arc,
- points_to_push_update: Arc>,
+ points_to_push_update: Arc,
+ last_update: Arc>,
+ amount_last_update: Arc,
}
pub struct ProgressHandle {
@@ -58,7 +60,9 @@ impl ProgressObject {
sender,
points_towards_update: Arc::new(AtomicUsize::new(0)),
- points_to_push_update: Arc::new(Mutex::new(points_to_push_update)),
+ 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)),
}
}
@@ -67,16 +71,39 @@ impl ProgressObject {
.points_towards_update
.fetch_add(amount_added, Ordering::Relaxed);
- let to_update_handle = self.points_to_push_update.lock().unwrap();
- let to_update = *to_update_handle;
- drop(to_update_handle);
+ let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
- if current_amount < to_update {
- return;
+ if current_amount >= to_update {
+ self.points_towards_update
+ .fetch_sub(to_update, Ordering::Relaxed);
+ update_queue(&self);
+ }
+
+ 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);
+
+ update_ui(&self, kilobytes_per_second, time_remaining);
}
- self.points_towards_update
- .fetch_sub(to_update, Ordering::Relaxed);
- self.sender.send(DownloadManagerSignal::Update).unwrap();
}
pub fn set_time_now(&self) {
@@ -95,7 +122,8 @@ impl ProgressObject {
}
pub fn set_max(&self, new_max: usize) {
*self.max.lock().unwrap() = new_max;
- *self.points_to_push_update.lock().unwrap() = new_max / PROGRESS_UPDATES;
+ 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) {
@@ -109,3 +137,23 @@ impl ProgressObject {
self.progress_instances.lock().unwrap()[index].clone()
}
}
+
+#[throttle(50, Duration::from_secs(1))]
+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();
+}
+
+#[throttle(50, Duration::from_secs(1))]
+fn update_queue(progress: &ProgressObject) {
+ progress
+ .sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+}
+
diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/download_manager/queue.rs
similarity index 52%
rename from src-tauri/src/downloads/queue.rs
rename to src-tauri/src/download_manager/queue.rs
index 0ea65ca..8025c91 100644
--- a/src-tauri/src/downloads/queue.rs
+++ b/src-tauri/src/download_manager/queue.rs
@@ -3,63 +3,73 @@ use std::{
sync::{Arc, Mutex, MutexGuard},
};
-use super::download_manager::GameDownloadAgentQueueStandin;
+use super::downloadable_metadata::DownloadableMetadata;
#[derive(Clone)]
pub struct Queue {
- inner: Arc>>>,
+ inner: Arc>>,
}
#[allow(dead_code)]
+impl Default for Queue {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
impl Queue {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(VecDeque::new())),
}
}
- pub fn read(&self) -> VecDeque> {
+ pub fn read(&self) -> VecDeque {
self.inner.lock().unwrap().clone()
}
- pub fn edit(&self) -> MutexGuard<'_, VecDeque>> {
+ pub fn edit(&self) -> MutexGuard<'_, VecDeque> {
self.inner.lock().unwrap()
}
- pub fn pop_front(&self) -> Option> {
+ pub fn pop_front(&self) -> Option {
self.edit().pop_front()
}
pub fn 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: GameDownloadAgentQueueStandin, index: usize) {
+ pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
if self.read().len() > index {
self.append(interface);
} else {
- self.edit().insert(index, Arc::new(interface));
+ self.edit().insert(index, interface);
}
}
- pub fn append(&self, interface: GameDownloadAgentQueueStandin) {
- self.edit().push_back(Arc::new(interface));
+ pub fn append(&self, interface: DownloadableMetadata) {
+ self.edit().push_back(interface);
}
- pub fn pop_front_if_equal(
- &self,
- game_id: String,
- ) -> Option> {
+ pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option {
let mut queue = self.edit();
let front = match queue.front() {
Some(front) => front,
None => return None,
};
- if front.id == game_id {
+ if front == meta {
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 get_by_meta(&self, meta: &DownloadableMetadata) -> Option {
+ self.read().iter().position(|data| data == meta)
}
- pub fn move_to_index_by_id(&self, game_id: String, new_index: usize) -> Result<(), ()> {
- let index = match self.get_by_id(game_id) {
+ pub fn move_to_index_by_meta(
+ &self,
+ meta: &DownloadableMetadata,
+ new_index: usize,
+ ) -> Result<(), ()> {
+ let index = match self.get_by_meta(meta) {
Some(index) => index,
None => return Err(()),
};
diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs
deleted file mode 100644
index 99d8217..0000000
--- a/src-tauri/src/downloads/download_agent.rs
+++ /dev/null
@@ -1,335 +0,0 @@
-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 core::time;
-use log::{debug, error, info};
-use rayon::ThreadPoolBuilder;
-use serde::ser::{Error, SerializeMap};
-use serde::{Deserialize, Serialize};
-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: Mutex>,
- pub manifest: Mutex>,
- pub progress: Arc,
- sender: Sender,
- pub stored_manifest: StoredManifest,
-}
-
-#[derive(Debug)]
-pub enum GameDownloadError {
- Communication(RemoteAccessError),
- Checksum,
- Setup(SetupError),
- Lock,
- IoError(io::Error),
- DownloadError,
-}
-
-#[derive(Debug)]
-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: Mutex::new(Vec::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.lock().unwrap() = self.stored_manifest.get_completed_contexts();
-
- info!(
- "Completed contexts: {:?}",
- *self.completed_contexts.lock().unwrap()
- );
-
- 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(&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(Mutex::new(Vec::new()));
- let completed_indexes_loop_arc = completed_indexes.clone();
-
- pool.scope(move |scope| {
- let completed_lock = self.completed_contexts.lock().unwrap();
-
- for (index, context) in self.contexts.iter().enumerate() {
- 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 completed_lock.contains(&index) {
- progress_handle.add(context.length);
- continue;
- }
-
- let context = context.clone();
- let control_flag = self.control_flag.clone(); // Clone arcs
- let completed_indexes_ref = completed_indexes_loop_arc.clone();
-
- scope.spawn(move |_| {
- match download_game_chunk(context.clone(), control_flag, progress_handle) {
- Ok(res) => {
- if res {
- let mut lock = completed_indexes_ref.lock().unwrap();
- lock.push(index);
- }
- }
- Err(e) => {
- error!("GameDownloadError: {}", e);
- self.sender.send(DownloadManagerSignal::Error(e)).unwrap();
- }
- }
- });
- }
- });
-
- let completed_lock_len = {
- let mut completed_lock = self.completed_contexts.lock().unwrap();
- let newly_completed_lock = completed_indexes.lock().unwrap();
-
- completed_lock.extend(newly_completed_lock.iter());
-
- completed_lock.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);
- 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
deleted file mode 100644
index d3ca2e7..0000000
--- a/src-tauri/src/downloads/download_commands.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-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/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs
deleted file mode 100644
index 9d2ab9d..0000000
--- a/src-tauri/src/downloads/download_manager_builder.rs
+++ /dev/null
@@ -1,417 +0,0 @@
-use std::{
- collections::HashMap,
- 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, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData},
- 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);
-
- self.app_handle
- .emit(
- &format!("update_game/{}", id),
- GameUpdateEvent {
- game_id: id,
- status,
- },
- )
- .unwrap();
- }
-
- fn push_manager_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 event_data = QueueUpdateEvent { queue: queue_objs };
- 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 sync_download_agent(&self) {}
-
- fn remove_and_cleanup_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::Update => {
- self.push_manager_update();
- }
- DownloadManagerSignal::Finish => {
- self.stop_and_wait_current_download();
- return Ok(());
- }
- DownloadManagerSignal::Remove(game_id) => {
- self.manage_remove_game(game_id);
- }
- };
- }
- }
-
- fn manage_remove_game(&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()).unwrap();
- 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_manager_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_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::Update).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");
- 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::Update).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::Update).unwrap();
- }
- fn manage_error_signal(&mut self, error: GameDownloadError) {
- let current_status = self.current_download_agent.clone().unwrap();
-
- self.remove_and_cleanup_game(¤t_status.id); // Remove all the locks and shit
-
- 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::Update).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/downloads/mod.rs b/src-tauri/src/downloads/mod.rs
deleted file mode 100644
index 023b2c7..0000000
--- a/src-tauri/src/downloads/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-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;
\ No newline at end of file
diff --git a/src-tauri/src/error/application_download_error.rs b/src-tauri/src/error/application_download_error.rs
new file mode 100644
index 0000000..fb6035a
--- /dev/null
+++ b/src-tauri/src/error/application_download_error.rs
@@ -0,0 +1,30 @@
+use std::{
+ fmt::{Display, Formatter},
+ io,
+};
+
+use super::{remote_access_error::RemoteAccessError, setup_error::SetupError};
+
+// TODO: Rename / separate from downloads
+#[derive(Debug, Clone)]
+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
new file mode 100644
index 0000000..ab42263
--- /dev/null
+++ b/src-tauri/src/error/drop_server_error.rs
@@ -0,0 +1,10 @@
+use serde::Deserialize;
+
+#[derive(Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct DropServerError {
+ pub status_code: usize,
+ pub status_message: String,
+ pub message: String,
+ pub url: String,
+}
diff --git a/src-tauri/src/error/library_error.rs b/src-tauri/src/error/library_error.rs
new file mode 100644
index 0000000..4957ae9
--- /dev/null
+++ b/src-tauri/src/error/library_error.rs
@@ -0,0 +1,16 @@
+use std::fmt::Display;
+
+pub enum LibraryError {
+ MetaNotFound(String),
+}
+impl Display for LibraryError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ LibraryError::MetaNotFound(id) => write!(
+ f,
+ "Could not locate any installed version of game ID {} in the database",
+ id
+ ),
+ }
+ }
+}
diff --git a/src-tauri/src/error/mod.rs b/src-tauri/src/error/mod.rs
new file mode 100644
index 0000000..a38a696
--- /dev/null
+++ b/src-tauri/src/error/mod.rs
@@ -0,0 +1,7 @@
+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;
+pub mod user_error;
diff --git a/src-tauri/src/error/process_error.rs b/src-tauri/src/error/process_error.rs
new file mode 100644
index 0000000..8a105a7
--- /dev/null
+++ b/src-tauri/src/error/process_error.rs
@@ -0,0 +1,28 @@
+use std::{fmt::Display, io::Error};
+
+pub enum ProcessError {
+ SetupRequired,
+ NotInstalled,
+ AlreadyRunning,
+ NotDownloaded,
+ InvalidID,
+ InvalidVersion,
+ IOError(Error),
+ InvalidPlatform,
+}
+
+impl Display for ProcessError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let s = match self {
+ ProcessError::SetupRequired => "Game not set up",
+ ProcessError::NotInstalled => "Game not installed",
+ ProcessError::AlreadyRunning => "Game already running",
+ ProcessError::NotDownloaded => "Game not downloaded",
+ ProcessError::InvalidID => "Invalid Game ID",
+ ProcessError::InvalidVersion => "Invalid Game version",
+ ProcessError::IOError(error) => &error.to_string(),
+ ProcessError::InvalidPlatform => "This Game cannot be played on the current platform",
+ };
+ write!(f, "{}", s)
+ }
+}
diff --git a/src-tauri/src/error/remote_access_error.rs b/src-tauri/src/error/remote_access_error.rs
new file mode 100644
index 0000000..40740b2
--- /dev/null
+++ b/src-tauri/src/error/remote_access_error.rs
@@ -0,0 +1,68 @@
+use std::{
+ error::Error,
+ fmt::{Display, Formatter},
+ sync::Arc,
+};
+
+use http::StatusCode;
+use url::ParseError;
+
+use super::drop_server_error::DropServerError;
+
+#[derive(Debug, Clone)]
+pub enum RemoteAccessError {
+ FetchError(Arc),
+ ParsingError(ParseError),
+ InvalidEndpoint,
+ HandshakeFailed(String),
+ GameNotFound,
+ InvalidResponse(DropServerError),
+ InvalidRedirect,
+ ManifestDownloadFailed(StatusCode, String),
+ OutOfSync,
+ Generic(String),
+}
+
+impl Display for RemoteAccessError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ RemoteAccessError::FetchError(error) => write!(
+ f,
+ "{}: {}",
+ error,
+ error
+ .source()
+ .map(|e| e.to_string())
+ .or_else(|| Some("Unknown error".to_string()))
+ .unwrap()
+ ),
+ RemoteAccessError::ParsingError(parse_error) => {
+ write!(f, "{}", parse_error)
+ }
+ RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
+ RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
+ RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
+ RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message),
+ RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
+ RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
+ f,
+ "Failed to download game manifest: {} {}",
+ status, response
+ ),
+ RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
+ RemoteAccessError::Generic(message) => write!(f, "{}", message),
+ }
+ }
+}
+
+impl From for RemoteAccessError {
+ fn from(err: reqwest::Error) -> Self {
+ RemoteAccessError::FetchError(Arc::new(err))
+ }
+}
+impl From for RemoteAccessError {
+ fn from(err: ParseError) -> Self {
+ RemoteAccessError::ParsingError(err)
+ }
+}
+impl std::error::Error for RemoteAccessError {}
diff --git a/src-tauri/src/error/setup_error.rs b/src-tauri/src/error/setup_error.rs
new file mode 100644
index 0000000..bae7fea
--- /dev/null
+++ b/src-tauri/src/error/setup_error.rs
@@ -0,0 +1,14 @@
+use std::fmt::{Display, Formatter};
+
+#[derive(Debug, Clone)]
+pub enum SetupError {
+ Context,
+}
+
+impl Display for SetupError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ SetupError::Context => write!(f, "Failed to generate contexts for download"),
+ }
+ }
+}
diff --git a/src-tauri/src/error/user_error.rs b/src-tauri/src/error/user_error.rs
new file mode 100644
index 0000000..bb019fc
--- /dev/null
+++ b/src-tauri/src/error/user_error.rs
@@ -0,0 +1,66 @@
+use std::{
+ fmt::Display,
+ ops::{FromResidual, Try},
+};
+
+use serde::Serialize;
+
+pub enum UserValue
+where
+ T: Serialize,
+ D: Display,
+{
+ Ok(T),
+ Err(D),
+}
+impl Serialize for UserValue {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ match self {
+ UserValue::Ok(data) => data.serialize(serializer),
+ UserValue::Err(err) => serializer.serialize_str(err.to_string().as_ref()),
+ }
+ }
+}
+
+impl From> for UserValue {
+ fn from(value: Result) -> Self {
+ match value {
+ Ok(data) => UserValue::Ok(data),
+ Err(data) => UserValue::Err(data),
+ }
+ }
+}
+
+impl Try for UserValue {
+ type Output = T;
+
+ type Residual = D;
+
+ fn from_output(output: Self::Output) -> Self {
+ Self::Ok(output)
+ }
+
+ fn branch(self) -> std::ops::ControlFlow {
+ match self {
+ UserValue::Ok(data) => std::ops::ControlFlow::Continue(data),
+ UserValue::Err(e) => std::ops::ControlFlow::Break(e),
+ }
+ }
+}
+impl FromResidual for UserValue {
+ fn from_residual(residual: ::Residual) -> Self {
+ UserValue::Err(residual)
+ }
+}
+impl FromResidual> for UserValue {
+ fn from_residual(residual: Result) -> Self {
+ match residual {
+ Ok(_) => unreachable!(),
+ Err(e) => UserValue::Err(e),
+ }
+ }
+}
+
diff --git a/src-tauri/src/games/commands.rs b/src-tauri/src/games/commands.rs
new file mode 100644
index 0000000..beec0bc
--- /dev/null
+++ b/src-tauri/src/games/commands.rs
@@ -0,0 +1,57 @@
+use std::sync::Mutex;
+
+use tauri::AppHandle;
+
+use crate::{
+ error::{
+ library_error::LibraryError, remote_access_error::RemoteAccessError, user_error::UserValue,
+ },
+ 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, GameVersionOption,
+ },
+ state::{GameStatusManager, GameStatusWithTransient},
+};
+
+#[tauri::command]
+pub fn fetch_library(app: AppHandle) -> UserValue, RemoteAccessError> {
+ fetch_library_logic(app).into()
+}
+
+#[tauri::command]
+pub fn fetch_game(
+ game_id: String,
+ app: tauri::AppHandle,
+) -> UserValue {
+ fetch_game_logic(game_id, app).into()
+}
+
+#[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) -> UserValue<(), LibraryError> {
+ let meta = match get_current_meta(&game_id) {
+ Some(data) => data,
+ None => return UserValue::Err(LibraryError::MetaNotFound(game_id)),
+ };
+ println!("{:?}", meta);
+ uninstall_game_logic(meta, &app_handle);
+
+ UserValue::Ok(())
+}
+
+#[tauri::command]
+pub fn fetch_game_verion_options(
+ game_id: String,
+ state: tauri::State<'_, Mutex>,
+) -> UserValue, RemoteAccessError> {
+ fetch_game_verion_options_logic(game_id, state).into()
+}
diff --git a/src-tauri/src/games/downloads/commands.rs b/src-tauri/src/games/downloads/commands.rs
new file mode 100644
index 0000000..d3c604a
--- /dev/null
+++ b/src-tauri/src/games/downloads/commands.rs
@@ -0,0 +1,31 @@
+use std::sync::{mpsc::SendError, Arc, Mutex};
+
+use crate::{
+ download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable},
+ error::user_error::UserValue,
+ 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>,
+) -> UserValue<(), SendError> {
+ let sender = state.lock().unwrap().download_manager.get_sender();
+ let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
+ game_id,
+ game_version,
+ install_dir,
+ sender,
+ )) as Box);
+ state
+ .lock()
+ .unwrap()
+ .download_manager
+ .queue_download(game_download_agent)
+ .into()
+}
diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs
new file mode 100644
index 0000000..a9da4c3
--- /dev/null
+++ b/src-tauri/src/games/downloads/download_agent.rs
@@ -0,0 +1,429 @@
+use crate::auth::generate_authorization_header;
+use crate::database::db::{
+ 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::DB;
+use log::{debug, error, info};
+use rayon::ThreadPoolBuilder;
+use slice_deque::SliceDeque;
+use std::collections::HashMap;
+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 = DB.borrow_data().unwrap();
+ let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
+ drop(db_lock);
+
+ let base_dir_path = Path::new(&base_dir);
+ let data_base_dir_path = base_dir_path.join(id.clone());
+
+ let stored_manifest =
+ StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
+
+ Self {
+ id,
+ version,
+ control_flag,
+ manifest: Mutex::new(None),
+ contexts: 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()?;
+ info!("Ensured manifest exists");
+
+ self.ensure_contexts()?;
+ info!("Ensured contexts exists");
+
+ self.control_flag.set(DownloadThreadControlFlag::Go);
+
+ Ok(())
+ }
+
+ // Blocking
+ pub fn download(&self, app_handle: &AppHandle) -> Result {
+ info!("Setting up download");
+ self.setup_download()?;
+ info!("Setting progress object params");
+ self.set_progress_object_params();
+ info!("Running");
+ let timer = Instant::now();
+ push_game_update(
+ app_handle,
+ &self.metadata().id,
+ (
+ None,
+ Some(ApplicationTransientStatus::Downloading {
+ version_name: self.version.clone(),
+ }),
+ ),
+ );
+ let res = self
+ .run()
+ .map_err(|_| ApplicationDownloadError::DownloadError);
+
+ info!(
+ "{} 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 base_url = DB.fetch_base_url();
+ let manifest_url = base_url
+ .join(
+ format!(
+ "/api/v1/client/game/manifest?id={}&version={}",
+ self.id,
+ encode(&self.version)
+ )
+ .as_str(),
+ )
+ .unwrap();
+
+ let header = generate_authorization_header();
+ let client = reqwest::blocking::Client::new();
+ let response = client
+ .get(manifest_url.to_string())
+ .header("Authorization", header)
+ .send()
+ .unwrap();
+
+ if response.status() != 200 {
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::ManifestDownloadFailed(
+ response.status(),
+ response.text().unwrap(),
+ ),
+ ));
+ }
+
+ let manifest_download: 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();
+
+ 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(&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 = DB.borrow_data().unwrap().settings.max_download_threads;
+
+ info!(
+ "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 base_url = DB.fetch_base_url();
+
+ 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.add(context.length);
+ continue;
+ }
+
+ let sender = self.sender.clone();
+
+ let request = generate_request(&base_url, client, context);
+
+ 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();
+ }
+ }
+ info!("Completed context id {}", index);
+ });
+ }
+ });
+
+ let newly_completed = completed_indexes.to_owned();
+
+ let completed_lock_len = {
+ let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
+ for (item, _) in newly_completed.iter() {
+ completed_contexts_lock.push_front(item);
+ }
+
+ completed_contexts_lock.len()
+ };
+
+ info!("Got newly completed");
+
+ // If we're not out of contexts, we're not done, so we don't fire completed
+ if completed_lock_len != contexts.len() {
+ info!("da for {} exited without completing", self.id.clone());
+ self.stored_manifest
+ .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
+ info!("Setting completed contexts");
+ self.stored_manifest.write();
+ info!("Wrote completed contexts");
+ return Ok(false);
+ }
+
+ info!("Sending completed signal");
+
+ // We've completed
+ self.sender
+ .send(DownloadManagerSignal::Completed(self.metadata()))
+ .unwrap();
+
+ Ok(true)
+ }
+}
+
+fn generate_request(
+ base_url: &url::Url,
+ client: reqwest::blocking::Client,
+ context: &DropDownloadContext,
+) -> reqwest::blocking::RequestBuilder {
+ let chunk_url = base_url
+ .join(&format!(
+ "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
+ // Encode the parts we don't trust
+ context.game_id,
+ encode(&context.version),
+ encode(&context.file_name),
+ context.index
+ ))
+ .unwrap();
+
+ let header = generate_authorization_header();
+
+ client.get(chunk_url).header("Authorization", header)
+}
+
+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();
+ }
+
+ 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/downloads/download_logic.rs b/src-tauri/src/games/downloads/download_logic.rs
similarity index 67%
rename from src-tauri/src/downloads/download_logic.rs
rename to src-tauri/src/games/downloads/download_logic.rs
index 52cd6ba..87f057f 100644
--- a/src-tauri/src/downloads/download_logic.rs
+++ b/src-tauri/src/games/downloads/download_logic.rs
@@ -1,29 +1,23 @@
-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 crate::download_manager::download_thread_control_flag::{
+ DownloadThreadControl, DownloadThreadControlFlag,
+};
+use crate::download_manager::progress_object::ProgressHandle;
+use crate::error::application_download_error::ApplicationDownloadError;
+use crate::error::remote_access_error::RemoteAccessError;
+use crate::games::downloads::manifest::DropDownloadContext;
+use log::{error, warn};
use md5::{Context, Digest};
-use reqwest::blocking::Response;
-use tauri::utils::acl::Permission;
+use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions};
use std::io::Read;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
-use std::thread::sleep;
-use std::time::Duration;
use std::{
fs::{File, OpenOptions},
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,
@@ -68,18 +62,18 @@ impl Seek for DropWriter {
}
}
-pub struct DropDownloadPipeline {
+pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
pub source: R,
pub destination: DropWriter,
- pub control_flag: DownloadThreadControl,
+ pub control_flag: &'a DownloadThreadControl,
pub progress: ProgressHandle,
pub size: usize,
}
-impl DropDownloadPipeline {
+impl<'a> DropDownloadPipeline<'a, Response, File> {
fn new(
source: Response,
destination: DropWriter,
- control_flag: DownloadThreadControl,
+ control_flag: &'a DownloadThreadControl,
progress: ProgressHandle,
size: usize,
) -> Self {
@@ -124,42 +118,26 @@ impl DropDownloadPipeline {
}
pub fn download_game_chunk(
- ctx: DropDownloadContext,
- control_flag: DownloadThreadControl,
+ ctx: &DropDownloadContext,
+ control_flag: &DownloadThreadControl,
progress: ProgressHandle,
-) -> Result {
+ request: RequestBuilder,
+) -> Result {
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0);
return Ok(false);
}
- 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)
+ let response = request
.send()
- .map_err(|e| GameDownloadError::Communication(e.into()))?;
+ .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
- warn!("{}", response.text().unwrap());
- return Err(GameDownloadError::Communication(
- RemoteAccessError::InvalidCodeError(400),
+ let err = response.json().unwrap();
+ warn!("{:?}", err);
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::InvalidResponse(err),
));
}
@@ -173,8 +151,9 @@ pub fn download_game_chunk(
let content_length = response.content_length();
if content_length.is_none() {
- return Err(GameDownloadError::Communication(
- RemoteAccessError::InvalidResponse,
+ warn!("Recieved 0 length content from server");
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::InvalidResponse(response.json().unwrap()),
));
}
@@ -186,7 +165,9 @@ pub fn download_game_chunk(
content_length.unwrap().try_into().unwrap(),
);
- let completed = pipeline.copy().map_err(GameDownloadError::IoError)?;
+ let completed = pipeline
+ .copy()
+ .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
if !completed {
return Ok(false);
};
@@ -195,7 +176,7 @@ pub fn download_game_chunk(
#[cfg(unix)]
{
let permissions = Permissions::from_mode(ctx.permissions);
- set_permissions(ctx.path, permissions).unwrap();
+ set_permissions(ctx.path.clone(), permissions).unwrap();
}
/*
diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/games/downloads/manifest.rs
similarity index 100%
rename from src-tauri/src/downloads/manifest.rs
rename to src-tauri/src/games/downloads/manifest.rs
diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs
new file mode 100644
index 0000000..c9b3cd4
--- /dev/null
+++ b/src-tauri/src/games/downloads/mod.rs
@@ -0,0 +1,5 @@
+pub mod commands;
+pub mod download_agent;
+mod download_logic;
+mod manifest;
+mod stored_manifest;
diff --git a/src-tauri/src/downloads/stored_manifest.rs b/src-tauri/src/games/downloads/stored_manifest.rs
similarity index 85%
rename from src-tauri/src/downloads/stored_manifest.rs
rename to src-tauri/src/games/downloads/stored_manifest.rs
index cd4006d..fdc232b 100644
--- a/src-tauri/src/downloads/stored_manifest.rs
+++ b/src-tauri/src/games/downloads/stored_manifest.rs
@@ -1,12 +1,11 @@
use std::{
- default,
fs::File,
io::{Read, Write},
- path::{Path, PathBuf},
+ path::PathBuf,
sync::Mutex,
};
-use log::{error, info};
+use log::{error, warn};
use serde::{Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
@@ -44,15 +43,13 @@ impl StoredManifest {
}
};
- let manifest = match serde_binary::from_vec::(s, Endian::Little) {
+ match serde_binary::from_vec::(s, Endian::Little) {
Ok(manifest) => manifest,
Err(e) => {
- error!("{}", e);
+ warn!("{}", e);
StoredManifest::new(game_id, game_version, base_path)
}
- };
-
- return manifest;
+ }
}
pub fn write(&self) {
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
@@ -73,8 +70,8 @@ impl StoredManifest {
Err(e) => error!("{}", e),
};
}
- pub fn set_completed_contexts(&self, completed_contexts: &Mutex>) {
- *self.completed_contexts.lock().unwrap() = completed_contexts.lock().unwrap().clone();
+ pub fn set_completed_contexts(&self, completed_contexts: &[usize]) {
+ *self.completed_contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec {
self.completed_contexts.lock().unwrap().clone()
diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs
new file mode 100644
index 0000000..ab4cc9e
--- /dev/null
+++ b/src-tauri/src/games/library.rs
@@ -0,0 +1,381 @@
+use std::fs::remove_dir_all;
+use std::sync::Mutex;
+use std::thread::spawn;
+
+use log::{error, info, warn};
+use serde::{Deserialize, Serialize};
+use tauri::Emitter;
+use tauri::{AppHandle, Manager};
+use urlencoding::encode;
+
+use crate::database::db::GameVersion;
+use crate::database::db::{ApplicationTransientStatus, DatabaseImpls, 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::process::process_manager::Platform;
+use crate::remote::auth::generate_authorization_header;
+use crate::{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: (
+ Option,
+ Option,
+ ),
+}
+
+#[derive(Serialize, Clone)]
+pub struct QueueUpdateEventQueueData {
+ pub meta: DownloadableMetadata,
+ pub status: DownloadStatus,
+ pub progress: f64,
+}
+
+#[derive(serde::Serialize, Clone)]
+pub struct QueueUpdateEvent {
+ pub queue: Vec,
+}
+
+#[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,
+}
+
+pub 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 {
+ 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 = DB.borrow_data_mut().unwrap();
+
+ 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 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 {
+ 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 = DB.borrow_data_mut().unwrap();
+
+ 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 base_url = DB.fetch_base_url();
+
+ let endpoint =
+ base_url.join(format!("/api/v1/client/game/versions?id={}", game_id).as_str())?;
+ let header = generate_authorization_header();
+
+ let client = reqwest::blocking::Client::new();
+ let response = client
+ .get(endpoint.to_string())
+ .header("Authorization", header)
+ .send()?;
+
+ if response.status() != 200 {
+ 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 = DB.borrow_data_mut().unwrap();
+ db_handle
+ .applications
+ .transient_statuses
+ .entry(meta.clone())
+ .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
+
+ push_game_update(
+ app_handle,
+ &meta.id,
+ (None, Some(ApplicationTransientStatus::Uninstalling {})),
+ );
+
+ let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
+ if previous_state.is_none() {
+ info!("uninstall job doesn't have previous state, failing silently");
+ 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 = DB.borrow_data_mut().unwrap();
+ 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);
+ DB.save().unwrap();
+
+ info!("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 {
+ DB.borrow_data()
+ .unwrap()
+ .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
+ let base_url = DB.fetch_base_url();
+ if meta.version.is_none() {
+ return Err(RemoteAccessError::GameNotFound);
+ }
+
+ let endpoint = base_url.join(
+ format!(
+ "/api/v1/client/game/version?id={}&version={}",
+ meta.id,
+ encode(meta.version.as_ref().unwrap())
+ )
+ .as_str(),
+ )?;
+ let header = generate_authorization_header();
+
+ let client = reqwest::blocking::Client::new();
+ let response = client
+ .get(endpoint.to_string())
+ .header("Authorization", header)
+ .send()?;
+
+ let data: GameVersion = response.json()?;
+
+ let mut handle = DB.borrow_data_mut().unwrap();
+ 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);
+ DB.save().unwrap();
+
+ 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 = DB.borrow_data_mut().unwrap();
+ db_handle
+ .applications
+ .game_statuses
+ .insert(meta.id.clone(), status.clone());
+ drop(db_handle);
+ DB.save().unwrap();
+ 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
new file mode 100644
index 0000000..65c5c6b
--- /dev/null
+++ b/src-tauri/src/games/mod.rs
@@ -0,0 +1,4 @@
+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
new file mode 100644
index 0000000..29287db
--- /dev/null
+++ b/src-tauri/src/games/state.rs
@@ -0,0 +1,32 @@
+use crate::{
+ database::db::{ApplicationTransientStatus, GameDownloadStatus},
+ DB,
+};
+
+pub type GameStatusWithTransient = (
+ Option,
+ Option,
+);
+pub struct GameStatusManager {}
+
+impl GameStatusManager {
+ pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
+ let db_lock = DB.borrow_data().unwrap();
+ let 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 6e6dcf5..1a3903e 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,47 +1,61 @@
-mod auth;
-mod db;
-mod downloads;
-mod library;
+#![feature(try_trait_v2)]
+mod database;
+mod games;
+
+mod autostart;
+mod cleanup;
+mod commands;
+mod download_manager;
+mod error;
mod process;
mod remote;
-mod state;
-#[cfg(test)]
-mod tests;
-mod cleanup;
-use crate::db::DatabaseImpls;
-use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect};
+use crate::database::db::DatabaseImpls;
+use autostart::{get_autostart_enabled, toggle_autostart};
use cleanup::{cleanup_and_exit, quit};
-use db::{
- add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface,
- DATA_ROOT_DIR,
+use commands::fetch_state;
+use database::commands::{
+ add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_system_data,
+ update_settings,
};
-use downloads::download_commands::*;
-use downloads::download_manager::DownloadManager;
-use downloads::download_manager_builder::DownloadManagerBuilder;
+use database::db::{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 http::Response;
use http::{header::*, response::Builder as ResponseBuilder};
-use library::{fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, Game};
-use log::{debug, info, LevelFilter};
+use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
-use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
-use process::process_commands::launch_game;
+use process::commands::{kill_game, launch_game};
use process::process_manager::ProcessManager;
-use remote::{gen_drop_url, use_remote};
+use remote::auth::{self, generate_authorization_header, recieve_handshake};
+use remote::commands::{
+ auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote,
+};
use serde::{Deserialize, Serialize};
+use std::path::Path;
use std::sync::Arc;
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
-use tauri::menu::{Menu, MenuItem, MenuItemBuilder, PredefinedMenuItem};
+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 {
@@ -65,7 +79,7 @@ pub struct User {
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct AppState {
+pub struct AppState<'a> {
status: AppStatus,
user: Option,
games: HashMap,
@@ -73,18 +87,10 @@ pub struct AppState {
#[serde(skip_serializing)]
download_manager: Arc,
#[serde(skip_serializing)]
- process_manager: Arc>,
+ process_manager: Arc>>,
}
-#[tauri::command]
-fn fetch_state(state: tauri::State<'_, Mutex>) -> Result {
- let guard = state.lock().unwrap();
- let cloned_state = guard.clone();
- drop(guard);
- Ok(cloned_state)
-}
-
-fn setup(handle: AppHandle) -> AppState {
+fn setup(handle: AppHandle) -> AppState<'static> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
.append(false)
@@ -110,8 +116,8 @@ fn setup(handle: AppHandle) -> AppState {
log4rs::init_config(config).unwrap();
let games = HashMap::new();
- let download_manager = Arc::new(DownloadManagerBuilder::build(handle));
- let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
+ let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
+ let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
debug!("Checking if database is set up");
let is_set_up = DB.database_is_set_up();
@@ -127,7 +133,57 @@ fn setup(handle: AppHandle) -> AppState {
debug!("Database is set up");
+ // TODO: Account for possible failure
let (app_status, user) = auth::setup().unwrap();
+
+ let db_handle = DB.borrow_data().unwrap();
+ let mut missing_games = Vec::new();
+ let statuses = db_handle.applications.game_statuses.clone();
+ drop(db_handle);
+ for (game_id, status) in statuses.into_iter() {
+ match status {
+ database::db::GameDownloadStatus::Remote {} => {}
+ database::db::GameDownloadStatus::SetupRequired {
+ version_name: _,
+ install_dir,
+ } => {
+ let install_dir_path = Path::new(&install_dir);
+ if !install_dir_path.exists() {
+ missing_games.push(game_id);
+ }
+ }
+ database::db::GameDownloadStatus::Installed {
+ version_name: _,
+ install_dir,
+ } => {
+ let install_dir_path = Path::new(&install_dir);
+ if !install_dir_path.exists() {
+ missing_games.push(game_id);
+ }
+ }
+ }
+ }
+
+ info!("detected games missing: {:?}", missing_games);
+
+ 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 {});
+ }
+
+ drop(db_handle);
+
+ info!("finished setup!");
+
+ // Sync autostart state
+ if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
+ warn!("Failed to sync autostart state: {}", e);
+ }
+
AppState {
status: app_status,
user,
@@ -137,12 +193,13 @@ fn setup(handle: AppHandle) -> AppState {
}
}
-
pub static DB: LazyLock = LazyLock::new(DatabaseInterface::set_up_database);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
- let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init());
+ let mut builder = tauri::Builder::default()
+ .plugin(tauri_plugin_os::init())
+ .plugin(tauri_plugin_dialog::init());
#[cfg(desktop)]
#[allow(unused_variables)]
@@ -152,15 +209,20 @@ pub fn run() {
}));
}
- let mut app = builder
+ let app = builder
.plugin(tauri_plugin_deep_link::init())
.invoke_handler(tauri::generate_handler![
// Core utils
fetch_state,
quit,
+ fetch_system_data,
+ // User utils
+ update_settings,
// Auth
auth_initiate,
retry_connect,
+ manual_recieve_handshake,
+ sign_out,
// Remote
use_remote,
gen_drop_url,
@@ -174,15 +236,23 @@ pub fn run() {
fetch_game_verion_options,
// Downloads
download_game,
- move_game_in_queue,
- pause_game_downloads,
- resume_game_downloads,
+ move_download_in_queue,
+ pause_downloads,
+ resume_downloads,
cancel_game,
+ uninstall_game,
// Processes
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"]),
+ ))
.setup(|app| {
let handle = app.handle().clone();
let state = setup(handle);
@@ -253,6 +323,27 @@ pub fn run() {
.build(app)
.expect("error while setting up tray menu");
+ {
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ if let Some(original) = db_handle.prev_database.take() {
+ warn!(
+ "Database corrupted. Original file at {}",
+ original
+ .canonicalize()
+ .unwrap()
+ .to_string_lossy()
+ .to_string()
+ );
+ 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| {
@@ -272,8 +363,16 @@ pub fn run() {
let response = client
.get(object_url.to_string())
.header("Authorization", header)
- .send()
- .unwrap();
+ .send();
+ if response.is_err() {
+ warn!(
+ "failed to fetch object with error: {}",
+ response.err().unwrap()
+ );
+ responder.respond(Response::builder().status(500).body(Vec::new()).unwrap());
+ return;
+ }
+ let response = response.unwrap();
let resp_builder = ResponseBuilder::new().header(
CONTENT_TYPE,
@@ -284,22 +383,20 @@ pub fn run() {
responder.respond(resp);
})
- .on_window_event(|window, event| match event {
- WindowEvent::CloseRequested { api, .. } => {
+ .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| match event {
- RunEvent::ExitRequested { code, api, .. } => {
+ 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
deleted file mode 100644
index 6e8536e..0000000
--- a/src-tauri/src/library.rs
+++ /dev/null
@@ -1,302 +0,0 @@
-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, GameTransientStatus};
-use crate::downloads::download_manager::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: (Option, Option),
-}
-
-#[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,
-}
-
-// 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 {
- let status = GameStatusManager::fetch_state(&id);
-
- Ok(status)
-}
-
-fn fetch_game_verion_options_logic<'a>(
- game_id: String,
- state: tauri::State<'_, Mutex>,
-) -> Result, RemoteAccessError> {
- let base_url = DB.fetch_base_url();
-
- let endpoint =
- base_url.join(format!("/api/v1/client/metadata/versions?id={}", game_id).as_str())?;
- let header = generate_authorization_header();
-
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(endpoint.to_string())
- .header("Authorization", header)
- .send()?;
-
- if response.status() != 200 {
- return Err(RemoteAccessError::InvalidCodeError(
- response.status().into(),
- ));
- }
-
- let data = response.json::>()?;
-
- let state_lock = state.lock().unwrap();
- let process_manager_lock = state_lock.process_manager.lock().unwrap();
- let data = data
- .into_iter()
- .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
- .collect::>();
- drop(process_manager_lock);
- drop(state_lock);
-
- Ok(data)
-}
-
-#[tauri::command]
-pub fn fetch_game_verion_options<'a>(
- game_id: String,
- state: tauri::State<'_, Mutex>,
-) -> Result, String> {
- fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string())
-}
-
-pub fn on_game_complete(
- game_id: String,
- version_name: String,
- install_dir: String,
- app_handle: &AppHandle,
-) -> Result<(), RemoteAccessError> {
- // Fetch game version information from remote
- let base_url = DB.fetch_base_url();
-
- let endpoint = base_url.join(
- format!(
- "/api/v1/client/metadata/version?id={}&version={}",
- game_id,
- encode(&version_name)
- )
- .as_str(),
- )?;
- let header = generate_authorization_header();
-
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(endpoint.to_string())
- .header("Authorization", header)
- .send()?;
-
- let data = response.json::()?;
-
- let mut handle = DB.borrow_data_mut().unwrap();
- handle
- .games
- .versions
- .entry(game_id.clone())
- .or_default()
- .insert(version_name.clone(), data.clone());
- drop(handle);
- DB.save().unwrap();
-
- let status = if data.setup_command.is_empty() {
- GameStatus::Installed {
- version_name,
- install_dir,
- }
- } else {
- GameStatus::SetupRequired {
- version_name,
- install_dir,
- }
- };
-
- let mut db_handle = DB.borrow_data_mut().unwrap();
- db_handle
- .games
- .statuses
- .insert(game_id.clone(), status.clone());
- drop(db_handle);
- DB.save().unwrap();
- app_handle
- .emit(
- &format!("update_game/{}", game_id),
- GameUpdateEvent {
- game_id,
- status: (Some(status), None),
- },
- )
- .unwrap();
-
- Ok(())
-}
diff --git a/src-tauri/src/process/commands.rs b/src-tauri/src/process/commands.rs
new file mode 100644
index 0000000..0d7492e
--- /dev/null
+++ b/src-tauri/src/process/commands.rs
@@ -0,0 +1,44 @@
+use std::sync::Mutex;
+
+use crate::{
+ error::{process_error::ProcessError, user_error::UserValue},
+ AppState, DB,
+};
+
+#[tauri::command]
+pub fn launch_game(
+ id: String,
+ state: tauri::State<'_, Mutex>,
+) -> UserValue<(), ProcessError> {
+ let state_lock = state.lock().unwrap();
+ let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
+
+ //let meta = DownloadableMetadata {
+ // id,
+ // version: Some(version),
+ // download_type: DownloadType::Game,
+ //};
+
+ match process_manager_lock.launch_process(id) {
+ Ok(_) => {}
+ Err(e) => return UserValue::Err(e),
+ };
+
+ drop(process_manager_lock);
+ drop(state_lock);
+
+ UserValue::Ok(())
+}
+
+#[tauri::command]
+pub fn kill_game(
+ game_id: String,
+ state: tauri::State<'_, Mutex>,
+) -> UserValue<(), ProcessError> {
+ let state_lock = state.lock().unwrap();
+ let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
+ process_manager_lock
+ .kill_game(game_id)
+ .map_err(ProcessError::IOError)
+ .into()
+}
diff --git a/src-tauri/src/process/compat.rs b/src-tauri/src/process/compat.rs
new file mode 100644
index 0000000..2b4f307
--- /dev/null
+++ b/src-tauri/src/process/compat.rs
@@ -0,0 +1,13 @@
+// Since this code isn't being used, we can either:
+// 1. Delete the entire file if compatibility features are not planned
+// 2. Or add a TODO comment if planning to implement later
+
+// Option 1: Delete the file
+// Delete src-tauri/src/process/compat.rs
+
+// Option 2: Add TODO comment
+/*
+TODO: Compatibility layer for running Windows games on Linux
+This module is currently unused but reserved for future implementation
+of Windows game compatibility features on Linux.
+*/
diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs
index a4ceb45..6a1aed5 100644
--- a/src-tauri/src/process/mod.rs
+++ b/src-tauri/src/process/mod.rs
@@ -1,2 +1,3 @@
+pub mod commands;
+pub mod compat;
pub mod process_manager;
-pub mod process_commands;
\ No newline at end of file
diff --git a/src-tauri/src/process/process_commands.rs b/src-tauri/src/process/process_commands.rs
deleted file mode 100644
index 327728f..0000000
--- a/src-tauri/src/process/process_commands.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use std::sync::Mutex;
-
-use crate::AppState;
-
-#[tauri::command]
-pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex>) -> Result<(), String> {
- let state_lock = state.lock().unwrap();
- let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
-
- process_manager_lock.launch_game(game_id)?;
-
- drop(process_manager_lock);
- drop(state_lock);
-
- Ok(())
-}
diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs
index 5266849..f53ea20 100644
--- a/src-tauri/src/process/process_manager.rs
+++ b/src-tauri/src/process/process_manager.rs
@@ -1,28 +1,37 @@
use std::{
collections::HashMap,
fs::{File, OpenOptions},
- io::{Stdout, Write},
+ io::{self, Error},
path::{Path, PathBuf},
- process::{Child, Command},
- sync::LazyLock,
+ process::{Child, Command, ExitStatus},
+ sync::{Arc, Mutex},
+ thread::spawn,
};
-use log::info;
+use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
+use shared_child::SharedChild;
+use tauri::{AppHandle, Manager};
+use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{
- db::{GameStatus, DATA_ROOT_DIR},
- DB,
+ database::db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
+ download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
+ error::process_error::ProcessError,
+ games::{library::push_game_update, state::GameStatusManager},
+ AppState, DB,
};
-pub struct ProcessManager {
+pub struct ProcessManager<'a> {
current_platform: Platform,
log_output_dir: PathBuf,
- processes: HashMap,
+ processes: HashMap>,
+ app_handle: AppHandle,
+ game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
}
-impl ProcessManager {
- pub fn new() -> Self {
+impl ProcessManager<'_> {
+ pub fn new(app_handle: AppHandle) -> Self {
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
let log_output_dir = root_dir_lock.join("logs");
drop(root_dir_lock);
@@ -34,119 +43,340 @@ impl ProcessManager {
Platform::Linux
},
+ app_handle,
processes: HashMap::new(),
log_output_dir,
+ game_launchers: HashMap::from([
+ // Current platform to target platform
+ (
+ (Platform::Windows, Platform::Windows),
+ &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
+ ),
+ (
+ (Platform::Linux, Platform::Linux),
+ &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
+ ),
+ (
+ (Platform::Linux, Platform::Windows),
+ &UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
+ ),
+ ]),
}
}
- fn process_command(&self, install_dir: &String, raw_command: String) -> (String, Vec) {
- let command_components = raw_command.split(" ").collect::>();
- let root = command_components[0].to_string();
+ // There's no easy way to distinguish between an executable name with
+ // spaces and it's arguments.
+ // I think if we just join the install_dir to whatever the user provides us, we'll be alright
+ // In future, we should have a separate field for executable name and it's arguments
+ fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec) {
+ let root = raw_command;
let install_dir = Path::new(install_dir);
let absolute_exe = install_dir.join(root);
+ /*
let args = command_components[1..]
- .into_iter()
+ .iter()
.map(|v| v.to_string())
.collect();
- (absolute_exe.to_str().unwrap().to_owned(), args)
+ */
+ (absolute_exe, Vec::new())
+ }
+ pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> {
+ return match self.processes.get(&game_id) {
+ Some(child) => {
+ child.kill()?;
+ child.wait()?;
+ Ok(())
+ }
+ None => Err(io::Error::new(
+ io::ErrorKind::NotFound,
+ "Game ID not running",
+ )),
+ };
+ }
+
+ fn on_process_finish(&mut self, game_id: String, result: Result) {
+ if !self.processes.contains_key(&game_id) {
+ warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
+ return;
+ }
+
+ debug!("process for {:?} exited with {:?}", &game_id, result);
+
+ self.processes.remove(&game_id);
+
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ let meta = db_handle
+ .applications
+ .installed_game_version
+ .get(&game_id)
+ .cloned()
+ .unwrap();
+ db_handle.applications.transient_statuses.remove(&meta);
+
+ let current_state = db_handle.applications.game_statuses.get(&game_id).cloned();
+ if let Some(saved_state) = current_state {
+ if let GameDownloadStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } = saved_state
+ {
+ if let Ok(exit_code) = result {
+ if exit_code.success() {
+ db_handle.applications.game_statuses.insert(
+ game_id.clone(),
+ GameDownloadStatus::Installed {
+ version_name: version_name.to_string(),
+ install_dir: install_dir.to_string(),
+ },
+ );
+ }
+ }
+ }
+ }
+ drop(db_handle);
+
+ let status = GameStatusManager::fetch_state(&game_id);
+
+ push_game_update(&self.app_handle, &game_id, status);
+
+ // TODO better management
}
pub fn valid_platform(&self, platform: &Platform) -> Result {
let current = &self.current_platform;
- let valid_platforms = PROCESS_COMPATABILITY_MATRIX
- .get(current)
- .ok_or("Incomplete platform compatability matrix.")?;
-
- Ok(valid_platforms.contains(platform))
+ Ok(self
+ .game_launchers
+ .contains_key(&(current.clone(), platform.clone())))
}
- pub fn launch_game(&mut self, game_id: String) -> Result<(), String> {
+ pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> {
if self.processes.contains_key(&game_id) {
- return Err("Game or setup is already running.".to_owned());
+ return Err(ProcessError::AlreadyRunning);
}
- let db_lock = DB.borrow_data().unwrap();
- let game_status = db_lock
- .games
- .statuses
+ let version = match DB
+ .borrow_data()
+ .unwrap()
+ .applications
+ .game_statuses
.get(&game_id)
- .ok_or("Game not installed")?;
-
- let GameStatus::Installed {
- version_name,
- install_dir,
- } = game_status
- else {
- return Err("Game not installed.".to_owned());
+ .cloned()
+ {
+ Some(GameDownloadStatus::Installed { version_name, .. }) => version_name,
+ Some(GameDownloadStatus::SetupRequired { .. }) => {
+ return Err(ProcessError::SetupRequired).into()
+ }
+ _ => return Err(ProcessError::NotInstalled).into(),
+ };
+ let meta = DownloadableMetadata {
+ id: game_id.clone(),
+ version: Some(version.clone()),
+ download_type: DownloadType::Game,
};
- let game_version = db_lock
- .games
- .versions
+ let mut db_lock = DB.borrow_data_mut().unwrap();
+ debug!(
+ "Launching process {:?} with games {:?}",
+ &game_id, db_lock.applications.game_versions
+ );
+
+ let game_status = db_lock
+ .applications
+ .game_statuses
.get(&game_id)
- .ok_or("Invalid game ID".to_owned())?
+ .ok_or(ProcessError::NotInstalled)?;
+
+ let status_metadata: Option<(&String, &String)> = match game_status {
+ GameDownloadStatus::Installed {
+ version_name,
+ install_dir,
+ } => Some((&version_name, &install_dir)),
+ GameDownloadStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ _ => None,
+ };
+
+ if status_metadata.is_none() {
+ return Err(ProcessError::NotDownloaded);
+ }
+
+ let (version_name, install_dir) = status_metadata.unwrap();
+
+ let game_version = db_lock
+ .applications
+ .game_versions
+ .get(&game_id)
+ .ok_or(ProcessError::InvalidID)?
.get(version_name)
- .ok_or("Invalid version name".to_owned())?;
+ .ok_or(ProcessError::InvalidVersion)?;
- let (command, args) =
- self.process_command(install_dir, game_version.launch_command.clone());
+ let raw_command: String = match game_status {
+ GameDownloadStatus::Installed {
+ version_name: _,
+ install_dir: _,
+ } => game_version.launch_command.clone(),
+ GameDownloadStatus::SetupRequired {
+ version_name: _,
+ install_dir: _,
+ } => game_version.setup_command.clone(),
+ _ => panic!("unreachable code"),
+ };
- info!("launching process {} in {}", command, install_dir);
+ let (command, args) = self.process_command(install_dir, raw_command);
+
+ let target_current_dir = command.parent().unwrap().to_str().unwrap();
+
+ info!(
+ "launching process {} in {}",
+ command.to_str().unwrap(),
+ target_current_dir
+ );
let current_time = chrono::offset::Local::now();
- let mut log_file = OpenOptions::new()
+ let log_file = OpenOptions::new()
.write(true)
.truncate(true)
.read(true)
.create(true)
- .open(
- self.log_output_dir
- .join(format!("{}-{}.log", game_id, current_time.timestamp())),
- )
- .map_err(|v| v.to_string())?;
+ .open(self.log_output_dir.join(format!(
+ "{}-{}-{}.log",
+ &game_id,
+ &version,
+ current_time.timestamp()
+ )))
+ .map_err(ProcessError::IOError)?;
- let mut error_file = OpenOptions::new()
+ let error_file = OpenOptions::new()
.write(true)
.truncate(true)
.read(true)
.create(true)
- .open(
- self.log_output_dir
- .join(format!("{}-{}-error.log", game_id, current_time.timestamp())),
+ .open(self.log_output_dir.join(format!(
+ "{}-{}-{}-error.log",
+ &game_id,
+ &version,
+ current_time.timestamp()
+ )))
+ .map_err(ProcessError::IOError)?;
+
+ let current_platform = self.current_platform.clone();
+ let target_platform = game_version.platform.clone();
+
+ let game_launcher = self
+ .game_launchers
+ .get(&(current_platform, target_platform))
+ .ok_or(ProcessError::InvalidPlatform)?;
+
+ let launch_process = game_launcher
+ .launch_process(
+ &meta,
+ command.to_str().unwrap().to_owned(),
+ args,
+ target_current_dir,
+ log_file,
+ error_file,
)
- .map_err(|v| v.to_string())?;
+ .map_err(ProcessError::IOError)?;
- info!("opened log file for {}", command);
+ let launch_process_handle =
+ Arc::new(SharedChild::new(launch_process).map_err(ProcessError::IOError)?);
- let launch_process = Command::new(command)
- .current_dir(install_dir)
- .stdout(log_file)
- .stderr(error_file)
- .args(args)
- .spawn()
- .map_err(|v| v.to_string())?;
+ db_lock
+ .applications
+ .transient_statuses
+ .insert(meta.clone(), ApplicationTransientStatus::Running {});
- self.processes.insert(game_id, launch_process);
+ push_game_update(
+ &self.app_handle,
+ &meta.id,
+ (None, Some(ApplicationTransientStatus::Running {})),
+ );
+
+ let wait_thread_handle = launch_process_handle.clone();
+ let wait_thread_apphandle = self.app_handle.clone();
+ let wait_thread_game_id = meta.clone();
+
+ spawn(move || {
+ let result: Result = launch_process_handle.wait();
+
+ let app_state = wait_thread_apphandle.state::>();
+ let app_state_handle = app_state.lock().unwrap();
+
+ let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
+ process_manager_handle.on_process_finish(wait_thread_game_id.id, result);
+
+ // As everything goes out of scope, they should get dropped
+ // But just to explicit about it
+ drop(process_manager_handle);
+ drop(app_state_handle);
+ });
+
+ self.processes.insert(meta.id, wait_thread_handle);
+
+ info!("finished spawning process");
Ok(())
}
}
-#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)]
+#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)]
pub enum Platform {
Windows,
Linux,
}
-pub type ProcessCompatabilityMatrix = HashMap>;
-pub static PROCESS_COMPATABILITY_MATRIX: LazyLock =
- LazyLock::new(|| {
- let mut matrix: ProcessCompatabilityMatrix = HashMap::new();
+pub trait ProcessHandler: Send + 'static {
+ fn launch_process(
+ &self,
+ meta: &DownloadableMetadata,
+ command: String,
+ args: Vec,
+ current_dir: &str,
+ log_file: File,
+ error_file: File,
+ ) -> Result;
+}
- matrix.insert(Platform::Windows, vec![Platform::Windows]);
- matrix.insert(Platform::Linux, vec![Platform::Linux]); // TODO: add Proton support
+struct NativeGameLauncher;
+impl ProcessHandler for NativeGameLauncher {
+ fn launch_process(
+ &self,
+ _meta: &DownloadableMetadata,
+ command: String,
+ args: Vec,
+ current_dir: &str,
+ log_file: File,
+ error_file: File,
+ ) -> Result {
+ Command::new(command)
+ .current_dir(current_dir)
+ .stdout(log_file)
+ .stderr(error_file)
+ .args(args)
+ .spawn()
+ }
+}
- return matrix;
- });
+const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
+struct UMULauncher;
+impl ProcessHandler for UMULauncher {
+ fn launch_process(
+ &self,
+ _meta: &DownloadableMetadata,
+ command: String,
+ args: Vec,
+ _current_dir: &str,
+ _log_file: File,
+ _error_file: File,
+ ) -> Result {
+ UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
+ .game_id(String::from("0"))
+ .launch_args(args)
+ .build()
+ .spawn()
+ }
+}
diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs
deleted file mode 100644
index 33655c2..0000000
--- a/src-tauri/src/remote.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-use std::{
- fmt::{Display, Formatter},
- sync::{Arc, Mutex},
-};
-
-use http::StatusCode;
-use log::{info, warn};
-use serde::Deserialize;
-use url::{ParseError, Url};
-
-use crate::{AppState, AppStatus, DB};
-
-#[derive(Debug, Clone)]
-pub enum RemoteAccessError {
- FetchError(Arc),
- ParsingError(ParseError),
- InvalidCodeError(u16),
- InvalidEndpoint,
- HandshakeFailed,
- GameNotFound,
- InvalidResponse,
- InvalidRedirect,
- ManifestDownloadFailed(StatusCode, String),
-}
-
-impl Display for RemoteAccessError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- RemoteAccessError::FetchError(error) => write!(f, "{}", error),
- RemoteAccessError::ParsingError(parse_error) => {
- write!(f, "{}", parse_error)
- }
- RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error),
- RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
- RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"),
- RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
- RemoteAccessError::InvalidResponse => write!(f, "Server returned an invalid response"),
- RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
- RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
- f,
- "Failed to download game manifest: {} {}",
- status, response
- ),
- }
- }
-}
-
-impl From for RemoteAccessError {
- fn from(err: reqwest::Error) -> Self {
- RemoteAccessError::FetchError(Arc::new(err))
- }
-}
-impl From for RemoteAccessError {
- fn from(err: ParseError) -> Self {
- RemoteAccessError::ParsingError(err)
- }
-}
-impl From for RemoteAccessError {
- fn from(err: u16) -> Self {
- RemoteAccessError::InvalidCodeError(err)
- }
-}
-
-impl std::error::Error for RemoteAccessError {}
-
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct DropHealthcheck {
- app_name: String,
-}
-
-async fn use_remote_logic<'a>(
- url: String,
- state: tauri::State<'_, Mutex>,
-) -> Result<(), RemoteAccessError> {
- info!("connecting to url {}", url);
- let base_url = Url::parse(&url)?;
-
- // Test Drop url
- let test_endpoint = base_url.join("/api/v1")?;
- let response = reqwest::get(test_endpoint.to_string()).await?;
-
- let result = response.json::().await?;
-
- if result.app_name != "Drop" {
- warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
- return Err(RemoteAccessError::InvalidEndpoint);
- }
-
- let mut app_state = state.lock().unwrap();
- app_state.status = AppStatus::SignedOut;
- drop(app_state);
-
- let mut db_state = DB.borrow_data_mut().unwrap();
- db_state.base_url = base_url.to_string();
- drop(db_state);
-
- DB.save().unwrap();
-
- Ok(())
-}
-
-#[tauri::command]
-pub async fn use_remote<'a>(
- url: String,
- state: tauri::State<'_, Mutex>,
-) -> Result<(), String> {
- let result = use_remote_logic(url, state).await;
-
- if result.is_err() {
- return Err(result.err().unwrap().to_string());
- }
-
- Ok(())
-}
-
-#[tauri::command]
-pub fn gen_drop_url(path: String) -> Result {
- let base_url = {
- let handle = DB.borrow_data().unwrap();
-
- if handle.base_url.is_empty() {
- return Ok("".to_string());
- };
-
- Url::parse(&handle.base_url).unwrap()
- };
-
- let url = base_url.join(&path).unwrap();
-
- Ok(url.to_string())
-}
diff --git a/src-tauri/src/auth.rs b/src-tauri/src/remote/auth.rs
similarity index 69%
rename from src-tauri/src/auth.rs
rename to src-tauri/src/remote/auth.rs
index 0f1010f..afd0c9a 100644
--- a/src-tauri/src/auth.rs
+++ b/src-tauri/src/remote/auth.rs
@@ -1,19 +1,15 @@
-use std::{
- env,
- sync::Mutex,
- time::{SystemTime, UNIX_EPOCH},
-};
+use std::{env, sync::Mutex};
use chrono::Utc;
-use log::{info, warn};
+use log::{debug, error, 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::{
- db::{DatabaseAuth, DatabaseImpls},
- remote::RemoteAccessError,
+ database::db::{DatabaseAuth, DatabaseImpls},
+ error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User, DB,
};
@@ -39,6 +35,7 @@ struct HandshakeResponse {
id: String,
}
+// TODO: Change return value on Err
pub fn sign_nonce(private_key: String, nonce: String) -> Result {
let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap();
let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap();
@@ -76,22 +73,27 @@ pub fn fetch_user() -> Result {
.get(endpoint.to_string())
.header("Authorization", header)
.send()?;
-
if response.status() != 200 {
- info!("Could not fetch user: {}", response.text().unwrap());
- return Err(RemoteAccessError::InvalidCodeError(0));
+ let err: DropServerError = response.json().unwrap();
+ warn!("{:?}", err);
+
+ if err.status_message == "Nonce expired" {
+ return Err(RemoteAccessError::OutOfSync);
+ }
+
+ return Err(RemoteAccessError::InvalidResponse(err));
}
- let user = response.json::()?;
-
- Ok(user)
+ response.json::().map_err(|e| e.into())
}
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
let path_chunks: Vec<&str> = path.split("/").collect();
if path_chunks.len() != 3 {
app.emit("auth/failed", ()).unwrap();
- return Err(RemoteAccessError::InvalidResponse);
+ return Err(RemoteAccessError::HandshakeFailed(
+ "failed to parse token".to_string(),
+ ));
}
let base_url = {
@@ -109,8 +111,8 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = reqwest::blocking::Client::new();
let response = client.post(endpoint).json(&body).send()?;
- info!("{}", response.status().as_u16());
- let response_struct = response.json::