feat(recovery): Added database recovery

Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
quexeky
2025-01-04 21:08:14 +11:00
parent 005bab2fb8
commit 32ae7d5385
3 changed files with 62 additions and 4 deletions

15
app.vue
View File

@ -29,6 +29,21 @@ router.beforeEach(async () => {
setupHooks(); setupHooks();
initialNavigation(state); initialNavigation(state);
listen("database_corrupted", (event) => {
createModal(
ModalType.Notification,
{
title: "Database corrupted",
description: `Drop encountered an error while reading your download. A copy can be found at: "${(
event.payload as unknown as string
).toString()}"`,
buttonText: "Close"
},
(e, c) => c()
);
})
useHead({ useHead({
title: "Drop", title: "Drop",
}); });

View File

@ -2,12 +2,13 @@ use std::{
collections::HashMap, collections::HashMap,
fs::{self, create_dir_all}, fs::{self, create_dir_all},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH},
}; };
use chrono::Utc;
use directories::BaseDirs; use directories::BaseDirs;
use log::debug; use log::debug;
use rustbreak::{DeSerError, DeSerializer, PathDatabase}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use tauri::AppHandle; use tauri::AppHandle;
@ -92,6 +93,7 @@ pub struct Database {
pub auth: Option<DatabaseAuth>, pub auth: Option<DatabaseAuth>,
pub base_url: String, pub base_url: String,
pub applications: DatabaseApplications, pub applications: DatabaseApplications,
pub prev_database: Option<PathBuf>
} }
pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> = pub static DATA_ROOT_DIR: LazyLock<Mutex<PathBuf>> =
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
@ -135,7 +137,11 @@ impl DatabaseImpls for DatabaseInterface {
let exists = fs::exists(db_path.clone()).unwrap(); let exists = fs::exists(db_path.clone()).unwrap();
match exists { match exists {
true => PathDatabase::load_from_path(db_path).expect("Database loading failed"), true =>
match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir),
},
false => { false => {
let default = Database { let default = Database {
settings: Settings::default(), settings: Settings::default(),
@ -148,6 +154,7 @@ impl DatabaseImpls for DatabaseInterface {
game_versions: HashMap::new(), game_versions: HashMap::new(),
installed_game_version: HashMap::new(), installed_game_version: HashMap::new(),
}, },
prev_database: None,
}; };
debug!( debug!(
"Creating database at path {}", "Creating database at path {}",
@ -238,3 +245,32 @@ pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &Downloada
push_game_update(app_handle, &meta, status); push_game_update(app_handle, &meta, status);
} }
// 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) -> rustbreak::Database<Database, rustbreak::backend::PathBackend, DropDatabaseSerializer> {
let new_path = {
let time = Utc::now().timestamp();
let mut base = db_path.clone().into_os_string();
base.push(time.to_string());
base
};
fs::copy(&db_path, &new_path);
let db = Database {
auth: None,
base_url: "".to_string(),
applications: DatabaseApplications {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
game_statuses: HashMap::new(),
transient_statuses: HashMap::new(),
game_versions: HashMap::new(),
installed_game_version: HashMap::new(),
},
prev_database: Some(new_path.into()),
};
PathDatabase::create_at_path(db_path, db)
.expect("Database could not be created")
}

View File

@ -50,7 +50,7 @@ use std::{
}; };
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIconBuilder; use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
#[derive(Clone, Copy, Serialize)] #[derive(Clone, Copy, Serialize)]
@ -181,7 +181,14 @@ fn setup(handle: AppHandle) -> AppState<'static> {
.entry(game_id) .entry(game_id)
.and_modify(|v| *v = GameDownloadStatus::Remote {}); .and_modify(|v| *v = GameDownloadStatus::Remote {});
} }
if let Some(original) = db_handle.prev_database.take() {
warn!("Database corrupted. Original file at {}", original.canonicalize().unwrap().to_string_lossy().to_string());
handle.emit("database_corrupted", original.to_string_lossy().to_string()).unwrap();
}
drop(db_handle); drop(db_handle);
info!("finished setup!"); info!("finished setup!");
// Sync autostart state // Sync autostart state