use std::{ fs::{self, create_dir_all}, mem::ManuallyDrop, ops::{Deref, DerefMut}, path::PathBuf, sync::{Arc, LazyLock, RwLockReadGuard, RwLockWriteGuard}, }; use chrono::Utc; use log::{debug, error, info, warn}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use serde::{Serialize, de::DeserializeOwned}; use url::Url; use crate::DB; use super::models::data::Database; #[cfg(not(debug_assertions))] static DATA_ROOT_PREFIX: &'static str = "drop"; #[cfg(debug_assertions)] static DATA_ROOT_PREFIX: &str = "drop-debug"; pub static DATA_ROOT_DIR: LazyLock> = 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 DeSerializer for DropDatabaseSerializer { fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> { native_model::encode(val) .map_err(|e| DeSerError::Internal(e.to_string())) } fn deserialize(&self, mut s: R) -> rustbreak::error::DeSerResult { 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) } } pub type DatabaseInterface = rustbreak::Database; pub trait DatabaseImpls { fn set_up_database() -> DatabaseInterface; fn database_is_set_up(&self) -> bool; fn fetch_base_url(&self) -> Url; } impl DatabaseImpls for DatabaseInterface { fn set_up_database() -> DatabaseInterface { let db_path = DATA_ROOT_DIR.join("drop.db"); let games_base_dir = DATA_ROOT_DIR.join("games"); let logs_root_dir = DATA_ROOT_DIR.join("logs"); let cache_dir = DATA_ROOT_DIR.join("cache"); let pfx_dir = DATA_ROOT_DIR.join("pfx"); debug!("creating data directory at {DATA_ROOT_DIR:?}"); create_dir_all(DATA_ROOT_DIR.as_path()).unwrap_or_else(|e| { panic!( "Failed to create directory {} with error {}", DATA_ROOT_DIR.display(), e ) }); create_dir_all(&games_base_dir).unwrap_or_else(|e| { panic!( "Failed to create directory {} with error {}", games_base_dir.display(), e ) }); create_dir_all(&logs_root_dir).unwrap_or_else(|e| { panic!( "Failed to create directory {} with error {}", logs_root_dir.display(), e ) }); create_dir_all(&cache_dir).unwrap_or_else(|e| { panic!( "Failed to create directory {} with error {}", cache_dir.display(), e ) }); create_dir_all(&pfx_dir).unwrap_or_else(|e| { panic!( "Failed to create directory {} with error {}", pfx_dir.display(), e ) }); let exists = fs::exists(db_path.clone()).unwrap_or_else(|e| { panic!( "Failed to find if {} exists with error {}", db_path.display(), e ) }); if exists { match PathDatabase::load_from_path(db_path.clone()) { Ok(db) => db, Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir), } } else { let default = Database::new(games_base_dir, None, cache_dir); debug!("Creating database at path {}", db_path.display()); PathDatabase::create_at_path(db_path, default).expect("Database could not be created") } } fn database_is_set_up(&self) -> bool { !borrow_db_checked().base_url.is_empty() } fn fetch_base_url(&self) -> Url { let handle = borrow_db_checked(); Url::parse(&handle.base_url) .unwrap_or_else(|_| panic!("Failed to parse base url {}", handle.base_url)) } } // TODO: Make the error relelvant rather than just assume that it's a Deserialize error fn handle_invalid_database( _e: RustbreakError, db_path: PathBuf, games_base_dir: PathBuf, cache_dir: PathBuf, ) -> rustbreak::Database { warn!("{_e}"); let new_path = { let time = Utc::now().timestamp(); let mut base = db_path.clone(); base.set_file_name(format!("drop.db.backup-{time}")); base }; info!("old database stored at: {}", new_path.to_string_lossy()); fs::rename(&db_path, &new_path).unwrap_or_else(|e| { panic!( "Could not rename database {} to {} with error {}", db_path.display(), new_path.display(), e ) }); let db = Database::new(games_base_dir, Some(new_path), cache_dir); PathDatabase::create_at_path(db_path, db).expect("Database could not be created") } // To automatically save the database upon drop pub struct DBRead<'a>(RwLockReadGuard<'a, Database>); pub struct DBWrite<'a>(ManuallyDrop>); impl<'a> Deref for DBWrite<'a> { type Target = Database; fn deref(&self) -> &Self::Target { &self.0 } } impl<'a> DerefMut for DBWrite<'a> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl<'a> Deref for DBRead<'a> { type Target = Database; fn deref(&self) -> &Self::Target { &self.0 } } impl Drop for DBWrite<'_> { fn drop(&mut self) { unsafe { ManuallyDrop::drop(&mut self.0); } match DB.save() { Ok(()) => {} Err(e) => { error!("database failed to save with error {e}"); panic!("database failed to save with error {e}") } } } } pub fn borrow_db_checked<'a>() -> DBRead<'a> { match DB.borrow_data() { Ok(data) => DBRead(data), Err(e) => { error!("database borrow failed with error {e}"); panic!("database borrow failed with error {e}"); } } } pub fn borrow_db_mut_checked<'a>() -> DBWrite<'a> { match DB.borrow_data_mut() { Ok(data) => DBWrite(ManuallyDrop::new(data)), Err(e) => { error!("database borrow mut failed with error {e}"); panic!("database borrow mut failed with error {e}"); } } }