mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-12 15:52:43 +10:00
Compare commits
9 Commits
v0.3.0-rc-
...
49-bug-bro
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f75bedcae | |||
| 86bce1c68d | |||
| 924e4e334c | |||
| 065eb2356a | |||
| 689e9ad890 | |||
| 7c35ed73aa | |||
| 8f261a5dac | |||
| 67b6f2aa2e | |||
| 11e2b3fe8a |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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.
|
||||||
|
|
||||||
|
|||||||
69
.github/workflows/test.yml
vendored
Normal file
69
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: 'test'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
# This can be used to automatically publish nightlies at UTC nighttime
|
||||||
|
# schedule:
|
||||||
|
# - cron: "0 2 * * *" # run at 2 AM UTC
|
||||||
|
|
||||||
|
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-tauri:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: 'macos-latest' # for Arm based macs (M1 and above).
|
||||||
|
args: '--target aarch64-apple-darwin'
|
||||||
|
- platform: 'macos-latest' # for Intel based macs.
|
||||||
|
args: '--target x86_64-apple-darwin'
|
||||||
|
- platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||||
|
args: ''
|
||||||
|
- platform: 'ubuntu-24.04-arm'
|
||||||
|
args: '--target aarch64-unknown-linux-gnu'
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: install Rust nightly
|
||||||
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with:
|
||||||
|
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y 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.
|
||||||
|
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||||
|
|
||||||
|
- name: install frontend dependencies
|
||||||
|
run: yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||||
|
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||||
|
releaseName: 'Auto testing release'
|
||||||
|
releaseBody: 'See the assets to download this version and install. This release was created automatically.'
|
||||||
|
releaseDraft: false
|
||||||
|
prerelease: true
|
||||||
|
args: ${{ matrix.args }}
|
||||||
@ -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",
|
||||||
|
|||||||
582
src-tauri/Cargo.lock
generated
582
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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" }
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|||||||
102
src-tauri/src/cloud_saves/backup_manager.rs
Normal file
102
src-tauri/src/cloud_saves/backup_manager.rs
Normal 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 {}
|
||||||
6
src-tauri/src/cloud_saves/conditions.rs
Normal file
6
src-tauri/src/cloud_saves/conditions.rs
Normal 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)
|
||||||
|
}
|
||||||
35
src-tauri/src/cloud_saves/metadata.rs
Normal file
35
src-tauri/src/cloud_saves/metadata.rs
Normal 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,
|
||||||
|
}
|
||||||
7
src-tauri/src/cloud_saves/mod.rs
Normal file
7
src-tauri/src/cloud_saves/mod.rs
Normal 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;
|
||||||
162
src-tauri/src/cloud_saves/normalise.rs
Normal file
162
src-tauri/src/cloud_saves/normalise.rs
Normal 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)
|
||||||
|
}
|
||||||
48
src-tauri/src/cloud_saves/path.rs
Normal file
48
src-tauri/src/cloud_saves/path.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src-tauri/src/cloud_saves/placeholder.rs
Normal file
51
src-tauri/src/cloud_saves/placeholder.rs
Normal 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());
|
||||||
262
src-tauri/src/cloud_saves/resolver.rs
Normal file
262
src-tauri/src/cloud_saves/resolver.rs
Normal 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 {:?}",
|
||||||
|
¤t_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();
|
||||||
|
}
|
||||||
0
src-tauri/src/cloud_saves/strict_path.rs
Normal file
0
src-tauri/src/cloud_saves/strict_path.rs
Normal file
@ -5,8 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 +16,8 @@ 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 +27,15 @@ 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::decode::<T>(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
native_model::rmp_serde_1_3::RmpSerde::decode(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
|
||||||
Ok(val)
|
Ok(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,6 +100,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();
|
||||||
|
|||||||
@ -27,7 +27,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 +69,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 +88,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 {
|
||||||
@ -111,7 +111,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 +130,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 {
|
||||||
@ -168,7 +168,7 @@ pub mod data {
|
|||||||
|
|
||||||
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)]
|
||||||
@ -176,12 +176,13 @@ pub mod data {
|
|||||||
pub auth: Option<DatabaseAuth>,
|
pub auth: Option<DatabaseAuth>,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub applications: DatabaseApplications,
|
pub applications: 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 {
|
||||||
|
|||||||
21
src-tauri/src/error/backup_error.rs
Normal file
21
src-tauri/src/error/backup_error.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,3 +5,4 @@ pub mod library_error;
|
|||||||
pub mod process_error;
|
pub mod process_error;
|
||||||
pub mod remote_access_error;
|
pub mod remote_access_error;
|
||||||
pub mod setup_error;
|
pub mod setup_error;
|
||||||
|
pub mod backup_error;
|
||||||
@ -328,13 +328,56 @@ 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,
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user