Compare commits

..

21 Commits

Author SHA1 Message Date
a5082ee2f3 chore: Ran cargo fix & cargo fmt
Signed-off-by: quexeky <git@quexeky.dev>
2025-07-04 14:36:43 +10:00
c38f1fbad3 feat: Download validation
Signed-off-by: quexeky <git@quexeky.dev>
2025-07-04 14:27:34 +10:00
6a3029473c feat: Resume button and PartiallyInstalled status
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-26 15:57:20 +10:00
8dfb96406f feat: Download resuming
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-22 13:01:18 +10:00
fd61903130 feat: Resume download button
Also added DBWrite and DBRead structs to make database management easier

Signed-off-by: quexeky <git@quexeky.dev>
2025-06-21 12:51:50 +10:00
abf371c9fc fix: Download chunks with wrong indexes
Migrated to using checksums as indexes instead

Signed-off-by: quexeky <git@quexeky.dev>
2025-06-18 09:51:49 +10:00
e8bcc67ed4 chore: Didn't import debug macro
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-12 10:23:57 +10:00
8c403eb8d7 fix: Downloads when resuming would truncate files which had not been finished
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-12 10:23:01 +10:00
47f64a3c68 Merge branch 'develop' into 55-feature-request-game-scanning-for-dropdata-to-import-existing-games 2025-06-10 12:01:46 +10:00
ae04099daa refactor: Rename StoredManifest to DropData
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 09:54:41 +10:00
6d295bd47f fix: Broken README path
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 06:37:14 +10:00
c3ee09af85 fix: Update broken README link in docs
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-07 06:35:57 +10:00
0ce55e12c5 fix: Re-update the user and app status when recieve_handshake is called (#54)
Also enabled assetProtocol for better caching in general

Signed-off-by: quexeky <git@quexeky.dev>
2025-06-06 12:09:44 +10:00
86bce1c68d Release: v0.3.0-rc-3 (#51) 2025-06-06 09:25:44 +10:00
924e4e334c Database not being properly serialised with rpm_serde (#48)
Signed-off-by: quexeky <git@quexeky.dev>
2025-06-05 17:22:22 +10:00
065eb2356a fix: database corrupted on every startup (#40) 2025-06-01 19:53:24 +10:00
689e9ad890 fix: add new dependencies to linux build 2025-05-28 20:51:32 +10:00
7c35ed73aa fix: Folders can now be copied too
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:48:34 +10:00
8f261a5dac chore: Add extract() function
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:48:34 +10:00
67b6f2aa2e chore: Initial path normalisation & parsing with backup generation
Signed-off-by: quexeky <git@quexeky.dev>
2025-05-28 20:47:43 +10:00
11e2b3fe8a fix: regenerate lockfile 2025-05-28 20:37:26 +10:00
49 changed files with 1916 additions and 707 deletions

View File

@ -51,7 +51,7 @@ jobs:
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk2.0-dev libsoup3.0-dev
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2. # webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# You can remove the one that doesn't apply to your app to speed up the workflow a bit. # You can remove the one that doesn't apply to your app to speed up the workflow a bit.

View File

@ -4,7 +4,7 @@ Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It u
## Running ## Running
Before setting up the drop app, be sure that you have a server set up. 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) The instructions for this can be found on the [Drop Docs](https://docs.droposs.org/docs/guides/quickstart)
## Current features ## Current features
Currently supported are the following features: Currently supported are the following features:

View File

@ -1,78 +1,52 @@
<template> <template>
<!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 --> <!-- Do not add scale animations to this: https://stackoverflow.com/a/35683068 -->
<div class="inline-flex divide-x divide-zinc-900"> <div class="inline-flex divide-x divide-zinc-900">
<button <button type="button" @click="() => buttonActions[props.status.type]()" :class="[
type="button" styles[props.status.type],
@click="() => buttonActions[props.status.type]()" showDropdown ? 'rounded-l-md' : 'rounded-md',
:class="[ 'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
styles[props.status.type], ]">
showDropdown ? 'rounded-l-md' : 'rounded-md', <component :is="buttonIcons[props.status.type]" class="-mr-0.5 size-5" aria-hidden="true" />
'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
]"
>
<component
:is="buttonIcons[props.status.type]"
class="-mr-0.5 size-5"
aria-hidden="true"
/>
{{ buttonNames[props.status.type] }} {{ buttonNames[props.status.type] }}
</button> </button>
<Menu <Menu v-if="showDropdown" as="div" class="relative inline-block text-left grow">
v-if="showDropdown"
as="div"
class="relative inline-block text-left grow"
>
<div class="h-full"> <div class="h-full">
<MenuButton <MenuButton :class="[
:class="[ styles[props.status.type],
styles[props.status.type], 'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group',
'inline-flex w-full h-full justify-center items-center rounded-r-md px-1 py-2 text-sm font-semibold shadow-sm group', 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', ]">
]"
>
<ChevronDownIcon class="size-5" aria-hidden="true" /> <ChevronDownIcon class="size-5" aria-hidden="true" />
</MenuButton> </MenuButton>
</div> </div>
<transition <transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95"
enter-active-class="transition ease-out duration-100" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75"
enter-from-class="transform opacity-0 scale-95" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems <MenuItems
class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none" class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none">
>
<div class="py-1"> <div class="py-1">
<MenuItem v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button <button @click="() => emit('options')" :class="[
@click="() => emit('options')" active
:class="[ ? 'bg-zinc-800 text-zinc-100 outline-none'
active : 'text-zinc-400',
? 'bg-zinc-800 text-zinc-100 outline-none' 'w-full block px-4 py-2 text-sm inline-flex justify-between',
: 'text-zinc-400', ]">
'w-full block px-4 py-2 text-sm inline-flex justify-between', Options
]" <Cog6ToothIcon class="size-5" />
> </button>
Options
<Cog6ToothIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
<MenuItem v-slot="{ active }"> <MenuItem v-slot="{ active }">
<button <button @click="() => emit('uninstall')" :class="[
@click="() => emit('uninstall')" active
:class="[ ? 'bg-zinc-800 text-zinc-100 outline-none'
active : 'text-zinc-400',
? 'bg-zinc-800 text-zinc-100 outline-none' 'w-full block px-4 py-2 text-sm inline-flex justify-between',
: 'text-zinc-400', ]">
'w-full block px-4 py-2 text-sm inline-flex justify-between', Uninstall
]" <TrashIcon class="size-5" />
> </button>
Uninstall
<TrashIcon class="size-5" />
</button>
</MenuItem> </MenuItem>
</div> </div>
</MenuItems> </MenuItems>
@ -103,12 +77,14 @@ const emit = defineEmits<{
(e: "uninstall"): void; (e: "uninstall"): void;
(e: "kill"): void; (e: "kill"): void;
(e: "options"): void; (e: "options"): void;
(e: "resume"): void
}>(); }>();
const showDropdown = computed( const showDropdown = computed(
() => () =>
props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.Installed ||
props.status.type === GameStatusEnum.SetupRequired props.status.type === GameStatusEnum.SetupRequired ||
props.status.type === GameStatusEnum.PartiallyInstalled
); );
const styles: { [key in GameStatusEnum]: string } = { const styles: { [key in GameStatusEnum]: string } = {
@ -128,6 +104,8 @@ const styles: { [key in GameStatusEnum]: string } = {
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.Running]: [GameStatusEnum.Running]:
"bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700",
[GameStatusEnum.PartiallyInstalled]:
"bg-gray-600 text-white hover:bg-gray-500 focus-visible:outline-gray-600 hover:bg-gray-500"
}; };
const buttonNames: { [key in GameStatusEnum]: string } = { const buttonNames: { [key in GameStatusEnum]: string } = {
@ -139,6 +117,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = {
[GameStatusEnum.Updating]: "Updating", [GameStatusEnum.Updating]: "Updating",
[GameStatusEnum.Uninstalling]: "Uninstalling", [GameStatusEnum.Uninstalling]: "Uninstalling",
[GameStatusEnum.Running]: "Stop", [GameStatusEnum.Running]: "Stop",
[GameStatusEnum.PartiallyInstalled]: "Resume"
}; };
const buttonIcons: { [key in GameStatusEnum]: Component } = { const buttonIcons: { [key in GameStatusEnum]: Component } = {
@ -150,6 +129,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = {
[GameStatusEnum.Updating]: ArrowDownTrayIcon, [GameStatusEnum.Updating]: ArrowDownTrayIcon,
[GameStatusEnum.Uninstalling]: TrashIcon, [GameStatusEnum.Uninstalling]: TrashIcon,
[GameStatusEnum.Running]: PlayIcon, [GameStatusEnum.Running]: PlayIcon,
[GameStatusEnum.PartiallyInstalled]: ArrowDownTrayIcon
}; };
const buttonActions: { [key in GameStatusEnum]: () => void } = { const buttonActions: { [key in GameStatusEnum]: () => void } = {
@ -159,7 +139,8 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = {
[GameStatusEnum.SetupRequired]: () => emit("launch"), [GameStatusEnum.SetupRequired]: () => emit("launch"),
[GameStatusEnum.Installed]: () => emit("launch"), [GameStatusEnum.Installed]: () => emit("launch"),
[GameStatusEnum.Updating]: () => emit("queue"), [GameStatusEnum.Updating]: () => emit("queue"),
[GameStatusEnum.Uninstalling]: () => {}, [GameStatusEnum.Uninstalling]: () => { },
[GameStatusEnum.Running]: () => emit("kill"), [GameStatusEnum.Running]: () => emit("kill"),
[GameStatusEnum.PartiallyInstalled]: () => emit("resume")
}; };
</script> </script>

View File

@ -1,9 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { data } from "autoprefixer"; import { data } from "autoprefixer";
import { AppStatus, type AppState } from "~/types"; import { AppStatus, type AppState } from "~/types";
export function setupHooks() { export function setupHooks() {
const router = useRouter(); const router = useRouter();
const state = useAppState();
listen("auth/processing", (event) => { listen("auth/processing", (event) => {
router.push("/auth/processing"); router.push("/auth/processing");
@ -15,8 +17,9 @@ export function setupHooks() {
); );
}); });
listen("auth/finished", (event) => { listen("auth/finished", async (event) => {
router.push("/store"); router.push("/store");
state.value = JSON.parse(await invoke("fetch_state"));
}); });
listen("download_error", (event) => { listen("download_error", (event) => {

View File

@ -1,7 +1,7 @@
{ {
"name": "drop-app", "name": "drop-app",
"private": true, "private": true,
"version": "0.3.0-rc-2", "version": "0.3.0-rc-3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",

View File

@ -32,6 +32,7 @@
@uninstall="() => uninstall()" @uninstall="() => uninstall()"
@kill="() => kill()" @kill="() => kill()"
@options="() => (configureModalOpen = true)" @options="() => (configureModalOpen = true)"
@resume="() => resumeDownload()"
:status="status" :status="status"
/> />
<a <a
@ -495,6 +496,7 @@ const currentImageIndex = ref(0);
const configureModalOpen = ref(false); const configureModalOpen = ref(false);
async function installFlow() { async function installFlow() {
installFlowOpen.value = true; installFlowOpen.value = true;
versionOptions.value = undefined; versionOptions.value = undefined;
@ -532,6 +534,15 @@ async function install() {
installLoading.value = false; installLoading.value = false;
} }
async function resumeDownload() {
try {
await invoke("resume_download", { gameId: game.value.id })
}
catch(e) {
console.error(e)
}
}
async function launch() { async function launch() {
try { try {
await invoke("launch_game", { id: game.value.id }); await invoke("launch_game", { id: game.value.id });

View File

@ -91,7 +91,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid"; import { ServerIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { DownloadableMetadata, Game, GameStatus } from "~/types"; import { GameStatusEnum, type DownloadableMetadata, type Game, type GameStatus } from "~/types";
// const actionNames = {
// [GameStatusEnum.Downloading]: "downloading",
// [GameStatusEnum.Verifying]: "verifying",
// }
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
window.addEventListener("resize", (event) => { window.addEventListener("resize", (event) => {

589
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "drop-app" name = "drop-app"
version = "0.3.0-rc-2" version = "0.3.0-rc-3"
description = "The client application for the open-source, self-hosted game distribution platform Drop" description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"] authors = ["Drop OSS"]
edition = "2021" edition = "2021"
@ -27,7 +27,6 @@ tauri-plugin-shell = "2.2.1"
serde_json = "1" serde_json = "1"
serde-binary = "0.5.0" serde-binary = "0.5.0"
rayon = "1.10.0" rayon = "1.10.0"
directories = "5.0.1"
webbrowser = "1.0.2" webbrowser = "1.0.2"
url = "2.5.2" url = "2.5.2"
tauri-plugin-deep-link = "2" tauri-plugin-deep-link = "2"
@ -55,6 +54,18 @@ reqwest-middleware-cache = "0.1.1"
deranged = "=0.4.0" deranged = "=0.4.0"
droplet-rs = "0.7.3" droplet-rs = "0.7.3"
gethostname = "1.0.1" gethostname = "1.0.1"
zstd = "0.13.3"
tar = "0.4.44"
rand = "0.9.1"
regex = "1.11.1"
tempfile = "3.19.1"
schemars = "0.8.22"
sha1 = "0.10.6"
dirs = "6.0.0"
whoami = "1.6.0"
filetime = "0.2.25"
walkdir = "2.5.0"
known-folders = "1.2.0"
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] } native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
# tailscale = { path = "./tailscale" } # tailscale = { path = "./tailscale" }
@ -64,7 +75,7 @@ features = ["curly"]
[dependencies.tauri] [dependencies.tauri]
version = "2.1.1" version = "2.1.1"
features = ["tray-icon"] features = ["protocol-asset", "tray-icon"]
[dependencies.tokio] [dependencies.tokio]
version = "1.40.0" version = "1.40.0"
@ -80,11 +91,7 @@ features = ["fs"]
[dependencies.uuid] [dependencies.uuid]
version = "1.10.0" version = "1.10.0"
features = [ features = ["v4", "fast-rng", "macro-diagnostics"]
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]
[dependencies.rustbreak] [dependencies.rustbreak]
version = "2" version = "2"

View File

@ -1,4 +1,4 @@
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use log::debug; use log::debug;
use tauri::AppHandle; use tauri::AppHandle;
use tauri_plugin_autostart::ManagerExt; use tauri_plugin_autostart::ManagerExt;
@ -17,7 +17,6 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
let mut db_handle = borrow_db_mut_checked(); let mut db_handle = borrow_db_mut_checked();
db_handle.settings.autostart = enabled; db_handle.settings.autostart = enabled;
drop(db_handle); drop(db_handle);
save_db();
Ok(()) Ok(())
} }

View File

@ -1,3 +1,3 @@
pub mod autostart; pub mod autostart;
pub mod cleanup; pub mod cleanup;
pub mod commands; pub mod commands;

View File

@ -0,0 +1,102 @@
use std::{collections::HashMap, path::PathBuf, str::FromStr};
use log::warn;
use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
use super::path::CommonPath;
pub struct BackupManager<'a> {
pub current_platform: Platform,
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
}
impl BackupManager<'_> {
pub fn new() -> Self {
BackupManager {
#[cfg(target_os = "windows")]
current_platform: Platform::Windows,
#[cfg(target_os = "macos")]
current_platform: Platform::MacOs,
#[cfg(target_os = "linux")]
current_platform: Platform::Linux,
sources: HashMap::from([
// Current platform to target platform
(
(Platform::Windows, Platform::Windows),
&WindowsBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
(
(Platform::Linux, Platform::Linux),
&LinuxBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
(
(Platform::MacOs, Platform::MacOs),
&MacBackupManager {} as &(dyn BackupHandler + Sync + Send),
),
]),
}
}
}
pub trait BackupHandler: Send + Sync {
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.lock().unwrap().join("games")) }
fn game_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&game.game_id).unwrap()) }
fn base_translate(&self, path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(self.root_translate(path, game)?.join(self.game_translate(path, game)?)) }
fn home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }
fn store_user_id_translate(&self, _path: &PathBuf, game: &GameVersion) -> Result<PathBuf, BackupError> { PathBuf::from_str(&game.game_id).map_err(|_| BackupError::ParseError) }
fn os_user_name_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::from_str(&whoami::username()).unwrap()) }
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winAppData>"); Err(BackupError::InvalidSystem) }
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppData>"); Err(BackupError::InvalidSystem) }
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winLocalAppDataLow>"); Err(BackupError::InvalidSystem) }
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDocuments>"); Err(BackupError::InvalidSystem) }
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winPublic>"); Err(BackupError::InvalidSystem) }
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winProgramData>"); Err(BackupError::InvalidSystem) }
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected Windows Reference in Backup <winDir>"); Err(BackupError::InvalidSystem) }
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgData>"); Err(BackupError::InvalidSystem) }
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> { warn!("Unexpected XDG Reference in Backup <xdgConfig>"); Err(BackupError::InvalidSystem) }
fn skip_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(PathBuf::new()) }
}
pub struct LinuxBackupManager {}
impl BackupHandler for LinuxBackupManager {
fn xdg_config_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Data.get().ok_or(BackupError::NotFound)?)
}
fn xdg_data_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
}
}
pub struct WindowsBackupManager {}
impl BackupHandler for WindowsBackupManager {
fn win_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Config.get().ok_or(BackupError::NotFound)?)
}
fn win_local_app_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::DataLocal.get().ok_or(BackupError::NotFound)?)
}
fn win_local_app_data_low_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::DataLocalLow.get().ok_or(BackupError::NotFound)?)
}
fn win_dir_translate(&self, _path: &PathBuf,_game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str("C:/Windows").unwrap())
}
fn win_documents_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Document.get().ok_or(BackupError::NotFound)?)
}
fn win_program_data_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(PathBuf::from_str("C:/ProgramData").unwrap())
}
fn win_public_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> {
Ok(CommonPath::Public.get().ok_or(BackupError::NotFound)?)
}
}
pub struct MacBackupManager {}
impl BackupHandler for MacBackupManager {}

View File

@ -0,0 +1,6 @@
use crate::process::process_manager::Platform;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Condition {
Os(Platform)
}

View File

@ -0,0 +1,35 @@
use crate::database::db::GameVersion;
use super::conditions::{Condition};
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CloudSaveMetadata {
pub files: Vec<GameFile>,
pub game_version: GameVersion,
pub save_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GameFile {
pub path: String,
pub id: Option<String>,
pub data_type: DataType,
pub tags: Vec<Tag>,
pub conditions: Vec<Condition>
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub enum DataType {
Registry,
File,
Other
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Tag {
Config,
Save,
#[default]
#[serde(other)]
Other,
}

View File

@ -0,0 +1,7 @@
pub mod conditions;
pub mod metadata;
pub mod resolver;
pub mod placeholder;
pub mod normalise;
pub mod path;
pub mod backup_manager;

View File

@ -0,0 +1,162 @@
use std::sync::LazyLock;
use regex::Regex;
use crate::process::process_manager::Platform;
use super::placeholder::*;
pub fn normalize(path: &str, os: Platform) -> String {
let mut path = path.trim().trim_end_matches(['/', '\\']).replace('\\', "/");
if path == "~" || path.starts_with("~/") {
path = path.replacen('~', HOME, 1);
}
static CONSECUTIVE_SLASHES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/{2,}").unwrap());
static UNNECESSARY_DOUBLE_STAR_1: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^/*])\*{2,}").unwrap());
static UNNECESSARY_DOUBLE_STAR_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\*{2,}([^/*])").unwrap());
static ENDING_WILDCARD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\*)+$").unwrap());
static ENDING_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\.)$").unwrap());
static INTERMEDIATE_DOT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\./)").unwrap());
static BLANK_SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(/\s+/)").unwrap());
static APP_DATA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%appdata%").unwrap());
static APP_DATA_ROAMING: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Roaming").unwrap());
static APP_DATA_LOCAL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%localappdata%").unwrap());
static APP_DATA_LOCAL_2: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/AppData/Local/").unwrap());
static USER_PROFILE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%").unwrap());
static DOCUMENTS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)%userprofile%/Documents").unwrap());
for (pattern, replacement) in [
(&CONSECUTIVE_SLASHES, "/"),
(&UNNECESSARY_DOUBLE_STAR_1, "${1}*"),
(&UNNECESSARY_DOUBLE_STAR_2, "*${1}"),
(&ENDING_WILDCARD, ""),
(&ENDING_DOT, ""),
(&INTERMEDIATE_DOT, "/"),
(&BLANK_SEGMENT, "/"),
(&APP_DATA, WIN_APP_DATA),
(&APP_DATA_ROAMING, WIN_APP_DATA),
(&APP_DATA_LOCAL, WIN_LOCAL_APP_DATA),
(&APP_DATA_LOCAL_2, &format!("{}/", WIN_LOCAL_APP_DATA)),
(&USER_PROFILE, HOME),
(&DOCUMENTS, WIN_DOCUMENTS),
] {
path = pattern.replace_all(&path, replacement).to_string();
}
if os == Platform::Windows {
let documents_2: Regex = Regex::new(r"(?i)<home>/Documents").unwrap();
#[allow(clippy::single_element_loop)]
for (pattern, replacement) in [(&documents_2, WIN_DOCUMENTS)] {
path = pattern.replace_all(&path, replacement).to_string();
}
}
for (pattern, replacement) in [
("{64BitSteamID}", STORE_USER_ID),
("{Steam3AccountID}", STORE_USER_ID),
] {
path = path.replace(pattern, replacement);
}
path
}
fn too_broad(path: &str) -> bool {
println!("Path: {}", path);
use {BASE, HOME, ROOT, STORE_USER_ID, WIN_APP_DATA, WIN_DIR, WIN_DOCUMENTS, XDG_CONFIG, XDG_DATA};
let path_lower = path.to_lowercase();
for item in ALL {
if path == *item {
return true;
}
}
for item in AVOID_WILDCARDS {
if path.starts_with(&format!("{}/*", item)) || path.starts_with(&format!("{}/{}", item, STORE_USER_ID)) {
return true;
}
}
// These paths are present whether or not the game is installed.
// If possible, they should be narrowed down on the wiki.
for item in [
format!("{}/{}", BASE, STORE_USER_ID), // because `<storeUserId>` is handled as `*`
format!("{}/Documents", HOME),
format!("{}/Saved Games", HOME),
format!("{}/AppData", HOME),
format!("{}/AppData/Local", HOME),
format!("{}/AppData/Local/Packages", HOME),
format!("{}/AppData/LocalLow", HOME),
format!("{}/AppData/Roaming", HOME),
format!("{}/Documents/My Games", HOME),
format!("{}/Library/Application Support", HOME),
format!("{}/Library/Application Support/UserData", HOME),
format!("{}/Library/Preferences", HOME),
format!("{}/.renpy", HOME),
format!("{}/.renpy/persistent", HOME),
format!("{}/Library", HOME),
format!("{}/Library/RenPy", HOME),
format!("{}/Telltale Games", HOME),
format!("{}/config", ROOT),
format!("{}/MMFApplications", WIN_APP_DATA),
format!("{}/RenPy", WIN_APP_DATA),
format!("{}/RenPy/persistent", WIN_APP_DATA),
format!("{}/win.ini", WIN_DIR),
format!("{}/SysWOW64", WIN_DIR),
format!("{}/My Games", WIN_DOCUMENTS),
format!("{}/Telltale Games", WIN_DOCUMENTS),
format!("{}/unity3d", XDG_CONFIG),
format!("{}/unity3d", XDG_DATA),
"C:/Program Files".to_string(),
"C:/Program Files (x86)".to_string(),
] {
let item = item.to_lowercase();
if path_lower == item
|| path_lower.starts_with(&format!("{}/*", item))
|| path_lower.starts_with(&format!("{}/{}", item, STORE_USER_ID.to_lowercase()))
|| path_lower.starts_with(&format!("{}/savesdir", item))
{
return true;
}
}
// Drive letters:
let drives: Regex = Regex::new(r"^[a-zA-Z]:$").unwrap();
if drives.is_match(path) {
return true;
}
// Colon not for a drive letter
if path.get(2..).is_some_and(|path| path.contains(':')) {
return true;
}
// Root:
if path == "/" {
return true;
}
// Relative path wildcard:
if path.starts_with('*') {
return true;
}
false
}
pub fn usable(path: &str) -> bool {
let unprintable: Regex = Regex::new(r"(\p{Cc}|\p{Cf})").unwrap();
!path.is_empty()
&& !path.contains("{{")
&& !path.starts_with("./")
&& !path.starts_with("../")
&& !too_broad(path)
&& !unprintable.is_match(path)
}

View File

@ -0,0 +1,48 @@
use std::{path::PathBuf, sync::LazyLock};
pub enum CommonPath {
Config,
Data,
DataLocal,
DataLocalLow,
Document,
Home,
Public,
SavedGames,
}
impl CommonPath {
pub fn get(&self) -> Option<PathBuf> {
static CONFIG: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::config_dir());
static DATA: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_dir());
static DATA_LOCAL: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::data_local_dir());
static DOCUMENT: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::document_dir());
static HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::home_dir());
static PUBLIC: LazyLock<Option<PathBuf>> = LazyLock::new(|| dirs::public_dir());
#[cfg(windows)]
static DATA_LOCAL_LOW: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
known_folders::get_known_folder_path(known_folders::KnownFolder::LocalAppDataLow)
});
#[cfg(not(windows))]
static DATA_LOCAL_LOW: Option<PathBuf> = None;
#[cfg(windows)]
static SAVED_GAMES: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
known_folders::get_known_folder_path(known_folders::KnownFolder::SavedGames)
});
#[cfg(not(windows))]
static SAVED_GAMES: Option<PathBuf> = None;
match self {
Self::Config => CONFIG.clone(),
Self::Data => DATA.clone(),
Self::DataLocal => DATA_LOCAL.clone(),
Self::DataLocalLow => DATA_LOCAL_LOW.clone(),
Self::Document => DOCUMENT.clone(),
Self::Home => HOME.clone(),
Self::Public => PUBLIC.clone(),
Self::SavedGames => SAVED_GAMES.clone(),
}
}
}

View File

@ -0,0 +1,51 @@
use std::sync::LazyLock;
pub const ALL: &[&str] = &[
ROOT,
GAME,
BASE,
HOME,
STORE_USER_ID,
OS_USER_NAME,
WIN_APP_DATA,
WIN_LOCAL_APP_DATA,
WIN_DOCUMENTS,
WIN_PUBLIC,
WIN_PROGRAM_DATA,
WIN_DIR,
XDG_DATA,
XDG_CONFIG,
];
/// These are paths where `<placeholder>/*/` is suspicious.
pub const AVOID_WILDCARDS: &[&str] = &[
ROOT,
HOME,
WIN_APP_DATA,
WIN_LOCAL_APP_DATA,
WIN_DOCUMENTS,
WIN_PUBLIC,
WIN_PROGRAM_DATA,
WIN_DIR,
XDG_DATA,
XDG_CONFIG,
];
pub const ROOT: &str = "<root>"; // a directory where games are installed (configured in backup tool)
pub const GAME: &str = "<game>"; // an installDir (if defined) or the game's canonical name in the manifest
pub const BASE: &str = "<base>"; // shorthand for <root>/<game> (unless overridden by store-specific rules)
pub const HOME: &str = "<home>"; // current user's home directory in the OS (~)
pub const STORE_USER_ID: &str = "<storeUserId>"; // a store-specific id from the manifest, corresponding to the root's store type
pub const OS_USER_NAME: &str = "<osUserName>"; // current user's ID in the game store
pub const WIN_APP_DATA: &str = "<winAppData>"; // current user's name in the OS
pub const WIN_LOCAL_APP_DATA: &str = "<winLocalAppData>"; // %APPDATA% on Windows
pub const WIN_LOCAL_APP_DATA_LOW: &str = "<winLocalAppDataLow>"; // %LOCALAPPDATA% on Windows
pub const WIN_DOCUMENTS: &str = "<winDocuments>"; // <home>/AppData/LocalLow on Windows
pub const WIN_PUBLIC: &str = "<winPublic>"; // <home>/Documents (f.k.a. <home>/My Documents) or a localized equivalent on Windows
pub const WIN_PROGRAM_DATA: &str = "<winProgramData>"; // %PUBLIC% on Windows
pub const WIN_DIR: &str = "<winDir>"; // %PROGRAMDATA% on Windows
pub const XDG_DATA: &str = "<xdgData>"; // %WINDIR% on Windows
pub const XDG_CONFIG: &str = "<xdgConfig>"; // $XDG_DATA_HOME on Linux
pub const SKIP: &str = "<skip>"; // $XDG_CONFIG_HOME on Linux
pub static OS_USERNAME: LazyLock<String> = LazyLock::new(|| whoami::username());

View File

@ -0,0 +1,262 @@
use std::{
fs::{self, create_dir_all, File},
io::{self, ErrorKind, Read, Write},
path::{Path, PathBuf},
thread::sleep,
time::Duration,
};
use super::{
backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*,
};
use log::{debug, warn};
use rustix::path::Arg;
use tempfile::tempfile;
use crate::{
database::db::GameVersion, error::backup_error::BackupError, process::process_manager::Platform,
};
use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize};
pub fn resolve(meta: &mut CloudSaveMetadata) -> File {
let f = File::create_new("save").unwrap();
let compressor = zstd::Encoder::new(f, 22).unwrap();
let mut tarball = tar::Builder::new(compressor);
let manager = BackupManager::new();
for file in meta.files.iter_mut() {
let id = uuid::Uuid::new_v4().to_string();
let os = match file
.conditions
.iter()
.find_map(|p| match p {
super::conditions::Condition::Os(os) => Some(os),
_ => None,
})
.cloned()
{
Some(os) => os,
None => {
warn!(
"File {:?} could not be backed up because it did not provide an OS",
&file
);
continue;
}
};
let handler = match manager.sources.get(&(manager.current_platform, os)) {
Some(h) => *h,
None => continue,
};
let t_path = PathBuf::from(normalize(&file.path, os));
println!("{:?}", &t_path);
let path = parse_path(t_path, handler, &meta.game_version).unwrap();
let f = std::fs::metadata(&path).unwrap(); // TODO: Fix unwrap here
if f.is_dir() {
tarball.append_dir_all(&id, path).unwrap();
} else if f.is_file() {
tarball
.append_file(&id, &mut File::open(path).unwrap())
.unwrap();
}
file.id = Some(id);
}
let binding = serde_json::to_string(meta).unwrap();
let serialized = binding.as_bytes();
let mut file = tempfile().unwrap();
file.write(serialized).unwrap();
tarball.append_file("metadata", &mut file).unwrap();
tarball.into_inner().unwrap().finish().unwrap()
}
pub fn extract(file: PathBuf) -> Result<(), BackupError> {
let tmpdir = tempfile::tempdir().unwrap();
// Reopen the file for reading
let file = File::open(file).unwrap();
let decompressor = zstd::Decoder::new(file).unwrap();
let mut f = tar::Archive::new(decompressor);
f.unpack(tmpdir.path()).unwrap();
let path = tmpdir.path();
let mut manifest = File::open(path.join("metadata")).unwrap();
let mut manifest_slice = Vec::new();
manifest.read_to_end(&mut manifest_slice).unwrap();
let manifest: CloudSaveMetadata = serde_json::from_slice(&manifest_slice).unwrap();
for file in manifest.files {
let current_path = path.join(file.id.as_ref().unwrap());
let manager = BackupManager::new();
let os = match file
.conditions
.iter()
.find_map(|p| match p {
super::conditions::Condition::Os(os) => Some(os),
_ => None,
})
.cloned()
{
Some(os) => os,
None => {
warn!(
"File {:?} could not be replaced up because it did not provide an OS",
&file
);
continue;
}
};
let handler = match manager.sources.get(&(manager.current_platform, os)) {
Some(h) => *h,
None => continue,
};
let new_path = parse_path(file.path.into(), handler, &manifest.game_version)?;
create_dir_all(&new_path.parent().unwrap()).unwrap();
println!(
"Current path {:?} copying to {:?}",
&current_path, &new_path
);
copy_item(current_path, new_path).unwrap();
}
Ok(())
}
pub fn copy_item<P: AsRef<Path>>(src: P, dest: P) -> io::Result<()> {
let src_path = src.as_ref();
let dest_path = dest.as_ref();
let metadata = fs::metadata(&src_path)?;
if metadata.is_file() {
// Ensure the parent directory of the destination exists for a file copy
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&src_path, &dest_path)?;
} else if metadata.is_dir() {
// For directories, we call the recursive helper function.
// The destination for the recursive copy is the `dest_path` itself.
copy_dir_recursive(&src_path, &dest_path)?;
} else {
// Handle other file types like symlinks if necessary,
// for now, return an error or skip.
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Source {:?} is neither a file nor a directory", src_path),
));
}
Ok(())
}
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
fs::create_dir_all(&dest)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let entry_path = entry.path();
let entry_file_name = match entry_path.file_name() {
Some(name) => name,
None => continue, // Skip if somehow there's no file name
};
let dest_entry_path = dest.join(entry_file_name);
let metadata = entry.metadata()?;
if metadata.is_file() {
debug!(
"Writing file {} to {}",
entry_path.display(),
dest_entry_path.display()
);
fs::copy(&entry_path, &dest_entry_path)?;
} else if metadata.is_dir() {
copy_dir_recursive(&entry_path, &dest_entry_path)?;
}
// Ignore other types like symlinks for this basic implementation
}
Ok(())
}
pub fn parse_path(
path: PathBuf,
backup_handler: &dyn BackupHandler,
game: &GameVersion,
) -> Result<PathBuf, BackupError> {
println!("Parsing: {:?}", &path);
let mut s = PathBuf::new();
for component in path.components() {
match component.as_str().unwrap() {
ROOT => s.push(backup_handler.root_translate(&path, game)?),
GAME => s.push(backup_handler.game_translate(&path, game)?),
BASE => s.push(backup_handler.base_translate(&path, game)?),
HOME => s.push(backup_handler.home_translate(&path, game)?),
STORE_USER_ID => s.push(backup_handler.store_user_id_translate(&path, game)?),
OS_USER_NAME => s.push(backup_handler.os_user_name_translate(&path, game)?),
WIN_APP_DATA => s.push(backup_handler.win_app_data_translate(&path, game)?),
WIN_LOCAL_APP_DATA => s.push(backup_handler.win_local_app_data_translate(&path, game)?),
WIN_LOCAL_APP_DATA_LOW => {
s.push(backup_handler.win_local_app_data_low_translate(&path, game)?)
}
WIN_DOCUMENTS => s.push(backup_handler.win_documents_translate(&path, game)?),
WIN_PUBLIC => s.push(backup_handler.win_public_translate(&path, game)?),
WIN_PROGRAM_DATA => s.push(backup_handler.win_program_data_translate(&path, game)?),
WIN_DIR => s.push(backup_handler.win_dir_translate(&path, game)?),
XDG_DATA => s.push(backup_handler.xdg_data_translate(&path, game)?),
XDG_CONFIG => s.push(backup_handler.xdg_config_translate(&path, game)?),
SKIP => s.push(backup_handler.skip_translate(&path, game)?),
_ => s.push(PathBuf::from(component.as_os_str())),
}
}
println!("Final line: {:?}", &s);
Ok(s)
}
pub fn test() {
let mut meta = CloudSaveMetadata {
files: vec![
GameFile {
path: String::from("<home>/favicon.png"),
id: None,
data_type: super::metadata::DataType::File,
tags: Vec::new(),
conditions: vec![Condition::Os(Platform::Linux)],
},
GameFile {
path: String::from("<home>/Documents/Pixel Art"),
id: None,
data_type: super::metadata::DataType::File,
tags: Vec::new(),
conditions: vec![Condition::Os(Platform::Linux)],
},
],
game_version: GameVersion {
game_id: String::new(),
version_name: String::new(),
platform: Platform::Linux,
launch_command: String::new(),
launch_args: Vec::new(),
launch_command_template: String::new(),
setup_command: String::new(),
setup_args: Vec::new(),
setup_command_template: String::new(),
only_setup: true,
version_index: 0,
delta: false,
umu_id_override: None,
},
save_id: String::from("aaaaaaa"),
};
//resolve(&mut meta);
extract("save".into()).unwrap();
}

View File

View File

@ -6,10 +6,12 @@ use std::{
use serde_json::Value; use serde_json::Value;
use crate::{database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError}; use crate::{
database::db::borrow_db_mut_checked, error::download_manager_error::DownloadManagerError,
};
use super::{ use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, db::{borrow_db_checked, DATA_ROOT_DIR},
debug::SystemData, debug::SystemData,
models::data::Settings, models::data::Settings,
}; };
@ -26,8 +28,6 @@ pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
pub fn delete_download_dir(index: usize) { pub fn delete_download_dir(index: usize) {
let mut lock = borrow_db_mut_checked(); let mut lock = borrow_db_mut_checked();
lock.applications.install_dirs.remove(index); lock.applications.install_dirs.remove(index);
drop(lock);
save_db();
} }
#[tauri::command] #[tauri::command]
@ -58,7 +58,6 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
} }
lock.applications.install_dirs.push(new_dir); lock.applications.install_dirs.push(new_dir);
drop(lock); drop(lock);
save_db();
Ok(()) Ok(())
} }
@ -72,8 +71,6 @@ pub fn update_settings(new_settings: Value) {
} }
let new_settings: Settings = serde_json::from_value(current_settings).unwrap(); let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
db_lock.settings = new_settings; db_lock.settings = new_settings;
drop(db_lock);
save_db();
} }
#[tauri::command] #[tauri::command]
pub fn fetch_settings() -> Settings { pub fn fetch_settings() -> Settings {

View File

@ -1,12 +1,14 @@
use std::{ use std::{
fs::{self, create_dir_all}, fs::{self, create_dir_all},
mem::ManuallyDrop,
ops::{Deref, DerefMut},
path::PathBuf, path::PathBuf,
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
}; };
use chrono::Utc; use chrono::Utc;
use directories::BaseDirs; use log::{debug, error, info, warn};
use log::{debug, error, info}; use native_model::{Decode, Encode};
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use url::Url; use url::Url;
@ -16,7 +18,7 @@ use crate::DB;
use super::models::data::Database; use super::models::data::Database;
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> = pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); LazyLock::new(|| Mutex::new(dirs::data_dir().unwrap().join("drop")));
// Custom JSON serializer to support everything we need // Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -26,15 +28,16 @@ impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer for DropDatabaseSerializer
{ {
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string())) native_model::rmp_serde_1_3::RmpSerde::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string()))
} }
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> { fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new(); let mut buf = Vec::new();
s.read_to_end(&mut buf) s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?; .map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let (val, _version) = let val = native_model::rmp_serde_1_3::RmpSerde::decode(buf)
native_model::decode::<T>(buf).map_err(|e| DeSerError::Internal(e.to_string()))?; .map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val) Ok(val)
} }
} }
@ -99,6 +102,7 @@ fn handle_invalid_database(
games_base_dir: PathBuf, games_base_dir: PathBuf,
cache_dir: PathBuf, cache_dir: PathBuf,
) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> { ) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
warn!("{}", _e);
let new_path = { let new_path = {
let time = Utc::now().timestamp(); let time = Utc::now().timestamp();
let mut base = db_path.clone(); let mut base = db_path.clone();
@ -120,9 +124,46 @@ fn handle_invalid_database(
PathDatabase::create_at_path(db_path, db).expect("Database could not be created") PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
} }
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> { // To automatically save the database upon drop
pub struct DBRead<'a>(RwLockReadGuard<'a, Database>);
pub struct DBWrite<'a>(ManuallyDrop<RwLockWriteGuard<'a, Database>>);
impl<'a> Deref for DBWrite<'a> {
type Target = RwLockWriteGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> Deref for DBRead<'a> {
type Target = RwLockReadGuard<'a, Database>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Drop for DBWrite<'a> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}
}
pub fn borrow_db_checked<'a>() -> DBRead<'a> {
match DB.borrow_data() { match DB.borrow_data() {
Ok(data) => data, Ok(data) => DBRead(data),
Err(e) => { Err(e) => {
error!("database borrow failed with error {}", e); error!("database borrow failed with error {}", e);
panic!("database borrow failed with error {}", e); panic!("database borrow failed with error {}", e);
@ -130,22 +171,12 @@ pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
} }
} }
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> { pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> {
match DB.borrow_data_mut() { match DB.borrow_data_mut() {
Ok(data) => data, Ok(data) => DBWrite(ManuallyDrop::new(data)),
Err(e) => { Err(e) => {
error!("database borrow mut failed with error {}", e); error!("database borrow mut failed with error {}", e);
panic!("database borrow mut failed with error {}", e); panic!("database borrow mut failed with error {}", e);
} }
} }
} }
pub fn save_db() {
match DB.save() {
Ok(_) => {}
Err(e) => {
error!("database failed to save with error {}", e);
panic!("database failed to save with error {}", e)
}
}
}

View File

@ -1,19 +1,27 @@
use crate::database::models::data::Database;
pub mod data { pub mod data {
use std::path::PathBuf;
use native_model::{native_model, Model}; use native_model::{native_model, Model};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion; pub type GameVersion = v1::GameVersion;
pub type Database = v2::Database; pub type Database = v3::Database;
pub type Settings = v1::Settings; pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth; pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus; pub type GameDownloadStatus = v2::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus; pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata; pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType; pub type DownloadType = v1::DownloadType;
pub type DatabaseApplications = v1::DatabaseApplications; pub type DatabaseApplications = v2::DatabaseApplications;
pub type DatabaseCompatInfo = v2::DatabaseCompatInfo; pub type DatabaseCompatInfo = v2::DatabaseCompatInfo;
use std::{collections::HashMap, process::Command};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE;
pub mod v1 { pub mod v1 {
use crate::process::process_manager::Platform; use crate::process::process_manager::Platform;
use serde_with::serde_as; use serde_with::serde_as;
@ -27,7 +35,7 @@ pub mod data {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[native_model(id = 2, version = 1)] #[native_model(id = 2, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct GameVersion { pub struct GameVersion {
pub game_id: String, pub game_id: String,
pub version_name: String, pub version_name: String,
@ -69,7 +77,7 @@ pub mod data {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[native_model(id = 4, version = 1)] #[native_model(id = 4, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct Settings { pub struct Settings {
pub autostart: bool, pub autostart: bool,
pub max_download_threads: usize, pub max_download_threads: usize,
@ -88,7 +96,7 @@ pub mod data {
// Strings are version names for a particular game // Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)] #[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[native_model(id = 5, version = 1)] #[native_model(id = 5, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus { pub enum GameDownloadStatus {
Remote {}, Remote {},
SetupRequired { SetupRequired {
@ -102,7 +110,7 @@ pub mod data {
} }
// Stuff that shouldn't be synced to disk // Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ApplicationTransientStatus { pub enum ApplicationTransientStatus {
Downloading { version_name: String }, Downloading { version_name: String },
Uninstalling {}, Uninstalling {},
@ -111,7 +119,7 @@ pub mod data {
} }
#[derive(serde::Serialize, Clone, Deserialize)] #[derive(serde::Serialize, Clone, Deserialize)]
#[native_model(id = 6, version = 1)] #[native_model(id = 6, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseAuth { pub struct DatabaseAuth {
pub private: String, pub private: String,
pub cert: String, pub cert: String,
@ -130,7 +138,7 @@ pub mod data {
Mod, Mod,
} }
#[native_model(id = 7, version = 1)] #[native_model(id = 7, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata { pub struct DownloadableMetadata {
@ -162,64 +170,31 @@ pub mod data {
} }
pub mod v2 { pub mod v2 {
use std::{collections::HashMap, path::PathBuf, process::Command}; use std::{collections::HashMap, path::PathBuf};
use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE; use serde_with::serde_as;
use super::*; use super::*;
#[native_model(id = 1, version = 2)] #[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database { pub struct Database {
#[serde(default)] #[serde(default)]
pub settings: Settings, pub settings: Settings,
pub auth: Option<DatabaseAuth>, pub auth: Option<DatabaseAuth>,
pub base_url: String, pub base_url: String,
pub applications: DatabaseApplications, pub applications: v1::DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>, pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf, pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>, pub compat_info: Option<DatabaseCompatInfo>,
} }
#[native_model(id = 8, version = 2)] #[native_model(id = 8, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone, Default)]
pub struct DatabaseCompatInfo { pub struct DatabaseCompatInfo {
umu_installed: bool, pub umu_installed: bool,
}
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
} }
impl From<v1::Database> for Database { impl From<v1::Database> for Database {
@ -231,9 +206,142 @@ pub mod data {
applications: value.applications, applications: value.applications,
prev_database: value.prev_database, prev_database: value.prev_database,
cache_dir: value.cache_dir, cache_dir: value.cache_dir,
compat_info: crate::database::models::Database::create_new_compat_info(),
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize, Debug)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
PartiallyInstalled {
version_name: String,
install_dir: String,
},
}
impl From<v1::GameDownloadStatus> for GameDownloadStatus {
fn from(value: v1::GameDownloadStatus) -> Self {
match value {
v1::GameDownloadStatus::Remote {} => Self::Remote {},
v1::GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Self::SetupRequired {
version_name,
install_dir,
},
v1::GameDownloadStatus::Installed {
version_name,
install_dir,
} => Self::Installed {
version_name,
install_dir,
},
}
}
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
impl From<v1::DatabaseApplications> for DatabaseApplications {
fn from(value: v1::DatabaseApplications) -> Self {
Self {
game_statuses: value
.game_statuses
.into_iter()
.map(|x| (x.0, x.1.into()))
.collect::<HashMap<String, GameDownloadStatus>>(),
install_dirs: value.install_dirs,
game_versions: value.game_versions,
installed_game_version: value.installed_game_version,
transient_statuses: value.transient_statuses,
}
}
}
}
mod v3 {
use std::path::PathBuf;
use super::*;
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
#[serde(skip)]
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
pub compat_info: Option<DatabaseCompatInfo>,
}
impl From<v2::Database> for Database {
fn from(value: v2::Database) -> Self {
Self {
settings: value.settings,
auth: value.auth,
base_url: value.base_url,
applications: value.applications.into(),
prev_database: value.prev_database,
cache_dir: value.cache_dir,
compat_info: Database::create_new_compat_info(), compat_info: Database::create_new_compat_info(),
} }
} }
} }
} }
impl Database {
fn create_new_compat_info() -> Option<DatabaseCompatInfo> {
#[cfg(target_os = "windows")]
return None;
let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok();
Some(DatabaseCompatInfo {
umu_installed: has_umu_installed,
})
}
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
compat_info: Database::create_new_compat_info(),
}
}
}
} }

View File

@ -18,7 +18,8 @@ use crate::{
}; };
use super::{ use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent}, util::queue::Queue, download_manager_builder::{CurrentProgressObject, DownloadAgent},
util::queue::Queue,
}; };
pub enum DownloadManagerSignal { pub enum DownloadManagerSignal {

View File

@ -18,7 +18,12 @@ use crate::{
use super::{ use super::{
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
downloadable::Downloadable, util::{download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue}, downloadable::Downloadable,
util::{
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
progress_object::ProgressObject,
queue::Queue,
},
}; };
pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>; pub type DownloadAgent = Arc<Box<dyn Downloadable + Send + Sync>>;
@ -243,13 +248,28 @@ impl DownloadManagerBuilder {
// Ok(true) is for completed and exited properly // Ok(true) is for completed and exited properly
Ok(true) => { Ok(true) => {
debug!("download {:?} has completed", download_agent.metadata()); debug!("download {:?} has completed", download_agent.metadata());
download_agent.on_complete(&app_handle); match download_agent.validate() {
sender Ok(true) => {
.send(DownloadManagerSignal::Completed(download_agent.metadata())) download_agent.on_complete(&app_handle);
.unwrap(); sender
.send(DownloadManagerSignal::Completed(download_agent.metadata()))
.unwrap();
}
Ok(false) => {
download_agent.on_incomplete(&app_handle);
}
Err(e) => {
error!(
"download {:?} has validation error {}",
download_agent.metadata(),
&e
);
}
}
} }
// Ok(false) is for incomplete but exited properly // Ok(false) is for incomplete but exited properly
Ok(false) => { Ok(false) => {
debug!("Donwload agent finished incomplete");
download_agent.on_incomplete(&app_handle); download_agent.on_incomplete(&app_handle);
} }
Err(e) => { Err(e) => {

View File

@ -8,13 +8,15 @@ use crate::{
}; };
use super::{ use super::{
download_manager::DownloadStatus, util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject}, download_manager::DownloadStatus,
util::{download_thread_control_flag::DownloadThreadControl, progress_object::ProgressObject},
}; };
pub trait Downloadable: Send + Sync { pub trait Downloadable: Send + Sync {
fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>; fn download(&self, app_handle: &AppHandle) -> Result<bool, ApplicationDownloadError>;
fn progress(&self) -> Arc<ProgressObject>; fn progress(&self) -> Arc<ProgressObject>;
fn control_flag(&self) -> DownloadThreadControl; fn control_flag(&self) -> DownloadThreadControl;
fn validate(&self) -> Result<bool, ApplicationDownloadError>;
fn status(&self) -> DownloadStatus; fn status(&self) -> DownloadStatus;
fn metadata(&self) -> DownloadableMetadata; fn metadata(&self) -> DownloadableMetadata;
fn on_initialised(&self, app_handle: &AppHandle); fn on_initialised(&self, app_handle: &AppHandle);

View File

@ -2,4 +2,4 @@ pub mod commands;
pub mod download_manager; pub mod download_manager;
pub mod download_manager_builder; pub mod download_manager_builder;
pub mod downloadable; pub mod downloadable;
pub mod util; pub mod util;

View File

@ -1,4 +1,4 @@
pub mod download_thread_control_flag;
pub mod progress_object; pub mod progress_object;
pub mod queue; pub mod queue;
pub mod rolling_progress_updates; pub mod rolling_progress_updates;
pub mod download_thread_control_flag;

View File

@ -12,9 +12,7 @@ use throttle_my_fn::throttle;
use crate::download_manager::download_manager::DownloadManagerSignal; use crate::download_manager::download_manager::DownloadManagerSignal;
use super::{ use super::rolling_progress_updates::RollingProgressWindow;
rolling_progress_updates::RollingProgressWindow,
};
#[derive(Clone)] #[derive(Clone)]
pub struct ProgressObject { pub struct ProgressObject {
@ -86,6 +84,17 @@ impl ProgressObject {
.map(|instance| instance.load(Ordering::Relaxed)) .map(|instance| instance.load(Ordering::Relaxed))
.sum() .sum()
} }
pub fn reset(&self, size: usize) {
self.set_time_now();
self.set_size(size);
self.bytes_last_update.store(0, Ordering::Release);
self.rolling.reset();
self.progress_instances
.lock()
.unwrap()
.iter()
.for_each(|x| x.store(0, Ordering::Release));
}
pub fn get_max(&self) -> usize { pub fn get_max(&self) -> usize {
*self.max.lock().unwrap() *self.max.lock().unwrap()
} }

View File

@ -30,4 +30,9 @@ impl<const S: usize> RollingProgressWindow<S> {
.sum::<usize>() .sum::<usize>()
/ S / S
} }
pub fn reset(&self) {
self.window
.iter()
.for_each(|x| x.store(0, Ordering::Release));
}
} }

View File

@ -0,0 +1,21 @@
use std::fmt::Display;
use serde_with::SerializeDisplay;
#[derive(Debug, SerializeDisplay, Clone, Copy)]
pub enum BackupError {
InvalidSystem,
NotFound,
ParseError,
}
impl Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
BackupError::InvalidSystem => "Attempted to generate path for invalid system",
BackupError::NotFound => "Could not generate or find path",
BackupError::ParseError => "Failed to parse path",
};
write!(f, "{}", s)
}
}

View File

@ -1,6 +1,7 @@
pub mod application_download_error; pub mod application_download_error;
pub mod drop_server_error; pub mod backup_error;
pub mod download_manager_error; pub mod download_manager_error;
pub mod drop_server_error;
pub mod library_error; pub mod library_error;
pub mod process_error; pub mod process_error;
pub mod remote_access_error; pub mod remote_access_error;

View File

@ -37,13 +37,14 @@ pub fn fetch_game(
game_id: String, game_id: String,
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
offline!( let res = offline!(
state, state,
fetch_game_logic, fetch_game_logic,
fetch_game_logic_offline, fetch_game_logic_offline,
game_id, game_id,
state state
) );
res
} }
#[tauri::command] #[tauri::command]

View File

@ -1,9 +1,13 @@
use std::sync::{Arc, Mutex}; use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use crate::{ use crate::{
download_manager::{ database::{db::borrow_db_checked, models::data::GameDownloadStatus},
download_manager::DownloadManagerSignal, downloadable::Downloadable, download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable},
}, error::download_manager_error::DownloadManagerError, AppState error::download_manager_error::DownloadManagerError,
AppState,
}; };
use super::download_agent::GameDownloadAgent; use super::download_agent::GameDownloadAgent;
@ -16,7 +20,7 @@ pub fn download_game(
state: tauri::State<'_, Mutex<AppState>>, state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> { ) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
let sender = state.lock().unwrap().download_manager.get_sender(); let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new( let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
game_id, game_id,
game_version, game_version,
install_dir, install_dir,
@ -28,3 +32,39 @@ pub fn download_game(
.download_manager .download_manager
.queue_download(game_download_agent)?) .queue_download(game_download_agent)?)
} }
#[tauri::command]
pub fn resume_download(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
let s = borrow_db_checked()
.applications
.game_statuses
.get(&game_id)
.unwrap()
.clone();
let (version_name, install_dir) = match s {
GameDownloadStatus::Remote {} => unreachable!(),
GameDownloadStatus::SetupRequired { .. } => unreachable!(),
GameDownloadStatus::Installed { .. } => unreachable!(),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
};
let sender = state.lock().unwrap().download_manager.get_sender();
let parent_dir: PathBuf = install_dir.into();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
version_name.clone(),
parent_dir.parent().unwrap().to_path_buf(),
sender,
)) as Box<dyn Downloadable + Send + Sync>);
Ok(state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)?)
}

View File

@ -1,23 +1,25 @@
use crate::auth::generate_authorization_header; use crate::auth::generate_authorization_header;
use crate::database::db::borrow_db_checked; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::{ use crate::database::models::data::{
ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus, ApplicationTransientStatus, DownloadType, DownloadableMetadata,
}; };
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::downloadable::Downloadable; use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; use crate::download_manager::util::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject}; use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObject};
use crate::error::application_download_error::ApplicationDownloadError; use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent}; use crate::games::downloads::validate::game_validate_logic;
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
use crate::remote::requests::make_request; use crate::remote::requests::make_request;
use crate::DB;
use log::{debug, error, info}; use log::{debug, error, info};
use rayon::ThreadPoolBuilder; use rayon::ThreadPoolBuilder;
use slice_deque::SliceDeque; use std::collections::HashMap;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, OpenOptions};
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant; use std::time::Instant;
@ -27,40 +29,48 @@ use tauri::{AppHandle, Emitter};
use rustix::fs::{fallocate, FallocateFlags}; use rustix::fs::{fallocate, FallocateFlags};
use super::download_logic::download_game_chunk; use super::download_logic::download_game_chunk;
use super::stored_manifest::StoredManifest; use super::drop_data::DropData;
pub struct GameDownloadAgent { pub struct GameDownloadAgent {
pub id: String, pub id: String,
pub version: String, pub version: String,
pub control_flag: DownloadThreadControl, pub control_flag: DownloadThreadControl,
contexts: Mutex<Vec<DropDownloadContext>>, contexts: Mutex<Vec<DropDownloadContext>>,
completed_contexts: Mutex<SliceDeque<usize>>, context_map: Mutex<HashMap<String, bool>>,
pub manifest: Mutex<Option<DropManifest>>, pub manifest: Mutex<Option<DropManifest>>,
pub progress: Arc<ProgressObject>, pub progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>, sender: Sender<DownloadManagerSignal>,
pub stored_manifest: StoredManifest, pub stored_manifest: DropData,
status: Mutex<DownloadStatus>, status: Mutex<DownloadStatus>,
} }
impl GameDownloadAgent { impl GameDownloadAgent {
pub fn new( pub fn new_from_index(
id: String, id: String,
version: String, version: String,
target_download_dir: usize, target_download_dir: usize,
sender: Sender<DownloadManagerSignal>, sender: Sender<DownloadManagerSignal>,
) -> Self { ) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let db_lock = borrow_db_checked(); let db_lock = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock); drop(db_lock);
Self::new(id, version, base_dir, sender)
}
pub fn new(
id: String,
version: String,
base_dir: PathBuf,
sender: Sender<DownloadManagerSignal>,
) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let base_dir_path = Path::new(&base_dir); let base_dir_path = Path::new(&base_dir);
let data_base_dir_path = base_dir_path.join(id.clone()); let data_base_dir_path = base_dir_path.join(id.clone());
let stored_manifest = let stored_manifest =
StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone()); DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
Self { Self {
id, id,
@ -68,7 +78,7 @@ impl GameDownloadAgent {
control_flag, control_flag,
manifest: Mutex::new(None), manifest: Mutex::new(None),
contexts: Mutex::new(Vec::new()), contexts: Mutex::new(Vec::new()),
completed_contexts: Mutex::new(SliceDeque::new()), context_map: Mutex::new(HashMap::new()),
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender, sender,
stored_manifest, stored_manifest,
@ -173,11 +183,15 @@ impl GameDownloadAgent {
} }
pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> { pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
if !self.contexts.lock().unwrap().is_empty() { if self.contexts.lock().unwrap().is_empty() {
return Ok(()); self.generate_contexts()?;
} }
self.generate_contexts()?; self.context_map
.lock()
.unwrap()
.extend(self.stored_manifest.get_contexts());
Ok(()) Ok(())
} }
@ -189,20 +203,18 @@ impl GameDownloadAgent {
let base_path = Path::new(&self.stored_manifest.base_path); let base_path = Path::new(&self.stored_manifest.base_path);
create_dir_all(base_path).unwrap(); 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 { for (raw_path, chunk) in manifest {
let path = base_path.join(Path::new(&raw_path)); let path = base_path.join(Path::new(&raw_path));
let container = path.parent().unwrap(); let container = path.parent().unwrap();
create_dir_all(container).unwrap(); create_dir_all(container).unwrap();
let file = File::create(path.clone()).unwrap(); let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(path.clone())
.unwrap();
let mut running_offset = 0; let mut running_offset = 0;
for (index, length) in chunk.lengths.iter().enumerate() { for (index, length) in chunk.lengths.iter().enumerate() {
@ -225,6 +237,14 @@ impl GameDownloadAgent {
let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
} }
} }
let existing_contexts = self.stored_manifest.get_completed_contexts();
self.stored_manifest.set_contexts(
&contexts
.iter()
.map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum)))
.collect::<Vec<(String, bool)>>(),
);
*self.contexts.lock().unwrap() = contexts; *self.contexts.lock().unwrap() = contexts;
Ok(()) Ok(())
@ -243,13 +263,14 @@ impl GameDownloadAgent {
.build() .build()
.unwrap(); .unwrap();
let completed_indexes = Arc::new(boxcar::Vec::new()); let completed_contexts = Arc::new(boxcar::Vec::new());
let completed_indexes_loop_arc = completed_indexes.clone(); let completed_indexes_loop_arc = completed_contexts.clone();
let contexts = self.contexts.lock().unwrap(); let contexts = self.contexts.lock().unwrap();
debug!("{:#?}", contexts); debug!("{:#?}", contexts);
pool.scope(|scope| { pool.scope(|scope| {
let client = &reqwest::blocking::Client::new(); let client = &reqwest::blocking::Client::new();
let context_map = self.context_map.lock().unwrap();
for (index, context) in contexts.iter().enumerate() { for (index, context) in contexts.iter().enumerate() {
let client = client.clone(); let client = client.clone();
let completed_indexes = completed_indexes_loop_arc.clone(); let completed_indexes = completed_indexes_loop_arc.clone();
@ -258,11 +279,13 @@ impl GameDownloadAgent {
let progress_handle = ProgressHandle::new(progress, self.progress.clone()); let progress_handle = ProgressHandle::new(progress, self.progress.clone());
// If we've done this one already, skip it // If we've done this one already, skip it
if self.completed_contexts.lock().unwrap().contains(&index) { if Some(&true) == context_map.get(&context.checksum) {
progress_handle.skip(context.length); progress_handle.skip(context.length);
continue; continue;
} }
debug!("Continuing download chunk {}", index);
let sender = self.sender.clone(); let sender = self.sender.clone();
let request = match make_request( let request = match make_request(
@ -290,10 +313,18 @@ impl GameDownloadAgent {
scope.spawn(move |_| { scope.spawn(move |_| {
match download_game_chunk(context, &self.control_flag, progress_handle, request) match download_game_chunk(context, &self.control_flag, progress_handle, request)
{ {
Ok(res) => { Ok(true) => {
if res { debug!(
completed_indexes.push(index); "Finished context #{} with checksum {}",
} index, context.checksum
);
completed_indexes.push(context.checksum.clone());
}
Ok(false) => {
debug!(
"Didn't finish context #{} with checksum {}",
index, context.checksum
);
} }
Err(e) => { Err(e) => {
error!("{}", e); error!("{}", e);
@ -304,36 +335,46 @@ impl GameDownloadAgent {
} }
}); });
let newly_completed = completed_indexes.to_owned(); let newly_completed = completed_contexts.to_owned();
let completed_lock_len = { let completed_lock_len = {
let mut completed_contexts_lock = self.completed_contexts.lock().unwrap(); let mut context_map_lock = self.context_map.lock().unwrap();
for (_, item) in newly_completed.iter() { for (_, item) in newly_completed.iter() {
completed_contexts_lock.push_front(*item); context_map_lock.insert(item.clone(), true);
} }
completed_contexts_lock.len() context_map_lock.values().filter(|x| **x).count()
}; };
let context_map_lock = self.context_map.lock().unwrap();
let contexts = contexts
.iter()
.map(|x| {
(
x.checksum.clone(),
context_map_lock
.get(&x.checksum)
.cloned()
.or(Some(false))
.unwrap(),
)
})
.collect::<Vec<(String, bool)>>();
drop(context_map_lock);
// If we're not out of contexts, we're not done, so we don't fire completed self.stored_manifest.set_contexts(&contexts);
if completed_lock_len != contexts.len() { self.stored_manifest.write();
// If there are any contexts left which are false
if !contexts.iter().all(|x| x.1) {
info!( info!(
"download agent for {} exited without completing ({}/{})", "download agent for {} exited without completing ({}/{})",
self.id.clone(), self.id.clone(),
completed_lock_len, completed_lock_len,
contexts.len(), contexts.len(),
); );
self.stored_manifest
.set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice());
self.stored_manifest.write();
return Ok(false); return Ok(false);
} }
// We've completed
self.sender
.send(DownloadManagerSignal::Completed(self.metadata()))
.unwrap();
Ok(true) Ok(true)
} }
} }
@ -372,7 +413,7 @@ impl Downloadable for GameDownloadAgent {
error!("error while managing download: {}", error); error!("error while managing download: {}", error);
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = borrow_db_mut_checked();
handle handle
.applications .applications
.transient_statuses .transient_statuses
@ -390,18 +431,11 @@ impl Downloadable for GameDownloadAgent {
// TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly // TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly
fn on_incomplete(&self, app_handle: &tauri::AppHandle) { fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
let meta = self.metadata(); on_game_incomplete(
*self.status.lock().unwrap() = DownloadStatus::Queued; &self.metadata(),
app_handle self.stored_manifest.base_path.to_string_lossy().to_string(),
.emit( app_handle,
&format!("update_game/{}", meta.id), );
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(GameDownloadStatus::Remote {}), None),
version: None,
},
)
.unwrap();
} }
fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {} fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {}
@ -409,4 +443,14 @@ impl Downloadable for GameDownloadAgent {
fn status(&self) -> DownloadStatus { fn status(&self) -> DownloadStatus {
self.status.lock().unwrap().clone() self.status.lock().unwrap().clone()
} }
fn validate(&self) -> Result<bool, ApplicationDownloadError> {
game_validate_logic(
&self.stored_manifest,
self.contexts.lock().unwrap().clone(),
self.progress.clone(),
self.sender.clone(),
&self.control_flag,
)
}
} }

View File

@ -1,14 +1,17 @@
use crate::download_manager::util::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; use crate::download_manager::util::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::util::progress_object::ProgressHandle; use crate::download_manager::util::progress_object::ProgressHandle;
use crate::error::application_download_error::ApplicationDownloadError; use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError; use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::DropDownloadContext; use crate::games::downloads::manifest::DropDownloadContext;
use log::warn; use log::{debug, warn};
use md5::{Context, Digest}; use md5::{Context, Digest};
use reqwest::blocking::{RequestBuilder, Response}; use reqwest::blocking::{RequestBuilder, Response};
use std::fs::{set_permissions, Permissions}; use std::fs::{set_permissions, Permissions};
use std::io::{ErrorKind, Read}; use std::io::{ErrorKind, Read};
use std::os::unix::fs::MetadataExt;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::{ use std::{
@ -90,6 +93,7 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
let mut current_size = 0; let mut current_size = 0;
loop { loop {
if self.control_flag.get() == DownloadThreadControlFlag::Stop { if self.control_flag.get() == DownloadThreadControlFlag::Stop {
buf_writer.flush()?;
return Ok(false); return Ok(false);
} }
@ -103,6 +107,7 @@ impl<'a> DropDownloadPipeline<'a, Response, File> {
break; break;
} }
} }
buf_writer.flush()?;
Ok(true) Ok(true)
} }
@ -119,6 +124,10 @@ pub fn download_game_chunk(
progress: ProgressHandle, progress: ProgressHandle,
request: RequestBuilder, request: RequestBuilder,
) -> Result<bool, ApplicationDownloadError> { ) -> Result<bool, ApplicationDownloadError> {
debug!(
"Starting download chunk {}, {}, {} #{}",
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
);
// If we're paused // If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop { if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0); progress.set(0);
@ -152,13 +161,14 @@ pub fn download_game_chunk(
)); ));
} }
let mut pipeline = DropDownloadPipeline::new( let length = content_length.unwrap().try_into().unwrap();
response,
destination, if length != ctx.length {
control_flag, return Err(ApplicationDownloadError::DownloadError);
progress, }
content_length.unwrap().try_into().unwrap(),
); let mut pipeline =
DropDownloadPipeline::new(response, destination, control_flag, progress, length);
let completed = pipeline let completed = pipeline
.copy() .copy()
@ -183,5 +193,10 @@ pub fn download_game_chunk(
return Err(ApplicationDownloadError::Checksum); return Err(ApplicationDownloadError::Checksum);
} }
debug!(
"Successfully finished download #{}, copied {} bytes",
ctx.checksum, length
);
Ok(true) Ok(true)
} }

View File

@ -0,0 +1,105 @@
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
};
use log::{debug, error, info, warn};
use native_model::{Decode, Encode};
pub type DropData = v1::DropData;
static DROP_DATA_PATH: &str = ".dropdata";
pub mod v1 {
use std::{path::PathBuf, sync::Mutex};
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[native_model(id = 9, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DropData {
pub game_id: String,
pub game_version: String,
pub contexts: Mutex<Vec<(String, bool)>>,
pub base_path: PathBuf,
}
impl DropData {
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
Self {
base_path,
game_id,
game_version,
contexts: Mutex::new(Vec::new()),
}
}
}
}
impl DropData {
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(_) => {
debug!("Generating new dropdata for game {}", game_id);
return DropData::new(game_id, game_version, base_path);
}
};
let mut s = Vec::new();
match file.read_to_end(&mut s) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
return DropData::new(game_id, game_version, base_path);
}
};
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
Ok(manifest) => manifest,
Err(e) => {
warn!("{}", e);
DropData::new(game_id, game_version, base_path)
}
}
}
pub fn write(&self) {
let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) {
Ok(data) => data,
Err(_) => return,
};
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(e) => {
error!("{}", e);
return;
}
};
match file.write_all(&manifest_raw) {
Ok(_) => {}
Err(e) => error!("{}", e),
};
}
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
*self.contexts.lock().unwrap() = completed_contexts.to_owned();
}
pub fn get_completed_contexts(&self) -> Vec<String> {
self.contexts
.lock()
.unwrap()
.iter()
.filter_map(|x| if x.1 { Some(x.0.clone()) } else { None })
.collect()
}
pub fn get_contexts(&self) -> Vec<(String, bool)> {
info!(
"Any contexts which are complete? {}",
self.contexts.lock().unwrap().iter().any(|x| x.1)
);
self.contexts.lock().unwrap().clone()
}
}

View File

@ -1,5 +1,6 @@
pub mod commands; pub mod commands;
pub mod download_agent; pub mod download_agent;
mod download_logic; mod download_logic;
mod drop_data;
mod manifest; mod manifest;
mod stored_manifest; pub mod validate;

View File

@ -1,79 +0,0 @@
use std::{
fs::File,
io::{Read, Write},
path::PathBuf,
sync::Mutex,
};
use log::{error, warn};
use serde::{Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
#[derive(Serialize, Deserialize, Debug)]
pub struct StoredManifest {
game_id: String,
game_version: String,
pub completed_contexts: Mutex<Vec<usize>>,
pub base_path: PathBuf,
}
static DROP_DATA_PATH: &str = ".dropdata";
impl StoredManifest {
pub fn new(game_id: String, game_version: String, base_path: PathBuf) -> Self {
Self {
base_path,
game_id,
game_version,
completed_contexts: Mutex::new(Vec::new()),
}
}
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(_) => return StoredManifest::new(game_id, game_version, base_path),
};
let mut s = Vec::new();
match file.read_to_end(&mut s) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
return StoredManifest::new(game_id, game_version, base_path);
}
};
match serde_binary::from_vec::<StoredManifest>(s, Endian::Little) {
Ok(manifest) => manifest,
Err(e) => {
warn!("{}", e);
StoredManifest::new(game_id, game_version, base_path)
}
}
}
pub fn write(&self) {
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
Ok(json) => json,
Err(_) => return,
};
let mut file = match File::create(self.base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(e) => {
error!("{}", e);
return;
}
};
match file.write_all(&manifest_raw) {
Ok(_) => {}
Err(e) => error!("{}", e),
};
}
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<usize> {
self.completed_contexts.lock().unwrap().clone()
}
}

View File

@ -0,0 +1,189 @@
use std::{
fs::File,
io::{self, BufWriter, Read, Seek, SeekFrom, Write},
sync::{mpsc::Sender, Arc},
};
use log::{debug, error, info};
use md5::Context;
use rayon::ThreadPoolBuilder;
use crate::{
database::db::borrow_db_checked,
download_manager::{
download_manager::DownloadManagerSignal,
util::{
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
progress_object::{ProgressHandle, ProgressObject},
},
},
error::application_download_error::ApplicationDownloadError,
games::downloads::{drop_data::DropData, manifest::DropDownloadContext},
remote::{auth::generate_authorization_header, requests::make_request},
};
pub fn game_validate_logic(
dropdata: &DropData,
contexts: Vec<DropDownloadContext>,
progress: Arc<ProgressObject>,
sender: Sender<DownloadManagerSignal>,
control_flag: &DownloadThreadControl,
) -> Result<bool, ApplicationDownloadError> {
progress.reset(contexts.len());
let max_download_threads = borrow_db_checked().settings.max_download_threads;
debug!(
"validating game: {} with {} threads",
dropdata.game_id, max_download_threads
);
let pool = ThreadPoolBuilder::new()
.num_threads(max_download_threads)
.build()
.unwrap();
debug!("{:#?}", contexts);
let invalid_chunks = Arc::new(boxcar::Vec::new());
pool.scope(|scope| {
let client = &reqwest::blocking::Client::new();
for (index, context) in contexts.iter().enumerate() {
let client = client.clone();
let current_progress = progress.get(index);
let progress_handle = ProgressHandle::new(current_progress, progress.clone());
let invalid_chunks_scoped = invalid_chunks.clone();
let sender = sender.clone();
let request = match make_request(
&client,
&["/api/v1/client/chunk"],
&[
("id", &context.game_id),
("version", &context.version),
("name", &context.file_name),
("chunk", &context.index.to_string()),
],
|r| r.header("Authorization", generate_authorization_header()),
) {
Ok(request) => request,
Err(e) => {
sender
.send(DownloadManagerSignal::Error(
ApplicationDownloadError::Communication(e),
))
.unwrap();
continue;
}
};
scope.spawn(move |_| {
match validate_game_chunk(context, control_flag, progress_handle) {
Ok(true) => {
debug!(
"Finished context #{} with checksum {}",
index, context.checksum
);
}
Ok(false) => {
debug!(
"Didn't finish context #{} with checksum {}",
index, &context.checksum
);
invalid_chunks_scoped.push(context.checksum.clone());
}
Err(e) => {
error!("{}", e);
sender.send(DownloadManagerSignal::Error(e)).unwrap();
}
}
});
}
});
// If there are any contexts left which are false
if !invalid_chunks.is_empty() {
info!(
"validation of game id {} failed for chunks {:?}",
dropdata.game_id.clone(),
invalid_chunks
);
return Ok(false);
}
Ok(true)
}
pub fn validate_game_chunk(
ctx: &DropDownloadContext,
control_flag: &DownloadThreadControl,
progress: ProgressHandle,
) -> Result<bool, ApplicationDownloadError> {
debug!(
"Starting chunk validation {}, {}, {} #{}",
ctx.file_name, ctx.index, ctx.offset, ctx.checksum
);
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0);
return Ok(false);
}
let mut source = File::open(&ctx.path).unwrap();
if ctx.offset != 0 {
source
.seek(SeekFrom::Start(ctx.offset))
.expect("Failed to seek to file offset");
}
let mut hasher = md5::Context::new();
let completed = validate_copy(&mut source, &mut hasher, control_flag, progress).unwrap();
if !completed {
return Ok(false);
};
let res = hex::encode(hasher.compute().0);
if res != ctx.checksum {
println!(
"Checksum failed. Correct: {}, actual: {}",
&ctx.checksum, &res
);
return Ok(false);
}
debug!(
"Successfully finished verification #{}, copied {} bytes",
ctx.checksum, ctx.length
);
Ok(true)
}
fn validate_copy(
source: &mut File,
dest: &mut Context,
control_flag: &DownloadThreadControl,
progress: ProgressHandle,
) -> Result<bool, io::Error> {
let copy_buf_size = 512;
let mut copy_buf = vec![0; copy_buf_size];
let mut buf_writer = BufWriter::with_capacity(1024 * 1024, dest);
loop {
if control_flag.get() == DownloadThreadControlFlag::Stop {
buf_writer.flush()?;
return Ok(false);
}
let bytes_read = source.read(&mut copy_buf)?;
buf_writer.write_all(&copy_buf[0..bytes_read])?;
progress.add(bytes_read);
if bytes_read == 0 {
break;
}
}
buf_writer.flush()?;
Ok(true)
}

View File

@ -4,10 +4,10 @@ use std::thread::spawn;
use log::{debug, error, warn}; use log::{debug, error, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::Emitter;
use tauri::AppHandle; use tauri::AppHandle;
use tauri::Emitter;
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked};
use crate::database::models::data::{ use crate::database::models::data::{
ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion, ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion,
}; };
@ -18,9 +18,9 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header; use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db}; use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::make_request; use crate::remote::requests::make_request;
use crate::{AppState, DB}; use crate::AppState;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct { pub struct FetchGameStruct {
game: Game, game: Game,
status: GameStatusWithTransient, status: GameStatusWithTransient,
@ -144,7 +144,7 @@ pub fn fetch_game_logic(
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let mut state_handle = state.lock().unwrap(); let mut state_handle = state.lock().unwrap();
let handle = DB.borrow_data().unwrap(); let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id); let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option { let version = match metadata_option {
@ -220,7 +220,7 @@ pub fn fetch_game_logic_offline(
id: String, id: String,
_state: tauri::State<'_, Mutex<AppState>>, _state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> { ) -> Result<FetchGameStruct, RemoteAccessError> {
let handle = DB.borrow_data().unwrap(); let handle = borrow_db_checked();
let metadata_option = handle.applications.installed_game_version.get(&id); let metadata_option = handle.applications.installed_game_version.get(&id);
let version = match metadata_option { let version = match metadata_option {
None => None, None => None,
@ -313,6 +313,10 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
version_name, version_name,
install_dir, install_dir,
} => Some((version_name, install_dir)), } => Some((version_name, install_dir)),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None, _ => None,
} { } {
db_handle db_handle
@ -341,7 +345,6 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
.entry(meta.id.clone()) .entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {}); .and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle); drop(db_handle);
save_db();
debug!("uninstalled game id {}", &meta.id); debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", {}).unwrap(); app_handle.emit("update_library", {}).unwrap();
@ -354,6 +357,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
); );
} }
}); });
} else {
warn!("invalid previous state for uninstall, failing silently.")
} }
} }
@ -365,6 +370,66 @@ pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
.cloned() .cloned()
} }
pub fn on_game_incomplete(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = reqwest::blocking::Client::new();
let response = make_request(
&client,
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
|f| f.header("Authorization", generate_authorization_header()),
)?
.send()?;
let game_version: GameVersion = response.json()?;
let mut handle = borrow_db_mut_checked();
handle
.applications
.game_versions
.entry(meta.id.clone())
.or_default()
.insert(meta.version.clone().unwrap(), game_version.clone());
handle
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
let status = GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.clone().unwrap(),
install_dir,
};
handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(handle);
app_handle
.emit(
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
},
)
.unwrap();
Ok(())
}
pub fn on_game_complete( pub fn on_game_complete(
meta: &DownloadableMetadata, meta: &DownloadableMetadata,
install_dir: String, install_dir: String,
@ -404,7 +469,6 @@ pub fn on_game_complete(
.insert(meta.id.clone(), meta.clone()); .insert(meta.id.clone(), meta.clone());
drop(handle); drop(handle);
save_db();
let status = if game_version.setup_command.is_empty() { let status = if game_version.setup_command.is_empty() {
GameDownloadStatus::Installed { GameDownloadStatus::Installed {
@ -424,7 +488,6 @@ pub fn on_game_complete(
.game_statuses .game_statuses
.insert(meta.id.clone(), status.clone()); .insert(meta.id.clone(), status.clone());
drop(db_handle); drop(db_handle);
save_db();
app_handle app_handle
.emit( .emit(
&format!("update_game/{}", meta.id), &format!("update_game/{}", meta.id),
@ -468,7 +531,7 @@ pub fn update_game_configuration(
game_id: String, game_id: String,
options: FrontendGameOptions, options: FrontendGameOptions,
) -> Result<(), LibraryError> { ) -> Result<(), LibraryError> {
let mut handle = DB.borrow_data_mut().unwrap(); let mut handle = borrow_db_mut_checked();
let installed_version = handle let installed_version = handle
.applications .applications
.installed_game_version .installed_game_version
@ -499,8 +562,5 @@ pub fn update_game_configuration(
.unwrap() .unwrap()
.insert(version.to_string(), existing_configuration); .insert(version.to_string(), existing_configuration);
drop(handle);
save_db();
Ok(()) Ok(())
} }

View File

@ -7,12 +7,12 @@ mod error;
mod process; mod process;
mod remote; mod remote;
use crate::database::db::DatabaseImpls; use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use client::commands::fetch_state;
use client::{ use client::{
autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart}, autostart::{get_autostart_enabled, sync_autostart_on_startup, toggle_autostart},
cleanup::{cleanup_and_exit, quit}, cleanup::{cleanup_and_exit, quit},
}; };
use client::commands::fetch_state;
use database::commands::{ use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings, add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
fetch_system_data, update_settings, fetch_system_data, update_settings,
@ -156,6 +156,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
for (game_id, status) in statuses.into_iter() { for (game_id, status) in statuses.into_iter() {
match status { match status {
GameDownloadStatus::Remote {} => {} GameDownloadStatus::Remote {} => {}
GameDownloadStatus::PartiallyInstalled { .. } => {}
GameDownloadStatus::SetupRequired { GameDownloadStatus::SetupRequired {
version_name: _, version_name: _,
install_dir, install_dir,
@ -259,6 +260,7 @@ pub fn run() {
delete_game_in_collection, delete_game_in_collection,
// Downloads // Downloads
download_game, download_game,
resume_download,
move_download_in_queue, move_download_in_queue,
pause_downloads, pause_downloads,
resume_downloads, resume_downloads,

View File

@ -193,6 +193,10 @@ impl ProcessManager<'_> {
version_name, version_name,
install_dir, install_dir,
} => (version_name, install_dir), } => (version_name, install_dir),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => (version_name, install_dir),
_ => return Err(ProcessError::NotDownloaded), _ => return Err(ProcessError::NotDownloaded),
}; };
@ -248,7 +252,11 @@ impl ProcessManager<'_> {
version_name: _, version_name: _,
install_dir: _, install_dir: _,
} => (&game_version.setup_command, &game_version.setup_args), } => (&game_version.setup_command, &game_version.setup_args),
GameDownloadStatus::Remote {} => unreachable!("nuh uh"), GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => unreachable!("Game registered as 'Partially Installed'"),
GameDownloadStatus::Remote {} => unreachable!("Game registered as 'Remote'"),
}; };
let launch = PathBuf::from_str(&install_dir).unwrap().join(launch); let launch = PathBuf::from_str(&install_dir).unwrap().join(launch);
@ -328,13 +336,54 @@ impl ProcessManager<'_> {
} }
} }
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)] #[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Platform { pub enum Platform {
Windows, Windows,
Linux, Linux,
MacOs, MacOs,
} }
impl Platform {
const WINDOWS: bool = cfg!(target_os = "windows");
const MAC: bool = cfg!(target_os = "macos");
const LINUX: bool = cfg!(target_os = "linux");
#[cfg(target_os = "windows")]
pub const HOST: Platform = Self::Windows;
#[cfg(target_os = "macos")]
pub const HOST: Platform = Self::MacOs;
#[cfg(target_os = "linux")]
pub const HOST: Platform = Self::Linux;
pub fn is_case_sensitive(&self) -> bool {
match self {
Self::Windows | Self::MacOs => false,
Self::Linux => true,
}
}
}
impl From<&str> for Platform {
fn from(value: &str) -> Self {
match value.to_lowercase().trim() {
"windows" => Self::Windows,
"linux" => Self::Linux,
"mac" | "macos" => Self::MacOs,
_ => unimplemented!(),
}
}
}
impl From<whoami::Platform> for Platform {
fn from(value: whoami::Platform) -> Self {
match value {
whoami::Platform::Windows => Platform::Windows,
whoami::Platform::Linux => Platform::Linux,
whoami::Platform::MacOS => Platform::MacOs,
_ => unimplemented!(),
}
}
}
pub trait ProcessHandler: Send + 'static { pub trait ProcessHandler: Send + 'static {
fn create_launch_process( fn create_launch_process(
&self, &self,

View File

@ -1,20 +1,20 @@
use std::{collections::HashMap, env}; use std::{collections::HashMap, env, sync::Mutex};
use chrono::Utc; use chrono::Utc;
use droplet_rs::ssl::sign_nonce; use droplet_rs::ssl::sign_nonce;
use gethostname::gethostname; use gethostname::gethostname;
use log::{debug, error, warn}; use log::{debug, error, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::{ database::{
db::{borrow_db_checked, borrow_db_mut_checked, save_db}, db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth, models::data::DatabaseAuth,
}, },
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppStatus, User, AppState, AppStatus, User,
}; };
use super::{ use super::{
@ -122,8 +122,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
client_id: response_struct.id, client_id: response_struct.id,
web_token: None, // gets created later web_token: None, // gets created later
}); });
drop(handle);
save_db();
} }
let web_token = { let web_token = {
@ -140,8 +138,6 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
let mut_auth = handle.auth.as_mut().unwrap(); let mut_auth = handle.auth.as_mut().unwrap();
mut_auth.web_token = Some(web_token); mut_auth.web_token = Some(web_token);
drop(handle);
save_db();
Ok(()) Ok(())
} }
@ -157,6 +153,16 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
return; return;
} }
let app_state = app.state::<Mutex<AppState>>();
let mut state_lock = app_state.lock().unwrap();
let (app_status, user) = setup();
state_lock.status = app_status;
state_lock.user = user;
drop(state_lock);
app.emit("auth/finished", ()).unwrap(); app.emit("auth/finished", ()).unwrap();
} }

View File

@ -6,7 +6,7 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, database::db::{borrow_db_checked, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError, error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request}, remote::{auth::generate_authorization_header, requests::make_request},
AppState, AppStatus, AppState, AppStatus,
@ -65,8 +65,6 @@ pub fn sign_out(app: AppHandle) {
{ {
let mut handle = borrow_db_mut_checked(); let mut handle = borrow_db_mut_checked();
handle.auth = None; handle.auth = None;
drop(handle);
save_db();
} }
// Update app state // Update app state

View File

@ -5,9 +5,8 @@ use serde::Deserialize;
use url::Url; use url::Url;
use crate::{ use crate::{
database::db::{borrow_db_mut_checked, save_db}, database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppState,
error::remote_access_error::RemoteAccessError, AppStatus,
AppState, AppStatus,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -40,9 +39,6 @@ pub fn use_remote_logic(
let mut db_state = borrow_db_mut_checked(); let mut db_state = borrow_db_mut_checked();
db_state.base_url = base_url.to_string(); db_state.base_url = base_url.to_string();
drop(db_state);
save_db();
Ok(()) Ok(())
} }

View File

@ -1,9 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use http::{ use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
uri::PathAndQuery,
Request, Response, StatusCode, Uri,
};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use tauri::UriSchemeResponder; use tauri::UriSchemeResponder;

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2.0.0", "$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client", "productName": "Drop Desktop Client",
"version": "0.3.0-rc-2", "version": "0.3.0-rc-3",
"identifier": "dev.drop.app", "identifier": "dev.drop.app",
"build": { "build": {
"beforeDevCommand": "yarn dev --port 1432", "beforeDevCommand": "yarn dev --port 1432",
@ -11,7 +11,11 @@
}, },
"app": { "app": {
"security": { "security": {
"csp": null "csp": null,
"assetProtocol": {
"enable": true,
"scope": {}
}
} }
}, },
"plugins": { "plugins": {

View File

@ -59,11 +59,13 @@ export enum GameStatusEnum {
Uninstalling = "Uninstalling", Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired", SetupRequired = "SetupRequired",
Running = "Running", Running = "Running",
PartiallyInstalled = "PartiallyInstalled"
} }
export type GameStatus = { export type GameStatus = {
type: GameStatusEnum; type: GameStatusEnum;
version_name?: string; version_name?: string;
install_dir?: string;
}; };
export enum DownloadableType { export enum DownloadableType {