feat: move to native_model to allow for database upgrades

This commit is contained in:
DecDuck
2025-05-15 10:13:24 +10:00
parent 02edb2cbc1
commit 790e8c2afe
24 changed files with 298 additions and 365 deletions

View File

@ -7,13 +7,13 @@ use std::{
use serde_json::Value;
use crate::{
database::{db::borrow_db_mut_checked, settings::Settings},
database::{db::borrow_db_mut_checked},
download_manager::internal_error::InternalError,
};
use super::{
db::{borrow_db_checked, save_db, DATA_ROOT_DIR},
debug::SystemData,
debug::SystemData, models::data::Settings,
};
// Will, in future, return disk/remaining size

View File

@ -15,122 +15,10 @@ use serde_with::serde_as;
use tauri::AppHandle;
use url::Url;
use crate::{
database::settings::Settings,
download_manager::downloadable_metadata::DownloadableMetadata,
games::{library::push_game_update, state::GameStatusManager},
process::process_manager::Platform,
DB,
};
use crate::DB;
#[derive(serde::Serialize, Clone, Deserialize)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
use super::models::data::{Database, GameVersion};
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Running {},
}
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String,
pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
impl Database {
fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
}
}
}
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
@ -138,13 +26,20 @@ pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
#[derive(Debug, Default, Clone)]
pub struct DropDatabaseSerializer;
impl<T: Serialize + DeserializeOwned> DeSerializer<T> for DropDatabaseSerializer {
impl<T: native_model::Model + Serialize + DeserializeOwned> DeSerializer<T>
for DropDatabaseSerializer
{
fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult<Vec<u8>> {
serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string()))
native_model::encode(val).map_err(|e| DeSerError::Internal(e.to_string()))
}
fn deserialize<R: std::io::Read>(&self, s: R) -> rustbreak::error::DeSerResult<T> {
serde_json::from_reader(s).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::<T>(buf).map_err(|e| DeSerError::Internal(e.to_string()))?;
Ok(val)
}
}

View File

@ -1,4 +1,4 @@
pub mod commands;
pub mod db;
pub mod debug;
pub mod settings;
pub mod models;

View File

@ -0,0 +1,184 @@
pub mod data {
use native_model::{native_model, Model};
use serde::{Deserialize, Serialize};
pub type GameVersion = v1::GameVersion;
pub type Database = v1::Database;
pub type Settings = v1::Settings;
pub type DatabaseAuth = v1::DatabaseAuth;
pub type GameDownloadStatus = v1::GameDownloadStatus;
pub type ApplicationTransientStatus = v1::ApplicationTransientStatus;
pub type DownloadableMetadata = v1::DownloadableMetadata;
pub type DownloadType = v1::DownloadType;
pub mod v1 {
use crate::process::process_manager::Platform;
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use super::*;
fn default_template() -> String {
"{}".to_owned()
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 2, version = 1)]
pub struct GameVersion {
pub game_id: String,
pub version_name: String,
pub platform: Platform,
pub launch_command: String,
pub launch_args: Vec<String>,
#[serde(default = "default_template")]
pub launch_command_template: String,
pub setup_command: String,
pub setup_args: Vec<String>,
#[serde(default = "default_template")]
pub setup_command_template: String,
pub only_setup: bool,
pub version_index: usize,
pub delta: bool,
pub umu_id_override: Option<String>,
}
#[serde_as]
#[derive(Serialize, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 3, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
pub struct DatabaseApplications {
pub install_dirs: Vec<PathBuf>,
// Guaranteed to exist if the game also exists in the app state map
pub game_statuses: HashMap<String, GameDownloadStatus>,
pub game_versions: HashMap<String, HashMap<String, GameVersion>>,
pub installed_game_version: HashMap<String, DownloadableMetadata>,
#[serde(skip)]
pub transient_statuses: HashMap<DownloadableMetadata, ApplicationTransientStatus>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[native_model(id = 4, version = 1)]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool, // ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false,
}
}
}
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
#[native_model(id = 5, version = 1)]
pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
}
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize, Deserialize)]
pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
Running {},
}
#[derive(serde::Serialize, Clone, Deserialize)]
#[native_model(id = 6, version = 1)]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
pub client_id: String,
pub web_token: Option<String>,
}
#[native_model(id = 8, version = 1)]
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy,
)]
pub enum DownloadType {
Game,
Tool,
DLC,
Mod,
}
#[native_model(id = 7, version = 1)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}
#[native_model(id = 1, version = 1)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
#[serde(default)]
pub settings: Settings,
pub auth: Option<DatabaseAuth>,
pub base_url: String,
pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>,
pub cache_dir: PathBuf,
}
impl Database {
pub fn new<T: Into<PathBuf>>(
games_base_dir: T,
prev_database: Option<PathBuf>,
cache_dir: PathBuf,
) -> Self {
Self {
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.into()],
game_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
auth: None,
settings: Settings::default(),
cache_dir,
}
}
}
}
}

View File

@ -1,26 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub autostart: bool,
pub max_download_threads: usize,
pub force_offline: bool
// ... other settings ...
}
impl Default for Settings {
fn default() -> Self {
Self {
autostart: false,
max_download_threads: 4,
force_offline: false
}
}
}
// Ideally use pointers instead of a macro to assign the settings
// fn deserialize_into<T>(v: serde_json::Value, t: &mut T) -> Result<(), serde_json::Error>
// where T: for<'a> Deserialize<'a>
// {
// *t = serde_json::from_value(v)?;
// Ok(())
// }

View File

@ -1,6 +1,6 @@
use std::sync::Mutex;
use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState};
use crate::{database::models::data::DownloadableMetadata, AppState};
#[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {

View File

@ -12,11 +12,10 @@ use std::{
use log::{debug, info};
use serde::Serialize;
use crate::error::application_download_error::ApplicationDownloadError;
use crate::{database::models::data::DownloadableMetadata, error::application_download_error::ApplicationDownloadError};
use super::{
download_manager_builder::{CurrentProgressObject, DownloadAgent},
downloadable_metadata::DownloadableMetadata,
queue::Queue,
};
@ -167,10 +166,7 @@ impl DownloadManager {
self.command_sender
.send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
self.command_sender
.send(DownloadManagerSignal::Go)
.unwrap();
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
pub fn pause_downloads(&self) {
self.command_sender

View File

@ -11,15 +11,13 @@ use log::{debug, error, info, warn};
use tauri::{AppHandle, Emitter};
use crate::{
error::application_download_error::ApplicationDownloadError,
games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent},
database::models::data::DownloadableMetadata, error::application_download_error::ApplicationDownloadError, games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent}
};
use super::{
download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus},
download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag},
downloadable::Downloadable,
downloadable_metadata::DownloadableMetadata,
progress_object::ProgressObject,
queue::Queue,
};
@ -209,7 +207,9 @@ impl DownloadManagerBuilder {
}
if self.current_download_agent.is_some() {
if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() {
if self.download_queue.read().front().unwrap()
== &self.current_download_agent.as_ref().unwrap().metadata()
{
debug!(
"Current download agent: {:?}",
self.current_download_agent.as_ref().unwrap().metadata()

View File

@ -2,11 +2,11 @@ use std::sync::Arc;
use tauri::AppHandle;
use crate::error::application_download_error::ApplicationDownloadError;
use crate::{database::models::data::DownloadableMetadata, error::application_download_error::ApplicationDownloadError};
use super::{
download_manager::DownloadStatus, download_thread_control_flag::DownloadThreadControl,
downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject,
progress_object::ProgressObject,
};
pub trait Downloadable: Send + Sync {

View File

@ -1,26 +1 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
pub enum DownloadType {
Game,
Tool,
DLC,
Mod,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DownloadableMetadata {
pub id: String,
pub version: Option<String>,
pub download_type: DownloadType,
}
impl DownloadableMetadata {
pub fn new(id: String, version: Option<String>, download_type: DownloadType) -> Self {
Self {
id,
version,
download_type,
}
}
}
use serde::{Deserialize, Serialize};

View File

@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex, MutexGuard},
};
use super::downloadable_metadata::DownloadableMetadata;
use crate::database::models::data::DownloadableMetadata;
#[derive(Clone)]
pub struct Queue {

View File

@ -3,7 +3,13 @@ use std::sync::Mutex;
use tauri::{AppHandle, Manager};
use crate::{
database::db::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, AppState
database::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, AppState,
};
use super::{
@ -15,16 +21,29 @@ use super::{
};
#[tauri::command]
pub fn fetch_library(state: tauri::State<'_, Mutex<AppState>>) -> Result<Vec<Game>, RemoteAccessError> {
offline!(state, fetch_library_logic, fetch_library_logic_offline, state)
pub fn fetch_library(
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<Game>, RemoteAccessError> {
offline!(
state,
fetch_library_logic,
fetch_library_logic_offline,
state
)
}
#[tauri::command]
pub fn fetch_game(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<FetchGameStruct, RemoteAccessError> {
offline!(state, fetch_game_logic, fetch_game_logic_offline, game_id, state)
offline!(
state,
fetch_game_logic,
fetch_game_logic_offline,
game_id,
state
)
}
#[tauri::command]

View File

@ -1,14 +1,11 @@
use crate::auth::generate_authorization_header;
use crate::database::db::{
borrow_db_checked, ApplicationTransientStatus, DatabaseImpls,
GameDownloadStatus,
};
use crate::database::db::borrow_db_checked;
use crate::database::models::data::{ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus};
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
use crate::download_manager::download_thread_control_flag::{
DownloadThreadControl, DownloadThreadControlFlag,
};
use crate::download_manager::downloadable::Downloadable;
use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
@ -25,7 +22,6 @@ use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tauri::{AppHandle, Emitter};
use urlencoding::encode;
#[cfg(target_os = "linux")]
use rustix::fs::{fallocate, FallocateFlags};
@ -377,7 +373,10 @@ impl Downloadable for GameDownloadAgent {
error!("error while managing download: {}", error);
let mut handle = DB.borrow_data_mut().unwrap();
handle.applications.transient_statuses.remove(&self.metadata());
handle
.applications
.transient_statuses
.remove(&self.metadata());
}
fn on_complete(&self, app_handle: &tauri::AppHandle) {

View File

@ -7,10 +7,9 @@ use serde::{Deserialize, Serialize};
use tauri::Emitter;
use tauri::{AppHandle, Manager};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus};
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
use crate::database::models::data::{ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion};
use crate::download_manager::download_manager::DownloadStatus;
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::error::library_error::LibraryError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::state::{GameStatusManager, GameStatusWithTransient};
@ -438,7 +437,12 @@ pub fn on_game_complete(
Ok(())
}
pub fn push_game_update(app_handle: &AppHandle, game_id: &String, version: Option<GameVersion>, status: GameStatusWithTransient) {
pub fn push_game_update(
app_handle: &AppHandle,
game_id: &String,
version: Option<GameVersion>,
status: GameStatusWithTransient,
) {
app_handle
.emit(
&format!("update_game/{}", game_id),

View File

@ -1,4 +1,4 @@
use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus};
use crate::database::{db::borrow_db_checked, models::data::{ApplicationTransientStatus, GameDownloadStatus}};
pub type GameStatusWithTransient = (
Option<GameDownloadStatus>,

View File

@ -17,9 +17,8 @@ use database::commands::{
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings,
fetch_system_data, update_settings,
};
use database::db::{
borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR,
};
use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, DATA_ROOT_DIR};
use database::models::data::GameDownloadStatus;
use download_manager::commands::{
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
};
@ -35,7 +34,6 @@ use games::commands::{
use games::downloads::commands::download_game;
use games::library::{update_game_configuration, Game};
use http::Response;
use http::{header::*, response::Builder as ResponseBuilder};
use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
@ -89,7 +87,6 @@ pub struct User {
profile_picture_object_id: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppState<'a> {
@ -161,8 +158,8 @@ fn setup(handle: AppHandle) -> AppState<'static> {
drop(db_handle);
for (game_id, status) in statuses.into_iter() {
match status {
database::db::GameDownloadStatus::Remote {} => {}
database::db::GameDownloadStatus::SetupRequired {
GameDownloadStatus::Remote {} => {}
GameDownloadStatus::SetupRequired {
version_name: _,
install_dir,
} => {
@ -171,7 +168,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
missing_games.push(game_id);
}
}
database::db::GameDownloadStatus::Installed {
GameDownloadStatus::Installed {
version_name: _,
install_dir,
} => {
@ -290,7 +287,7 @@ pub fn run() {
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
let _ = app.deep_link().register_all();
debug!("registered all pre-defined deep links");
}
@ -393,7 +390,6 @@ pub fn run() {
request,
responder
);
})
.on_window_event(|window, event| {
if let WindowEvent::CloseRequested { api, .. } = event {

View File

@ -18,11 +18,10 @@ use tauri::{AppHandle, Manager};
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{
database::db::{
borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion,
DATA_ROOT_DIR,
database::{
db::{borrow_db_mut_checked, DATA_ROOT_DIR},
models::data::{ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus, GameVersion},
},
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
error::process_error::ProcessError,
games::{library::push_game_update, state::GameStatusManager},
AppState, DB,

View File

@ -10,9 +10,7 @@ use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::db::{
borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls,
},
database::{db::{borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseImpls}, models::data::DatabaseAuth},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User, DB,
};

View File

@ -1,15 +1,11 @@
use std::sync::RwLockReadGuard;
use crate::{
database::db::{borrow_db_checked, Database},
error::remote_access_error::RemoteAccessError,
};
use crate::{database::{db::borrow_db_checked, models::data::Database}, error::remote_access_error::RemoteAccessError};
use cacache::Integrity;
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder, Response};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
#[macro_export]
macro_rules! offline {
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {