diff --git a/src/api/admin.rs b/src/api/admin.rs index d36da8f9..30496148 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -790,7 +790,7 @@ async fn delete_config(_token: AdminToken) -> EmptyResult { #[post("/config/backup_db", format = "application/json")] fn backup_db(_token: AdminToken) -> ApiResult { if *CAN_BACKUP { - match backup_sqlite() { + match backup_sqlite(None) { Ok(f) => Ok(format!("Backup to '{f}' was successful")), Err(e) => err!(format!("Backup was unsuccessful {e}")), } diff --git a/src/db/mod.rs b/src/db/mod.rs index ae2b1221..e9ae584f 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -384,8 +384,13 @@ pub mod models; /// Creates a back-up of the sqlite database /// MySQL/MariaDB and PostgreSQL are not supported. +/// +/// # Arguments +/// +/// * `backup_dir` - Optional custom directory path where the backup file will be created. +/// If `None`, the backup will be created in the same directory as the database file. #[cfg(sqlite)] -pub fn backup_sqlite() -> Result { +pub fn backup_sqlite(backup_dir: Option) -> Result { use diesel::Connection; use std::{fs::File, io::Write}; @@ -395,8 +400,21 @@ pub fn backup_sqlite() -> Result { // This way we can set a readonly flag on the opening mode without issues. let mut conn = diesel::sqlite::SqliteConnection::establish(&format!("sqlite://{db_url}?mode=ro"))?; - let db_path = std::path::Path::new(&db_url).parent().unwrap(); - let backup_file = db_path + let backup_path = match backup_dir { + Some(dir) => { + let path = std::path::Path::new(dir.as_str()).to_path_buf(); + + // Ensure the backup directory exists + if let Err(e) = std::fs::create_dir_all(&path) { + err_silent!(format!("Unable to create backup directory: {e:?}")) + } + + path + }, + None => std::path::Path::new(&db_url).parent().unwrap().to_path_buf(), + }; + + let backup_file = backup_path .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) .to_string_lossy() .into_owned(); @@ -417,7 +435,7 @@ pub fn backup_sqlite() -> Result { } #[cfg(not(sqlite))] -pub fn backup_sqlite() -> Result { +pub fn backup_sqlite(_backup_dir: Option<&str>) -> Result { err_silent!("The database type is not SQLite. Backups only works for SQLite databases") } diff --git a/src/main.rs b/src/main.rs index b5ff93ae..589e4bae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,7 +105,7 @@ FLAGS: COMMAND: hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN - backup Create a backup of the SQLite database + backup [--path] Create a backup of the SQLite database You can also send the USR1 signal to trigger a backup PRESETS: m= t= p= @@ -187,7 +187,9 @@ fn parse_args() { exit(1); } } else if command == "backup" { - match db::backup_sqlite() { + let backup_path: Option = pargs.opt_value_from_str(["-p", "--path"]).unwrap_or_default(); + + match db::backup_sqlite(backup_path) { Ok(f) => { println!("Backup to '{f}' was successful"); exit(0); @@ -606,7 +608,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> // If we need more signals to act upon, we might want to use select! here. // With only one item to listen for this is enough. let _ = signal_user1.recv().await; - match db::backup_sqlite() { + match db::backup_sqlite(None) { Ok(f) => info!("Backup to '{f}' was successful"), Err(e) => error!("Backup failed. {e:?}"), }