mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-10 04:22:13 +10:00
feat(database): Ensure that any database issues are resolved by standalone functions
Functions are as follows: - save_db() - borrow_db_checked() - borrow_db_mut_checked()
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
use crate::DB;
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db};
|
||||
use log::debug;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_autostart::ManagerExt;
|
||||
@ -14,17 +14,17 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin
|
||||
}
|
||||
|
||||
// Store the state in DB
|
||||
let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.settings.autostart = enabled;
|
||||
drop(db_handle);
|
||||
DB.save().map_err(|e| e.to_string())?;
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_autostart::Error> {
|
||||
// First check DB state
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let db_handle = borrow_db_checked();
|
||||
let db_state = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
@ -46,7 +46,7 @@ pub fn get_autostart_enabled_logic(app: AppHandle) -> Result<bool, tauri_plugin_
|
||||
|
||||
// New function to sync state on startup
|
||||
pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
|
||||
let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
|
||||
let db_handle = borrow_db_checked();
|
||||
let should_be_enabled = db_handle.settings.autostart;
|
||||
drop(db_handle);
|
||||
|
||||
|
||||
@ -6,24 +6,24 @@ use std::{
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{database::settings::Settings, download_manager::{download_manager::DownloadManagerSignal, internal_error::InternalError}, DB};
|
||||
use crate::{database::{db::borrow_db_mut_checked, settings::Settings}, download_manager::{download_manager::DownloadManagerSignal, internal_error::InternalError}, DB};
|
||||
|
||||
use super::{db::DATA_ROOT_DIR, debug::SystemData};
|
||||
use super::{db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, debug::SystemData};
|
||||
|
||||
// Will, in future, return disk/remaining size
|
||||
// Just returns the directories that have been set up
|
||||
#[tauri::command]
|
||||
pub fn fetch_download_dir_stats() -> Vec<PathBuf> {
|
||||
let lock = DB.borrow_data().unwrap();
|
||||
let lock = borrow_db_checked();
|
||||
lock.applications.install_dirs.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_download_dir(index: usize) {
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
lock.applications.install_dirs.remove(index);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -43,7 +43,7 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
|
||||
}
|
||||
|
||||
// Add it to the dictionary
|
||||
let mut lock = DB.borrow_data_mut().unwrap();
|
||||
let mut lock = borrow_db_mut_checked();
|
||||
if lock.applications.install_dirs.contains(&new_dir) {
|
||||
return Err(Error::new(
|
||||
ErrorKind::AlreadyExists,
|
||||
@ -52,14 +52,14 @@ pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> {
|
||||
}
|
||||
lock.applications.install_dirs.push(new_dir);
|
||||
drop(lock);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_settings(new_settings: Value) {
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap();
|
||||
for (key, value) in new_settings.as_object().unwrap() {
|
||||
current_settings[key] = value.clone();
|
||||
@ -70,11 +70,11 @@ pub fn update_settings(new_settings: Value) {
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn fetch_settings() -> Settings {
|
||||
DB.borrow_data().unwrap().settings.clone()
|
||||
borrow_db_checked().settings.clone()
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn fetch_system_data() -> SystemData {
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let db_handle = borrow_db_checked();
|
||||
SystemData::new(
|
||||
db_handle.auth.as_ref().unwrap().client_id.clone(),
|
||||
db_handle.base_url.clone(),
|
||||
|
||||
@ -2,12 +2,12 @@ use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, create_dir_all},
|
||||
path::{Path, PathBuf},
|
||||
sync::{LazyLock, Mutex, RwLockWriteGuard},
|
||||
sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use directories::BaseDirs;
|
||||
use log::{debug, info};
|
||||
use log::{debug, error, info};
|
||||
use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
@ -169,10 +169,10 @@ pub fn set_game_status<F: FnOnce(&mut RwLockWriteGuard<'_, Database>, &Downloada
|
||||
meta: DownloadableMetadata,
|
||||
setter: F,
|
||||
) {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
setter(&mut db_handle, &meta);
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
let status = GameStatusManager::fetch_state(&meta.id);
|
||||
|
||||
@ -200,3 +200,34 @@ fn handle_invalid_database(
|
||||
|
||||
PathDatabase::create_at_path(db_path, db).expect("Database could not be created")
|
||||
}
|
||||
|
||||
|
||||
pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> {
|
||||
match DB.borrow_data() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("database borrow failed with error {}", e);
|
||||
panic!("database borrow failed with error {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> {
|
||||
match DB.borrow_data_mut() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
error!("database borrow mut failed with error {}", e);
|
||||
panic!("database borrow mut failed with error {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_db() {
|
||||
match DB.save() {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
error!("database failed to save with error {}", e);
|
||||
panic!("database failed to save with error {}", e)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -289,9 +289,8 @@ impl DownloadManagerBuilder {
|
||||
|
||||
self.stop_and_wait_current_download();
|
||||
self.remove_and_cleanup_front_download(¤t_agent.metadata());
|
||||
|
||||
self.set_status(DownloadManagerStatus::Error(error));
|
||||
}
|
||||
self.set_status(DownloadManagerStatus::Error(error));
|
||||
}
|
||||
fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
|
||||
debug!("got signal Cancel");
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::auth::generate_authorization_header;
|
||||
use crate::database::db::{
|
||||
set_game_status, ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus,
|
||||
borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus
|
||||
};
|
||||
use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
|
||||
use crate::download_manager::download_thread_control_flag::{
|
||||
@ -56,7 +56,7 @@ impl GameDownloadAgent {
|
||||
// Don't run by default
|
||||
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
|
||||
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let db_lock = borrow_db_checked();
|
||||
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
|
||||
drop(db_lock);
|
||||
|
||||
@ -243,7 +243,7 @@ impl GameDownloadAgent {
|
||||
|
||||
// TODO: Change return value on Err
|
||||
pub fn run(&self) -> Result<bool, ()> {
|
||||
let max_download_threads = DB.borrow_data().unwrap().settings.max_download_threads;
|
||||
let max_download_threads = borrow_db_checked().settings.max_download_threads;
|
||||
|
||||
debug!(
|
||||
"downloading game: {} with {} threads",
|
||||
|
||||
@ -8,7 +8,7 @@ use tauri::Emitter;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use urlencoding::encode;
|
||||
|
||||
use crate::database::db::GameVersion;
|
||||
use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion};
|
||||
use crate::database::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
|
||||
use crate::download_manager::download_manager::DownloadStatus;
|
||||
use crate::download_manager::downloadable_metadata::DownloadableMetadata;
|
||||
@ -103,7 +103,7 @@ pub fn fetch_library_logic(app: AppHandle) -> Result<Vec<Game>, RemoteAccessErro
|
||||
let state = app.state::<Mutex<AppState>>();
|
||||
let mut handle = state.lock().unwrap();
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
for game in games.iter() {
|
||||
handle.games.insert(game.id.clone(), game.clone());
|
||||
@ -155,7 +155,7 @@ pub fn fetch_game_logic(
|
||||
let game: Game = response.json()?;
|
||||
state_handle.games.insert(id.clone(), game.clone());
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
|
||||
db_handle
|
||||
.applications
|
||||
@ -206,7 +206,7 @@ pub fn fetch_game_verion_options_logic(
|
||||
|
||||
pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
|
||||
println!("triggered uninstall for agent");
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.transient_statuses
|
||||
@ -249,7 +249,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
error!("{}", e);
|
||||
}
|
||||
Ok(_) => {
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle.applications.transient_statuses.remove(&meta);
|
||||
db_handle
|
||||
.applications
|
||||
@ -257,7 +257,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
.entry(meta.id.clone())
|
||||
.and_modify(|e| *e = GameDownloadStatus::Remote {});
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
debug!("uninstalled game id {}", &meta.id);
|
||||
|
||||
@ -272,8 +272,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
|
||||
}
|
||||
|
||||
pub fn get_current_meta(game_id: &String) -> Option<DownloadableMetadata> {
|
||||
DB.borrow_data()
|
||||
.unwrap()
|
||||
borrow_db_checked()
|
||||
.applications
|
||||
.installed_game_version
|
||||
.get(game_id)
|
||||
@ -309,7 +308,7 @@ pub fn on_game_complete(
|
||||
|
||||
let data: GameVersion = response.json()?;
|
||||
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle
|
||||
.applications
|
||||
.game_versions
|
||||
@ -322,7 +321,7 @@ pub fn on_game_complete(
|
||||
.insert(meta.id.clone(), meta.clone());
|
||||
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
let status = if data.setup_command.is_empty() {
|
||||
GameDownloadStatus::Installed {
|
||||
@ -336,13 +335,13 @@ pub fn on_game_complete(
|
||||
}
|
||||
};
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
db_handle
|
||||
.applications
|
||||
.game_statuses
|
||||
.insert(meta.id.clone(), status.clone());
|
||||
drop(db_handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
app_handle
|
||||
.emit(
|
||||
&format!("update_game/{}", meta.id),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
database::db::{ApplicationTransientStatus, GameDownloadStatus},
|
||||
database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus},
|
||||
DB,
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ pub struct GameStatusManager {}
|
||||
|
||||
impl GameStatusManager {
|
||||
pub fn fetch_state(game_id: &String) -> GameStatusWithTransient {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let db_lock = borrow_db_checked();
|
||||
let online_state = match db_lock.applications.installed_game_version.get(game_id) {
|
||||
Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(),
|
||||
None => None,
|
||||
|
||||
@ -18,7 +18,7 @@ use commands::fetch_state;
|
||||
use database::commands::{
|
||||
add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_system_data, fetch_settings, update_settings
|
||||
};
|
||||
use database::db::{DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR};
|
||||
use database::db::{borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR};
|
||||
use download_manager::commands::{
|
||||
cancel_game, move_download_in_queue, pause_downloads, resume_downloads,
|
||||
};
|
||||
@ -138,7 +138,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
// TODO: Account for possible failure
|
||||
let (app_status, user) = auth::setup();
|
||||
|
||||
let db_handle = DB.borrow_data().unwrap();
|
||||
let db_handle = borrow_db_checked();
|
||||
let mut missing_games = Vec::new();
|
||||
let statuses = db_handle.applications.game_statuses.clone();
|
||||
drop(db_handle);
|
||||
@ -168,7 +168,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
|
||||
|
||||
info!("detected games missing: {:?}", missing_games);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
for game_id in missing_games {
|
||||
db_handle
|
||||
.applications
|
||||
@ -327,7 +327,7 @@ pub fn run() {
|
||||
.expect("error while setting up tray menu");
|
||||
|
||||
{
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
if let Some(original) = db_handle.prev_database.take() {
|
||||
warn!(
|
||||
"Database corrupted. Original file at {}",
|
||||
|
||||
@ -15,7 +15,7 @@ use tauri::{AppHandle, Manager};
|
||||
use umu_wrapper_lib::command_builder::UmuCommandBuilder;
|
||||
|
||||
use crate::{
|
||||
database::db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
|
||||
database::db::{borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR},
|
||||
download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata},
|
||||
error::process_error::ProcessError,
|
||||
games::{library::push_game_update, state::GameStatusManager},
|
||||
@ -106,7 +106,7 @@ impl ProcessManager<'_> {
|
||||
|
||||
self.processes.remove(&game_id);
|
||||
|
||||
let mut db_handle = DB.borrow_data_mut().unwrap();
|
||||
let mut db_handle = borrow_db_mut_checked();
|
||||
let meta = db_handle
|
||||
.applications
|
||||
.installed_game_version
|
||||
@ -176,7 +176,7 @@ impl ProcessManager<'_> {
|
||||
download_type: DownloadType::Game,
|
||||
};
|
||||
|
||||
let mut db_lock = DB.borrow_data_mut().unwrap();
|
||||
let mut db_lock = borrow_db_mut_checked();
|
||||
debug!(
|
||||
"Launching process {:?} with games {:?}",
|
||||
&game_id, db_lock.applications.game_versions
|
||||
|
||||
@ -8,7 +8,7 @@ use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{DatabaseAuth, DatabaseImpls},
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls},
|
||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||
AppState, AppStatus, User, DB,
|
||||
};
|
||||
@ -51,7 +51,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
|
||||
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = DB.borrow_data().unwrap();
|
||||
let db = borrow_db_checked();
|
||||
db.auth.clone().unwrap()
|
||||
};
|
||||
|
||||
@ -97,7 +97,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
}
|
||||
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
let handle = borrow_db_checked();
|
||||
Url::parse(handle.base_url.as_str())?
|
||||
};
|
||||
|
||||
@ -115,14 +115,14 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
|
||||
let response_struct: HandshakeResponse = response.json()?;
|
||||
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = Some(DatabaseAuth {
|
||||
private: response_struct.private,
|
||||
cert: response_struct.certificate,
|
||||
client_id: response_struct.id,
|
||||
});
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
}
|
||||
|
||||
{
|
||||
@ -151,7 +151,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
|
||||
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
let db_lock = borrow_db_checked();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
};
|
||||
|
||||
@ -181,7 +181,7 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
}
|
||||
|
||||
pub fn setup() -> (AppStatus, Option<User>) {
|
||||
let data = DB.borrow_data().unwrap();
|
||||
let data = borrow_db_checked();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
|
||||
@ -4,8 +4,7 @@ use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
error::remote_access_error::RemoteAccessError,
|
||||
AppState, AppStatus, DB,
|
||||
database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -24,7 +23,7 @@ pub fn use_remote(
|
||||
#[tauri::command]
|
||||
pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
let handle = borrow_db_checked();
|
||||
|
||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||
};
|
||||
@ -38,10 +37,10 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
|
||||
pub fn sign_out(app: AppHandle) {
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
|
||||
@ -9,7 +9,7 @@ use log::{debug, info, warn};
|
||||
use serde::Deserialize;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use crate::{error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB};
|
||||
use crate::{database::db::{borrow_db_mut_checked, save_db}, error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -39,11 +39,11 @@ pub fn use_remote_logic(
|
||||
app_state.status = AppStatus::SignedOut;
|
||||
drop(app_state);
|
||||
|
||||
let mut db_state = DB.borrow_data_mut().unwrap();
|
||||
let mut db_state = borrow_db_mut_checked();
|
||||
db_state.base_url = base_url.to_string();
|
||||
drop(db_state);
|
||||
|
||||
DB.save().unwrap();
|
||||
save_db();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user