Compare commits

..

4 Commits

160 changed files with 2821 additions and 27617 deletions

View File

@ -14,8 +14,7 @@
"@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-os": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-shell": "^2.3.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-pretty": "^13.1.1", "pino-pretty": "^13.1.1"
"tauri": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.7.1" "@tauri-apps/cli": "^2.7.1"

2336
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,129 @@
[workspace] [package]
members = [ name = "drop-app"
"client", version = "0.3.3"
"database", description = "The client application for the open-source, self-hosted game distribution platform Drop"
"src-tauri", authors = ["Drop OSS"]
"process", edition = "2024"
"remote",
"utils", # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
"cloud_saves",
"download_manager", [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
"games", tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "drop_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
rustflags = ["-C", "target-feature=+aes,+sse2"]
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
[dependencies]
tauri-plugin-shell = "2.2.1"
serde_json = "1"
rayon = "1.10.0"
webbrowser = "1.0.2"
url = "2.5.2"
tauri-plugin-deep-link = "2"
log = "0.4.22"
hex = "0.4.3"
tauri-plugin-dialog = "2"
http = "1.1.0"
urlencoding = "2.1.3"
md5 = "0.7.0"
chrono = "0.4.38"
tauri-plugin-os = "2"
boxcar = "0.2.7"
umu-wrapper-lib = "0.1.0"
tauri-plugin-autostart = "2.0.0"
shared_child = "1.0.1"
serde_with = "3.12.0"
slice-deque = "0.3.0"
throttle_my_fn = "0.2.6"
parking_lot = "0.12.3"
atomic-instant-full = "0.1.0"
cacache = "13.1.0"
http-serde = "2.1.1"
reqwest-middleware = "0.4.0"
reqwest-middleware-cache = "0.1.1"
deranged = "=0.4.0"
droplet-rs = "0.7.3"
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.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"
futures-lite = "2.6.0"
page_size = "0.6.0"
sysinfo = "0.36.1"
humansize = "2.1.3"
tokio-util = { version = "0.7.16", features = ["io"] }
futures-core = "0.3.31"
bytes = "1.10.1"
# tailscale = { path = "./tailscale" }
[dependencies.dynfmt]
version = "0.1.5"
features = ["curly"]
[dependencies.tauri]
version = "2.7.0"
features = ["protocol-asset", "tray-icon"]
[dependencies.tokio]
version = "1.40.0"
features = ["rt", "tokio-macros", "signal"]
[dependencies.log4rs]
version = "1.3.0"
features = ["console_appender", "file_appender"]
[dependencies.rustix]
version = "0.38.37"
features = ["fs"]
[dependencies.uuid]
version = "1.10.0"
features = ["v4", "fast-rng", "macro-diagnostics"]
[dependencies.rustbreak]
version = "2"
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
[dependencies.reqwest]
version = "0.12.22"
default-features = false
features = [
"json",
"http2",
"blocking",
"rustls-tls",
"native-tls-alpn",
"rustls-tls-native-roots",
"stream",
] ]
resolver = "3" [dependencies.serde]
version = "1"
features = ["derive", "rc"]
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
[package]
name = "client"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "0.6.7"
database = { version = "0.1.0", path = "../database" }
log = "0.4.28"
serde = { version = "1.0.228", features = ["derive"] }
tauri = "2.8.5"
tauri-plugin-autostart = "2.5.0"

View File

@ -1,12 +0,0 @@
use serde::Serialize;
#[derive(Clone, Copy, Serialize, Eq, PartialEq)]
pub enum AppStatus {
NotConfigured,
Offline,
ServerError,
SignedOut,
SignedIn,
SignedInNeedsReauth,
ServerUnavailable,
}

View File

@ -1,3 +0,0 @@
pub mod autostart;
pub mod user;
pub mod app_status;

View File

@ -1,17 +0,0 @@
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct User {
id: String,
username: String,
admin: bool,
display_name: String,
profile_picture_object_id: String,
}
#[derive(Clone)]
pub struct CompatInfo {
umu_installed: bool,
}

View File

@ -1,18 +0,0 @@
[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"
tar = "0.4.44"
tempfile = "3.23.0"
uuid = "1.18.1"
whoami = "1.6.1"
zstd = "0.13.3"

View File

@ -1,5 +0,0 @@
pub enum BackupError {
InvalidSystem,
ParseError,
NotFound
}

View File

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

View File

@ -1,15 +0,0 @@
[package]
name = "database"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
dirs = "6.0.0"
log = "0.4.28"
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
rustbreak = "2.0.0"
serde = "1.0.228"
serde_with = "3.15.0"
url = "2.5.7"
whoami = "1.6.1"

View File

@ -1,47 +0,0 @@
use std::{
path::PathBuf,
sync::{Arc, LazyLock},
};
use rustbreak::{DeSerError, DeSerializer};
use serde::{Serialize, de::DeserializeOwned};
use crate::interface::{DatabaseImpls, DatabaseInterface};
pub static DB: LazyLock<DatabaseInterface> = LazyLock::new(DatabaseInterface::set_up_database);
#[cfg(not(debug_assertions))]
static DATA_ROOT_PREFIX: &str = "drop";
#[cfg(debug_assertions)]
static DATA_ROOT_PREFIX: &str = "drop-debug";
pub static DATA_ROOT_DIR: LazyLock<Arc<PathBuf>> = LazyLock::new(|| {
Arc::new(
dirs::data_dir()
.expect("Failed to get data dir")
.join(DATA_ROOT_PREFIX),
)
});
// Custom JSON serializer to support everything we need
#[derive(Debug, Default, Clone)]
pub struct DropDatabaseSerializer;
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
native_model::encode(val)
.map_err(|e| DeSerError::Internal(e.to_string()))
}
fn deserialize<R: std::io::Read>(&self, mut s: R) -> rustbreak::error::DeSerResult<T> {
let mut buf = Vec::new();
s.read_to_end(&mut buf)
.map_err(|e| rustbreak::error::DeSerError::Other(e.into()))?;
let (val, _version) = native_model::decode(buf)
.map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
}
}

View File

@ -1,21 +0,0 @@
#![feature(nonpoison_rwlock)]
pub mod db;
pub mod debug;
pub mod models;
pub mod platform;
pub mod interface;
pub use models::data::{
ApplicationTransientStatus,
Database,
DatabaseApplications,
DatabaseAuth,
DownloadType,
DownloadableMetadata,
GameDownloadStatus,
GameVersion,
Settings
};
pub use db::DB;
pub use interface::{borrow_db_checked, borrow_db_mut_checked};

View File

@ -1,47 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum Platform {
Windows,
Linux,
MacOs,
}
impl Platform {
#[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,
platform => unimplemented!("Playform {} is not supported", platform),
}
}
}

View File

@ -1,17 +0,0 @@
[package]
name = "download_manager"
version = "0.1.0"
edition = "2024"
[dependencies]
atomic-instant-full = "0.1.0"
database = { version = "0.1.0", path = "../database" }
humansize = "2.1.3"
log = "0.4.28"
parking_lot = "0.12.5"
remote = { version = "0.1.0", path = "../remote" }
serde = "1.0.228"
serde_with = "3.15.0"
tauri = "2.8.5"
throttle_my_fn = "0.2.6"
utils = { version = "0.1.0", path = "../utils" }

View File

@ -1,24 +0,0 @@
use database::DownloadableMetadata;
use serde::Serialize;
use crate::download_manager_frontend::DownloadStatus;
#[derive(Serialize, Clone)]
pub struct QueueUpdateEventQueueData {
pub meta: DownloadableMetadata,
pub status: DownloadStatus,
pub progress: f64,
pub current: usize,
pub max: usize,
}
#[derive(Serialize, Clone)]
pub struct QueueUpdateEvent {
pub queue: Vec<QueueUpdateEventQueueData>,
}
#[derive(serde::Serialize, Clone)]
pub struct StatsUpdateEvent {
pub speed: usize,
pub time: usize,
}

View File

@ -1,26 +0,0 @@
[package]
name = "games"
version = "0.1.0"
edition = "2024"
[dependencies]
atomic-instant-full = "0.1.0"
bitcode = "0.6.7"
boxcar = "0.2.14"
database = { version = "0.1.0", path = "../database" }
download_manager = { version = "0.1.0", path = "../download_manager" }
hex = "0.4.3"
log = "0.4.28"
md5 = "0.8.0"
rayon = "1.11.0"
remote = { version = "0.1.0", path = "../remote" }
reqwest = "0.12.23"
rustix = "1.1.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_with = "3.15.0"
sysinfo = "0.37.2"
tauri = "2.8.5"
throttle_my_fn = "0.2.6"
utils = { version = "0.1.0", path = "../utils" }
native_model = { version = "0.6.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
serde_json = "1.0.145"

View File

@ -1,21 +0,0 @@
use std::fmt::{Display};
use serde_with::SerializeDisplay;
#[derive(SerializeDisplay)]
pub enum LibraryError {
MetaNotFound(String),
VersionNotFound(String),
}
impl Display for LibraryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
LibraryError::MetaNotFound(id) => {
format!("Could not locate any installed version of game ID {id} in the database")
}
LibraryError::VersionNotFound(game_id) => {
format!("Could not locate any installed version for game id {game_id} in the database")
}
})
}
}

View File

@ -1,313 +0,0 @@
use std::fs::remove_dir_all;
use std::sync::Mutex;
use std::thread::spawn;
use bitcode::{Decode, Encode};
use database::{borrow_db_checked, borrow_db_mut_checked, ApplicationTransientStatus, Database, DownloadableMetadata, GameDownloadStatus, GameVersion};
use log::{debug, error, warn};
use remote::{auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url, utils::DROP_CLIENT_SYNC};
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use utils::app_emit;
use crate::{downloads::error::LibraryError, state::{GameStatusManager, GameStatusWithTransient}};
#[derive(Serialize, Deserialize, Debug)]
pub struct FetchGameStruct {
game: Game,
status: GameStatusWithTransient,
version: Option<GameVersion>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct Game {
id: String,
m_name: String,
m_short_description: String,
m_description: String,
// mDevelopers
// mPublishers
m_icon_object_id: String,
m_banner_object_id: String,
m_cover_object_id: String,
m_image_library_object_ids: Vec<String>,
m_image_carousel_object_ids: Vec<String>,
}
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {
pub game_id: String,
pub status: (
Option<GameDownloadStatus>,
Option<ApplicationTransientStatus>,
),
pub version: Option<GameVersion>,
}
/**
* Called by:
* - on_cancel, when cancelled, for obvious reasons
* - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though
* - when scanning, to import the game
*/
pub fn set_partially_installed(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle);
}
pub fn set_partially_installed_db(
db_lock: &mut Database,
meta: &DownloadableMetadata,
install_dir: String,
app_handle: Option<&AppHandle>,
) {
db_lock.applications.transient_statuses.remove(meta);
db_lock.applications.game_statuses.insert(
meta.id.clone(),
GameDownloadStatus::PartiallyInstalled {
version_name: meta.version.as_ref().unwrap().clone(),
install_dir,
},
);
db_lock
.applications
.installed_game_version
.insert(meta.id.clone(), meta.clone());
if let Some(app_handle) = app_handle {
push_game_update(
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, db_lock),
);
}
}
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
debug!("triggered uninstall for agent");
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.transient_statuses
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
push_game_update(
app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
);
let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
let previous_state = if let Some(state) = previous_state {
state
} else {
warn!("uninstall job doesn't have previous state, failing silently");
return;
};
if let Some((_, install_dir)) = match previous_state {
GameDownloadStatus::Installed {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::SetupRequired {
version_name,
install_dir,
} => Some((version_name, install_dir)),
GameDownloadStatus::PartiallyInstalled {
version_name,
install_dir,
} => Some((version_name, install_dir)),
_ => None,
} {
db_handle
.applications
.transient_statuses
.insert(meta.clone(), ApplicationTransientStatus::Uninstalling {});
drop(db_handle);
let app_handle = app_handle.clone();
spawn(move || {
if let Err(e) = remove_dir_all(install_dir) {
error!("{e}");
} else {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
.installed_game_version
.remove(&meta.id);
db_handle
.applications
.game_statuses
.insert(meta.id.clone(), GameDownloadStatus::Remote {});
let _ = db_handle.applications.transient_statuses.remove(&meta);
push_game_update(
&app_handle,
&meta.id,
None,
GameStatusManager::fetch_state(&meta.id, &db_handle),
);
debug!("uninstalled game id {}", &meta.id);
app_emit!(app_handle, "update_library", ());
}
});
} else {
warn!("invalid previous state for uninstall, failing silently.");
}
}
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
borrow_db_checked()
.applications
.installed_game_version
.get(game_id)
.cloned()
}
pub fn on_game_complete(
meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
if meta.version.is_none() {
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = DROP_CLIENT_SYNC.clone();
let response = generate_url(
&["/api/v1/client/game/version"],
&[
("id", &meta.id),
("version", meta.version.as_ref().unwrap()),
],
)?;
let response = client
.get(response)
.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());
drop(handle);
let status = if game_version.setup_command.is_empty() {
GameDownloadStatus::Installed {
version_name: meta.version.clone().unwrap(),
install_dir,
}
} else {
GameDownloadStatus::SetupRequired {
version_name: meta.version.clone().unwrap(),
install_dir,
}
};
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.game_statuses
.insert(meta.id.clone(), status.clone());
drop(db_handle);
app_emit!(
app_handle,
&format!("update_game/{}", meta.id),
GameUpdateEvent {
game_id: meta.id.clone(),
status: (Some(status), None),
version: Some(game_version),
}
);
Ok(())
}
pub fn push_game_update(
app_handle: &AppHandle,
game_id: &String,
version: Option<GameVersion>,
status: GameStatusWithTransient,
) {
if let Some(GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }) =
&status.0
&& version.is_none()
{
panic!("pushed game for installed game that doesn't have version information");
}
app_emit!(
app_handle,
&format!("update_game/{game_id}"),
GameUpdateEvent {
game_id: game_id.clone(),
status,
version,
}
);
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendGameOptions {
launch_string: String,
}
#[tauri::command]
pub fn update_game_configuration(
game_id: String,
options: FrontendGameOptions,
) -> Result<(), LibraryError> {
let mut handle = borrow_db_mut_checked();
let installed_version = handle
.applications
.installed_game_version
.get(&game_id)
.ok_or(LibraryError::MetaNotFound(game_id))?;
let id = installed_version.id.clone();
let version = installed_version.version.clone().ok_or(LibraryError::VersionNotFound(id.clone()))?;
let mut existing_configuration = handle
.applications
.game_versions
.get(&id)
.unwrap()
.get(&version)
.unwrap()
.clone();
// Add more options in here
existing_configuration.launch_command_template = options.launch_string;
// Add no more options past here
handle
.applications
.game_versions
.get_mut(&id)
.unwrap()
.insert(version.to_string(), existing_configuration);
Ok(())
}

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 911 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 803 B

After

Width:  |  Height:  |  Size: 803 B

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 515 B

View File

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 944 B

View File

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 944 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 749 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 944 B

After

Width:  |  Height:  |  Size: 944 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,17 +0,0 @@
[package]
name = "process"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.42"
client = { version = "0.1.0", path = "../client" }
database = { version = "0.1.0", path = "../database" }
dynfmt = "0.1.5"
log = "0.4.28"
serde = "1.0.228"
serde_with = "3.15.0"
shared_child = "1.1.1"
tauri = "2.8.5"
tauri-plugin-opener = "2.5.0"
utils = { version = "0.1.0", path = "../utils" }

View File

@ -1,23 +0,0 @@
[package]
name = "remote"
version = "0.1.0"
edition = "2024"
[dependencies]
bitcode = "0.6.7"
chrono = "0.4.42"
client = { version = "0.1.0", path = "../client" }
database = { version = "0.1.0", path = "../database" }
droplet-rs = "0.7.3"
gethostname = "1.0.2"
hex = "0.4.3"
http = "1.3.1"
log = "0.4.28"
md5 = "0.8.0"
reqwest = "0.12.23"
reqwest-websocket = "0.5.1"
serde = "1.0.228"
serde_with = "3.15.0"
tauri = "2.8.5"
url = "2.5.7"
utils = { version = "0.1.0", path = "../utils" }

View File

@ -1,136 +0,0 @@
use std::{collections::HashMap, env};
use chrono::Utc;
use client::{app_status::AppStatus, user::User};
use database::interface::borrow_db_checked;
use droplet_rs::ssl::sign_nonce;
use gethostname::gethostname;
use log::{error, warn};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{error::{DropServerError, RemoteAccessError}, requests::make_authenticated_get, utils::DROP_CLIENT_SYNC};
use super::{
cache::{cache_object, get_cached_object},
requests::generate_url,
};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CapabilityConfiguration {}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct InitiateRequestBody {
name: String,
platform: String,
capabilities: HashMap<String, CapabilityConfiguration>,
mode: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct HandshakeRequestBody {
client_id: String,
token: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct HandshakeResponse {
private: String,
certificate: String,
id: String,
}
pub fn generate_authorization_header() -> String {
let certs = {
let db = borrow_db_checked();
db.auth.clone().expect("Authorisation not initialised")
};
let nonce = Utc::now().timestamp_millis().to_string();
let signature =
sign_nonce(certs.private, nonce.clone()).expect("Failed to generate authorisation header");
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
}
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
if response.status() != 200 {
let err: DropServerError = response.json().await?;
warn!("{err:?}");
if err.status_message == "Nonce expired" {
return Err(RemoteAccessError::OutOfSync);
}
return Err(RemoteAccessError::InvalidResponse(err));
}
response
.json::<User>()
.await
.map_err(std::convert::Into::into)
}
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
let hostname = gethostname();
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
let body = InitiateRequestBody {
name: format!("{} (Desktop)", hostname.display()),
platform: env::consts::OS.to_string(),
capabilities: HashMap::from([
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
]),
mode,
};
let client = DROP_CLIENT_SYNC.clone();
let response = client.post(endpoint.to_string()).json(&body).send()?;
if response.status() != 200 {
let data: DropServerError = response.json()?;
error!("could not start handshake: {}", data.status_message);
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
let response = response.text()?;
Ok(response)
}
pub async fn setup() -> (AppStatus, Option<User>) {
let auth = {
let data = borrow_db_checked();
data.auth.clone()
};
if auth.is_some() {
let user_result = match fetch_user().await {
Ok(data) => data,
Err(RemoteAccessError::FetchError(_)) => {
let user = get_cached_object::<User>("user").ok();
return (AppStatus::Offline, user);
}
Err(_) => return (AppStatus::SignedInNeedsReauth, None),
};
if let Err(e) = cache_object("user", &user_result) {
warn!("Could not cache user object with error {e}");
}
return (AppStatus::SignedIn, Some(user_result));
}
(AppStatus::SignedOut, None)
}

View File

@ -1,76 +0,0 @@
use database::{interface::DatabaseImpls, DB};
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
use log::{debug, warn};
use tauri::UriSchemeResponder;
use crate::{error::CacheError, utils::DROP_CLIENT_ASYNC};
use super::{
auth::generate_authorization_header,
cache::{ObjectCache, cache_object, get_cached_object},
};
pub async fn fetch_object_wrapper(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
match fetch_object(request).await {
Ok(r) => responder.respond(r),
Err(e) => {
warn!("Cache error: {e}");
responder.respond(Response::builder().status(500).body(Vec::new()).expect("Failed to build error response"));
}
};
}
pub async fn fetch_object(request: http::Request<Vec<u8>>) -> Result<Response<Vec<u8>>, CacheError>
{
// Drop leading /
let object_id = &request.uri().path()[1..];
let cache_result = get_cached_object::<ObjectCache>(object_id);
if let Ok(cache_result) = &cache_result
&& !cache_result.has_expired()
{
return cache_result.try_into();
}
let header = generate_authorization_header();
let client = DROP_CLIENT_ASYNC.clone();
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
let response = client.get(url).header("Authorization", header).send().await;
match response {
Ok(r) => {
let resp_builder = ResponseBuilder::new().header(
CONTENT_TYPE,
r.headers()
.get("Content-Type")
.expect("Failed get Content-Type header"),
);
let data = match r.bytes().await {
Ok(data) => Vec::from(data),
Err(e) => {
warn!(
"Could not get data from cache object {object_id} with error {e}",
);
Vec::new()
}
};
let resp = resp_builder.body(data).expect("Failed to build object cache response body");
if cache_result.map_or(true, |x| x.has_expired()) {
cache_object::<ObjectCache>(object_id, &resp.clone().try_into()?)
.expect("Failed to create cached object");
}
Ok(resp)
}
Err(e) => {
debug!("Object fetch failed with error {e}. Attempting to download from cache");
match cache_result {
Ok(cache_result) => cache_result.try_into(),
Err(e) => {
warn!("{e}");
Err(CacheError::Remote(e))
}
}
}
}
}

View File

@ -1,93 +0,0 @@
use std::str::FromStr;
use database::borrow_db_checked;
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
use log::{error, warn};
use tauri::UriSchemeResponder;
use utils::webbrowser_open::webbrowser_open;
use crate::utils::DROP_CLIENT_SYNC;
pub async fn handle_server_proto_offline_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
responder.respond(match handle_server_proto_offline(request).await {
Ok(res) => res,
Err(_) => unreachable!()
});
}
pub async fn handle_server_proto_offline(_request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode>{
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Vec::new())
.expect("Failed to build error response for proto offline"))
}
pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: UriSchemeResponder) {
match handle_server_proto(request).await {
Ok(r) => responder.respond(r),
Err(e) => {
warn!("Cache error: {e}");
responder.respond(Response::builder().status(e).body(Vec::new()).expect("Failed to build error response"));
}
}
}
async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode> {
let db_handle = borrow_db_checked();
let auth = match db_handle.auth.as_ref() {
Some(auth) => auth,
None => {
error!("Could not find auth in database");
return Err(StatusCode::UNAUTHORIZED)
}
};
let web_token = match &auth.web_token {
Some(token) => token,
None => return Err(StatusCode::UNAUTHORIZED),
};
let remote_uri = db_handle.base_url.parse::<Uri>().expect("Failed to parse base url");
let path = request.uri().path();
let mut new_uri = request.uri().clone().into_parts();
new_uri.path_and_query =
Some(PathAndQuery::from_str(&format!("{path}?noWrapper=true")).expect("Failed to parse request path in proto"));
new_uri.authority = remote_uri.authority().cloned();
new_uri.scheme = remote_uri.scheme().cloned();
let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
webbrowser_open(new_uri.to_string());
return Ok(Response::new(Vec::new()))
}
let client = DROP_CLIENT_SYNC.clone();
let response = match client
.request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {web_token}"))
.headers(request.headers().clone())
.send() {
Ok(response) => response,
Err(e) => {
warn!("Could not send response. Got {e} when sending");
return Err(e.status().unwrap_or(StatusCode::BAD_REQUEST))
},
};
let response_status = response.status();
let response_body = match response.bytes() {
Ok(bytes) => bytes,
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
};
let http_response = Response::builder()
.status(response_status)
.body(response_body.to_vec())
.expect("Failed to build server proto response");
Ok(http_response)
}

View File

@ -1,107 +0,0 @@
use std::{
fs::{self, File},
io::Read,
sync::LazyLock,
};
use database::db::DATA_ROOT_DIR;
use log::{debug, info, warn};
use reqwest::Certificate;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
app_name: String,
}
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
fn fetch_certificates() -> Vec<Certificate> {
let certificate_dir = DATA_ROOT_DIR.join("certificates");
let mut certs = Vec::new();
match fs::read_dir(certificate_dir) {
Ok(c) => {
for entry in c {
match entry {
Ok(c) => {
let mut buf = Vec::new();
match File::open(c.path()) {
Ok(f) => f,
Err(e) => {
warn!(
"Failed to open file at {} with error {}",
c.path().display(),
e
);
continue;
}
}
.read_to_end(&mut buf)
.unwrap_or_else(|e| panic!(
"Failed to read to end of certificate file {} with error {}",
c.path().display(),
e
));
match Certificate::from_pem_bundle(&buf) {
Ok(certificates) => {
for cert in certificates {
certs.push(cert);
}
info!(
"added {} certificate(s) from {}",
certs.len(),
c.file_name().display()
);
}
Err(e) => warn!(
"Invalid certificate file {} with error {}",
c.path().display(),
e
),
}
}
Err(_) => todo!(),
}
}
}
Err(e) => {
debug!("not loading certificates due to error: {e}");
}
};
certs
}
pub fn get_client_sync() -> reqwest::blocking::Client {
let mut client = reqwest::blocking::ClientBuilder::new();
for cert in DROP_CERT_BUNDLE.iter() {
client = client.add_root_certificate(cert.clone());
}
client.use_rustls_tls().build().expect("Failed to build synchronous client")
}
pub fn get_client_async() -> reqwest::Client {
let mut client = reqwest::ClientBuilder::new();
for cert in DROP_CERT_BUNDLE.iter() {
client = client.add_root_certificate(cert.clone());
}
client.use_rustls_tls().build().expect("Failed to build asynchronous client")
}
pub fn get_client_ws() -> reqwest::Client {
let mut client = reqwest::ClientBuilder::new();
for cert in DROP_CERT_BUNDLE.iter() {
client = client.add_root_certificate(cert.clone());
}
client
.use_rustls_tls()
.http1_only()
.build()
.expect("Failed to build websocket client")
}

File diff suppressed because it is too large Load Diff

View File

@ -1,137 +0,0 @@
[package]
name = "drop-app"
version = "0.3.3"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "drop_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
rustflags = ["-C", "target-feature=+aes,+sse2"]
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
[dependencies]
tauri-plugin-shell = "2.2.1"
serde_json = "1"
rayon = "1.10.0"
webbrowser = "1.0.2"
url = "2.5.2"
tauri-plugin-deep-link = "2"
log = "0.4.22"
hex = "0.4.3"
tauri-plugin-dialog = "2"
http = "1.1.0"
urlencoding = "2.1.3"
md5 = "0.7.0"
chrono = "0.4.38"
tauri-plugin-os = "2"
boxcar = "0.2.7"
umu-wrapper-lib = "0.1.0"
tauri-plugin-autostart = "2.0.0"
shared_child = "1.0.1"
serde_with = "3.12.0"
slice-deque = "0.3.0"
throttle_my_fn = "0.2.6"
parking_lot = "0.12.3"
atomic-instant-full = "0.1.0"
cacache = "13.1.0"
http-serde = "2.1.1"
reqwest-middleware = "0.4.0"
reqwest-middleware-cache = "0.1.1"
deranged = "=0.4.0"
droplet-rs = "0.7.3"
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.4", features = ["rmp_serde_1_3"], git = "https://github.com/Drop-OSS/native_model.git"}
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"
futures-lite = "2.6.0"
page_size = "0.6.0"
sysinfo = "0.36.1"
humansize = "2.1.3"
tokio-util = { version = "0.7.16", features = ["io"] }
futures-core = "0.3.31"
bytes = "1.10.1"
# tailscale = { path = "./tailscale" }
# Workspaces
client = { path = "../client" }
database = { path = "../database" }
process = { path = "../process" }
remote = { path = "../remote" }
utils = { path = "../utils" }
[dependencies.dynfmt]
version = "0.1.5"
features = ["curly"]
[dependencies.tauri]
version = "2.7.0"
features = ["protocol-asset", "tray-icon"]
[dependencies.tokio]
version = "1.40.0"
features = ["rt", "tokio-macros", "signal"]
[dependencies.log4rs]
version = "1.3.0"
features = ["console_appender", "file_appender"]
[dependencies.rustix]
version = "0.38.37"
features = ["fs"]
[dependencies.uuid]
version = "1.10.0"
features = ["v4", "fast-rng", "macro-diagnostics"]
[dependencies.rustbreak]
version = "2"
features = ["other_errors"] # You can also use "yaml_enc" or "bin_enc"
[dependencies.reqwest]
version = "0.12.22"
default-features = false
features = [
"json",
"http2",
"blocking",
"rustls-tls",
"native-tls-alpn",
"rustls-tls-native-roots",
"stream",
]
[dependencies.serde]
version = "1"
features = ["derive", "rc"]
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
use crate::{lock, AppState};
#[tauri::command]
pub fn fetch_state(
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
) -> Result<String, String> {
let guard = lock!(state);
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}
#[tauri::command]
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
cleanup_and_exit(&app, &state);
}
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
debug!("cleaning up and exiting application");
let download_manager = lock!(state).download_manager.clone();
match download_manager.ensure_terminated() {
Ok(res) => match res {
Ok(()) => debug!("download manager terminated correctly"),
Err(()) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{e:?}"),
}
app.exit(0);
}
#[tauri::command]
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
toggle_autostart_logic(app, enabled)
}
#[tauri::command]
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
get_autostart_enabled_logic(app)
}

View File

@ -1,318 +0,0 @@
use std::sync::Mutex;
use tauri::AppHandle;
use crate::{
AppState,
database::{
db::borrow_db_checked,
models::data::GameVersion,
},
error::{library_error::LibraryError, remote_access_error::RemoteAccessError},
games::library::{
fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta,
uninstall_game_logic,
},
offline,
};
use super::{
library::{
FetchGameStruct, Game, fetch_game_logic, fetch_game_version_options_logic,
fetch_library_logic,
},
state::{GameStatusManager, GameStatusWithTransient},
};
#[tauri::command]
pub async fn fetch_library(
state: tauri::State<'_, Mutex<AppState<'_>>>,
hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
offline!(
state,
fetch_library_logic,
fetch_library_logic_offline,
state,
hard_refresh
).await
}
pub async fn fetch_library_logic(
state: tauri::State<'_, Mutex<AppState<'_>>>,
hard_fresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let do_hard_refresh = hard_fresh.unwrap_or(false);
if !do_hard_refresh && let Ok(library) = get_cached_object("library") {
return Ok(library);
}
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/user/library"], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.send()
.await?;
if response.status() != 200 {
let err = response.json().await.unwrap_or(DropServerError {
status_code: 500,
status_message: "Invalid response from server.".to_owned(),
});
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let mut games: Vec<Game> = response.json().await?;
let mut handle = lock!(state);
let mut db_handle = borrow_db_mut_checked();
for game in &games {
handle.games.insert(game.id.clone(), game.clone());
if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle
.applications
.game_statuses
.insert(game.id.clone(), GameDownloadStatus::Remote {});
}
}
// Add games that are installed but no longer in library
for meta in db_handle.applications.installed_game_version.values() {
if games.iter().any(|e| e.id == meta.id) {
continue;
}
// We should always have a cache of the object
// Pass db_handle because otherwise we get a gridlock
let game = match get_cached_object_db::<Game>(&meta.id.clone(), &db_handle) {
Ok(game) => game,
Err(err) => {
warn!(
"{} is installed, but encountered error fetching its error: {}.",
meta.id, err
);
continue;
}
};
games.push(game);
}
drop(handle);
drop(db_handle);
cache_object("library", &games)?;
Ok(games)
}
pub async fn fetch_library_logic_offline(
_state: tauri::State<'_, Mutex<AppState<'_>>>,
_hard_refresh: Option<bool>,
) -> Result<Vec<Game>, RemoteAccessError> {
let mut games: Vec<Game> = get_cached_object("library")?;
let db_handle = borrow_db_checked();
games.retain(|game| {
matches!(
&db_handle
.applications
.game_statuses
.get(&game.id)
.unwrap_or(&GameDownloadStatus::Remote {}),
GameDownloadStatus::Installed { .. } | GameDownloadStatus::SetupRequired { .. }
)
});
Ok(games)
}
pub async fn fetch_game_logic(
id: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let version = {
let state_handle = lock!(state);
let db_lock = borrow_db_checked();
let metadata_option = db_lock.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => db_lock
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.cloned(),
};
let game = state_handle.games.get(&id);
if let Some(game) = game {
let status = GameStatusManager::fetch_state(&id, &db_lock);
let data = FetchGameStruct {
game: game.clone(),
status,
version,
};
cache_object_db(&id, game, &db_lock)?;
return Ok(data);
}
version
};
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/", &id], &[])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.send()
.await?;
if response.status() == 404 {
let offline_fetch = fetch_game_logic_offline(id.clone(), state).await;
if let Ok(fetch_data) = offline_fetch {
return Ok(fetch_data);
}
return Err(RemoteAccessError::GameNotFound(id));
}
if response.status() != 200 {
let err = response.json().await?;
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let game: Game = response.json().await?;
let mut state_handle = lock!(state);
state_handle.games.insert(id.clone(), game.clone());
let mut db_handle = borrow_db_mut_checked();
db_handle
.applications
.game_statuses
.entry(id.clone())
.or_insert(GameDownloadStatus::Remote {});
let status = GameStatusManager::fetch_state(&id, &db_handle);
drop(db_handle);
let data = FetchGameStruct {
game: game.clone(),
status,
version,
};
cache_object(&id, &game)?;
Ok(data)
}
pub async fn fetch_game_version_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = DROP_CLIENT_ASYNC.clone();
let response = generate_url(&["/api/v1/client/game/versions"], &[("id", &game_id)])?;
let response = client
.get(response)
.header("Authorization", generate_authorization_header())
.send()
.await?;
if response.status() != 200 {
let err = response.json().await?;
warn!("{err:?}");
return Err(RemoteAccessError::InvalidResponse(err));
}
let data: Vec<GameVersion> = response.json().await?;
let state_lock = lock!(state);
let process_manager_lock = lock!(state_lock.process_manager);
let data: Vec<GameVersion> = data
.into_iter()
.filter(|v| process_manager_lock.valid_platform(&v.platform, &state_lock))
.collect();
drop(process_manager_lock);
drop(state_lock);
Ok(data)
}
pub async fn fetch_game_logic_offline(
id: String,
_state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
let db_handle = borrow_db_checked();
let metadata_option = db_handle.applications.installed_game_version.get(&id);
let version = match metadata_option {
None => None,
Some(metadata) => db_handle
.applications
.game_versions
.get(&metadata.id)
.map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap())
.cloned(),
};
let status = GameStatusManager::fetch_state(&id, &db_handle);
let game = get_cached_object::<Game>(&id)?;
drop(db_handle);
Ok(FetchGameStruct {
game,
status,
version,
})
}
#[tauri::command]
pub async fn fetch_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
offline!(
state,
fetch_game_logic,
fetch_game_logic_offline,
game_id,
state
).await
}
#[tauri::command]
pub fn fetch_game_status(id: String) -> GameStatusWithTransient {
let db_handle = borrow_db_checked();
GameStatusManager::fetch_state(&id, &db_handle)
}
#[tauri::command]
pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> {
let meta = match get_current_meta(&game_id) {
Some(data) => data,
None => return Err(LibraryError::MetaNotFound(game_id)),
};
uninstall_game_logic(meta, &app_handle);
Ok(())
}
#[tauri::command]
pub async fn fetch_game_version_options(
game_id: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
fetch_game_version_options_logic(game_id, state).await
}

View File

@ -1,4 +1,4 @@
use database::{borrow_db_checked, borrow_db_mut_checked}; 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;
@ -64,3 +64,12 @@ pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
Ok(()) Ok(())
} }
#[tauri::command]
pub fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
toggle_autostart_logic(app, enabled)
}
#[tauri::command]
pub fn get_autostart_enabled(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
get_autostart_enabled_logic(app)
}

View File

@ -0,0 +1,23 @@
use log::{debug, error};
use tauri::AppHandle;
use crate::AppState;
#[tauri::command]
pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
cleanup_and_exit(&app, &state);
}
pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex<AppState<'_>>>) {
debug!("cleaning up and exiting application");
let download_manager = state.lock().unwrap().download_manager.clone();
match download_manager.ensure_terminated() {
Ok(res) => match res {
Ok(()) => debug!("download manager terminated correctly"),
Err(()) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{e:?}"),
}
app.exit(0);
}

View File

@ -0,0 +1,11 @@
use crate::AppState;
#[tauri::command]
pub fn fetch_state(
state: tauri::State<'_, std::sync::Mutex<AppState<'_>>>,
) -> Result<String, String> {
let guard = state.lock().unwrap();
let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}

View File

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

View File

@ -1,11 +1,8 @@
use std::{collections::HashMap, path::PathBuf, str::FromStr}; use std::{collections::HashMap, path::PathBuf, str::FromStr};
#[cfg(target_os = "linux")]
use database::platform::Platform;
use database::{db::DATA_ROOT_DIR, GameVersion};
use log::warn; use log::warn;
use crate::error::BackupError; use crate::{database::db::{GameVersion, DATA_ROOT_DIR}, error::backup_error::BackupError, process::process_manager::Platform};
use super::path::CommonPath; use super::path::CommonPath;
@ -48,7 +45,7 @@ impl BackupManager<'_> {
} }
pub trait BackupHandler: Send + Sync { pub trait BackupHandler: Send + Sync {
fn root_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { Ok(DATA_ROOT_DIR.join("games")) } 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 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 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 home_translate(&self, _path: &PathBuf, _game: &GameVersion) -> Result<PathBuf, BackupError> { let c = CommonPath::Home.get().ok_or(BackupError::NotFound); println!("{:?}", c); c }

View File

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

View File

@ -1,4 +1,4 @@
use database::GameVersion; use crate::database::db::GameVersion;
use super::conditions::{Condition}; use super::conditions::{Condition};

View File

@ -1,7 +1,7 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use database::platform::Platform;
use regex::Regex; use regex::Regex;
use crate::process::process_manager::Platform;
use super::placeholder::*; use super::placeholder::*;

View File

@ -6,16 +6,17 @@ use std::{
time::Duration, time::Duration,
}; };
use crate::error::BackupError;
use super::{ use super::{
backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*, backup_manager::BackupHandler, conditions::Condition, metadata::GameFile, placeholder::*,
}; };
use database::{platform::Platform, GameVersion};
use log::{debug, warn}; use log::{debug, warn};
use rustix::path::Arg; use rustix::path::Arg;
use tempfile::tempfile; 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}; use super::{backup_manager::BackupManager, metadata::CloudSaveMetadata, normalise::normalize};
pub fn resolve(meta: &mut CloudSaveMetadata) -> File { pub fn resolve(meta: &mut CloudSaveMetadata) -> File {

View File

@ -4,16 +4,14 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use log::error;
use serde_json::Value; use serde_json::Value;
use crate::{ use crate::{
database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, database::{db::borrow_db_mut_checked, scan::scan_install_dirs}, error::download_manager_error::DownloadManagerError,
error::download_manager_error::DownloadManagerError,
}; };
use super::{ use super::{
db::{DATA_ROOT_DIR, borrow_db_checked}, db::{borrow_db_checked, DATA_ROOT_DIR},
debug::SystemData, debug::SystemData,
models::data::Settings, models::data::Settings,
}; };
@ -69,25 +67,11 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), DownloadManagerError<()>
#[tauri::command] #[tauri::command]
pub fn update_settings(new_settings: Value) { pub fn update_settings(new_settings: Value) {
let mut db_lock = borrow_db_mut_checked(); let mut db_lock = borrow_db_mut_checked();
let mut current_settings = let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
serde_json::to_value(db_lock.settings.clone()).expect("Failed to parse existing settings"); for (key, value) in new_settings.as_object().unwrap() {
let values = match new_settings.as_object() {
Some(values) => values,
None => {
error!("Could not parse settings values into object");
return;
}
};
for (key, value) in values {
current_settings[key] = value.clone(); current_settings[key] = value.clone();
} }
let new_settings: Settings = match serde_json::from_value(current_settings) { let new_settings: Settings = serde_json::from_value(current_settings).unwrap();
Ok(settings) => settings,
Err(e) => {
error!("Could not parse settings with error {}", e);
return;
}
};
db_lock.settings = new_settings; db_lock.settings = new_settings;
} }
#[tauri::command] #[tauri::command]

Some files were not shown because too many files have changed in this diff Show More