diff --git a/src/api/admin.rs b/src/api/admin.rs index d52e24ef..5a1c6e1b 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -793,7 +793,7 @@ async fn delete_config(_token: AdminToken) -> EmptyResult { #[post("/config/backup_db", format = "application/json")] async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult { if *CAN_BACKUP { - match backup_database(&mut conn).await { + match backup_database(&mut conn, None).await { 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 9f5bb150..0b77975f 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -368,7 +368,7 @@ pub mod models; /// Creates a back-up of the sqlite database /// MySQL/MariaDB and PostgreSQL are not supported. -pub async fn backup_database(conn: &mut DbConn) -> Result { +pub async fn backup_database(conn: &mut DbConn, output_dir: Option) -> Result { db_run! {@raw conn: postgresql, mysql { let _ = conn; @@ -376,8 +376,19 @@ pub async fn backup_database(conn: &mut DbConn) -> Result { } sqlite { let db_url = CONFIG.database_url(); - let db_path = std::path::Path::new(&db_url).parent().unwrap(); - let backup_file = db_path + + let backup_dir = match &output_dir { + Some(dir) => { + let output_path = std::path::Path::new(dir); + if !output_path.exists() || !output_path.is_dir() { + err!(format!("Backup directory does not exist or is not a directory: {}", dir)); + } + output_path + } + None => std::path::Path::new(&db_url).parent().unwrap(), + }; + + let backup_file = backup_dir .join(format!("db_{}.sqlite3", chrono::Utc::now().format("%Y%m%d_%H%M%S"))) .to_string_lossy() .into_owned(); diff --git a/src/main.rs b/src/main.rs index 3195300b..028622dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,7 +106,7 @@ FLAGS: COMMAND: hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN - backup Create a backup of the SQLite database + backup [--output ] Create a backup of the SQLite database You can also send the USR1 signal to trigger a backup PRESETS: m= t= p= @@ -188,7 +188,9 @@ async fn parse_args() { exit(1); } } else if command == "backup" { - match backup_sqlite().await { + let output_dir: Option = pargs.opt_value_from_str(["-o", "--output"]).unwrap_or_default(); + + match backup_sqlite(output_dir).await { Ok(f) => { println!("Backup to '{f}' was successful"); exit(0); @@ -203,7 +205,7 @@ async fn parse_args() { } } -async fn backup_sqlite() -> Result { +async fn backup_sqlite(output_dir: Option) -> Result { use crate::db::{backup_database, DbConnType}; if DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false) { // Establish a connection to the sqlite database @@ -213,7 +215,7 @@ async fn backup_sqlite() -> Result { .await .expect("Unable to get SQLite db pool"); - let backup_file = backup_database(&mut conn).await?; + let backup_file = backup_database(&mut conn, output_dir).await?; Ok(backup_file) } else { err_silent!("The database type is not SQLite. Backups only works for SQLite databases") @@ -622,7 +624,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 backup_sqlite().await { + match backup_sqlite(None).await { Ok(f) => info!("Backup to '{f}' was successful"), Err(e) => error!("Backup failed. {e:?}"), }