mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-16 09:41:17 +10:00
Squashed commit of the following:
commit3b09dcfb73Author: quexeky <git@quexeky.dev> Date: Mon Oct 13 08:10:52 2025 +1100 fix: #159 Signed-off-by: quexeky <git@quexeky.dev> commit2859a59622Author: quexeky <git@quexeky.dev> Date: Mon Oct 13 08:03:49 2025 +1100 Squashed commit of the following: commit0f48f3fb44Author: quexeky <git@quexeky.dev> Date: Sun Oct 12 19:35:04 2025 +1100 chore: Run cargo clippy && cargo fmt Signed-off-by: quexeky <git@quexeky.dev> commit974666efe2Author: quexeky <git@quexeky.dev> Date: Sun Oct 12 19:17:40 2025 +1100 refactor: Finish refactor Signed-off-by: quexeky <git@quexeky.dev> commit9e1bf9852fAuthor: quexeky <git@quexeky.dev> Date: Sun Oct 12 18:33:43 2025 +1100 refactor: Builds, but some logic still left to move back Signed-off-by: quexeky <git@quexeky.dev> commit5d22b883d5Author: quexeky <git@quexeky.dev> Date: Sun Oct 12 17:04:27 2025 +1100 refactor: Improvements to src-tauri Signed-off-by: quexeky <git@quexeky.dev> commit62a2561539Author: quexeky <git@quexeky.dev> Date: Sat Oct 11 09:51:04 2025 +1100 fix: Remote tauri dependency from process Signed-off-by: quexeky <git@quexeky.dev> commit59f040bc8bAuthor: quexeky <git@quexeky.dev> Date: Thu Oct 9 07:46:17 2025 +1100 chore: Major refactoring Still needs a massive go-over because there shouldn't be anything referencing tauri in any of the workspaces except the original one. Process manager has been refactored as an example Signed-off-by: quexeky <git@quexeky.dev> Signed-off-by: quexeky <git@quexeky.dev> Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
19
cloud_saves/Cargo.toml
Normal file
19
cloud_saves/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "cloud_saves"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
database = { version = "0.1.0", path = "../database" }
|
||||
dirs = "6.0.0"
|
||||
log = "0.4.28"
|
||||
regex = "1.11.3"
|
||||
rustix = "1.1.2"
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.145"
|
||||
serde_with = "3.15.0"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.23.0"
|
||||
uuid = "1.18.1"
|
||||
whoami = "1.6.1"
|
||||
zstd = "0.13.3"
|
||||
234
cloud_saves/src/backup_manager.rs
Normal file
234
cloud_saves/src/backup_manager.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use std::{collections::HashMap, path::PathBuf, str::FromStr};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use database::platform::Platform;
|
||||
use database::{GameVersion, db::DATA_ROOT_DIR};
|
||||
use log::warn;
|
||||
|
||||
use crate::error::BackupError;
|
||||
|
||||
use super::path::CommonPath;
|
||||
|
||||
pub struct BackupManager<'a> {
|
||||
pub current_platform: Platform,
|
||||
pub sources: HashMap<(Platform, Platform), &'a (dyn BackupHandler + Sync + Send)>,
|
||||
}
|
||||
|
||||
impl Default for BackupManager<'_> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
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.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> {
|
||||
CommonPath::Data.get().ok_or(BackupError::NotFound)
|
||||
}
|
||||
fn xdg_data_translate(
|
||||
&self,
|
||||
_path: &PathBuf,
|
||||
_game: &GameVersion,
|
||||
) -> Result<PathBuf, BackupError> {
|
||||
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> {
|
||||
CommonPath::Config.get().ok_or(BackupError::NotFound)
|
||||
}
|
||||
fn win_local_app_data_translate(
|
||||
&self,
|
||||
_path: &PathBuf,
|
||||
_game: &GameVersion,
|
||||
) -> Result<PathBuf, BackupError> {
|
||||
CommonPath::DataLocal.get().ok_or(BackupError::NotFound)
|
||||
}
|
||||
fn win_local_app_data_low_translate(
|
||||
&self,
|
||||
_path: &PathBuf,
|
||||
_game: &GameVersion,
|
||||
) -> Result<PathBuf, BackupError> {
|
||||
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> {
|
||||
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> {
|
||||
CommonPath::Public.get().ok_or(BackupError::NotFound)
|
||||
}
|
||||
}
|
||||
pub struct MacBackupManager {}
|
||||
impl BackupHandler for MacBackupManager {}
|
||||
7
cloud_saves/src/conditions.rs
Normal file
7
cloud_saves/src/conditions.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use database::platform::Platform;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Condition {
|
||||
Os(Platform),
|
||||
Other
|
||||
}
|
||||
27
cloud_saves/src/error.rs
Normal file
27
cloud_saves/src/error.rs
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
8
cloud_saves/src/lib.rs
Normal file
8
cloud_saves/src/lib.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod backup_manager;
|
||||
pub mod conditions;
|
||||
pub mod error;
|
||||
pub mod metadata;
|
||||
pub mod normalise;
|
||||
pub mod path;
|
||||
pub mod placeholder;
|
||||
pub mod resolver;
|
||||
36
cloud_saves/src/metadata.rs
Normal file
36
cloud_saves/src/metadata.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use database::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
cloud_saves/src/mod.rs
Normal file
7
cloud_saves/src/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;
|
||||
171
cloud_saves/src/normalise.rs
Normal file
171
cloud_saves/src/normalise.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use database::platform::Platform;
|
||||
use regex::Regex;
|
||||
|
||||
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
cloud_saves/src/path.rs
Normal file
48
cloud_saves/src/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
cloud_saves/src/placeholder.rs
Normal file
51
cloud_saves/src/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);
|
||||
216
cloud_saves/src/resolver.rs
Normal file
216
cloud_saves/src/resolver.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use std::{
|
||||
fs::{self, File, create_dir_all},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::error::BackupError;
|
||||
|
||||
use super::{backup_manager::BackupHandler, placeholder::*};
|
||||
use database::GameVersion;
|
||||
use log::{debug, warn};
|
||||
use rustix::path::Arg;
|
||||
use tempfile::tempfile;
|
||||
|
||||
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_all(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::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)
|
||||
}
|
||||
0
cloud_saves/src/strict_path.rs
Normal file
0
cloud_saves/src/strict_path.rs
Normal file
Reference in New Issue
Block a user