Release: NX9-Auth v0.3.0
This commit is contained in:
1 parent
6a04d7f793
commit
d93f2cef95
92 files changed
+2418
-1143
No files matched your search
+192
-283
@@ -1,3 +1,4 @@
|
||||
use anyhow::Context;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -161,6 +162,12 @@ pub enum Commands {
|
||||
/// Path where the backup file will be created.
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Restore the database from a backup file.
|
||||
Restore {
|
||||
/// Path to the backup file to restore from.
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -242,118 +249,55 @@ pub async fn run(command: Commands, config: Config) -> anyhow::Result<()> {
|
||||
} => cmd_show_user(&config, &id_or_username, permissions).await,
|
||||
Commands::ShowToken { id } => cmd_show_token(&config, &id).await,
|
||||
Commands::Backup { path } => cmd_backup(&config, &path).await,
|
||||
Commands::Restore { path } => cmd_restore(&config, &path).await,
|
||||
}
|
||||
}
|
||||
|
||||
// ── migrate ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_migrate(config: &Config) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
db::run_migrations(&pool).await?;
|
||||
println!("✓ Migrations applied successfully.");
|
||||
let (_provider, backend, _pool) = db::init_provider(config).await?;
|
||||
println!("✓ Migrations applied successfully ({backend}).");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── doctor ────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_provider(
|
||||
pool: sqlx::SqlitePool,
|
||||
) -> std::sync::Arc<dyn crate::db::provider::DatabaseProvider> {
|
||||
#[cfg(feature = "sqlite")]
|
||||
{
|
||||
std::sync::Arc::new(crate::db::provider::SqliteProvider::new(pool))
|
||||
}
|
||||
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
||||
{
|
||||
std::sync::Arc::new(crate::db::provider::PostgresProvider::new(pool))
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_doctor_checks(config: &Config) -> anyhow::Result<bool> {
|
||||
let mut ok = true;
|
||||
|
||||
println!("\nnx9-auth doctor\n");
|
||||
|
||||
// 1. Config loads (already done — we got here with a valid config)
|
||||
// 1. Config file loads
|
||||
println!(" ✓ Config file loads and parses");
|
||||
|
||||
// 2. DB path is writable
|
||||
let db_path = std::path::Path::new(&config.database.path);
|
||||
let db_dir_writable = if let Some(parent) = db_path.parent() {
|
||||
if parent.as_os_str().is_empty() {
|
||||
true
|
||||
} else if std::fs::create_dir_all(parent).is_err() {
|
||||
false
|
||||
} else {
|
||||
let temp_file = parent.join(format!(
|
||||
".nx9_auth_doctor_{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
if std::fs::write(&temp_file, b"test").is_ok() {
|
||||
let _ = std::fs::remove_file(temp_file);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
// 2. DB backend & connection
|
||||
let (url, backend) = match config.database.resolved_url() {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to resolve database configuration: {e}");
|
||||
println!("\nDoctor result: FAIL\n");
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if db_dir_writable {
|
||||
println!(" ✓ Database directory is writable");
|
||||
} else {
|
||||
println!(
|
||||
" ✗ Database directory is not writable: {}",
|
||||
config.database.path
|
||||
);
|
||||
ok = false;
|
||||
}
|
||||
println!(" ✓ Database backend detected: {backend}");
|
||||
println!(" ✓ Database URL: {url}");
|
||||
|
||||
// 3. DB connects
|
||||
let pool_result = db::create_pool(&config.database.path).await;
|
||||
let pool = match pool_result {
|
||||
Ok(p) => {
|
||||
println!(" ✓ Database connection successful");
|
||||
let provider = match db::init_provider(config).await {
|
||||
Ok((p, _, _)) => {
|
||||
println!(" ✓ Database connection & migrations successful");
|
||||
p
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Database connection failed: {}", e);
|
||||
println!(" ✗ Database initialization failed: {e}");
|
||||
println!("\nDoctor result: FAIL\n");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let provider = make_provider(pool.clone());
|
||||
|
||||
// 4. Migrations are up to date
|
||||
// Verify migrations are applied
|
||||
let migration_check: Result<(i64,), sqlx::Error> =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM _sqlx_migrations")
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
match migration_check {
|
||||
Ok((count,)) if count > 0 => println!(" ✓ Migrations applied ({} recorded)", count),
|
||||
Ok(_) => {
|
||||
println!(" ✗ No migrations recorded — run `nx9-auth migrate` first");
|
||||
ok = false;
|
||||
}
|
||||
Err(_) => {
|
||||
println!(" ✗ Migrations table missing — run `nx9-auth migrate` first");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Default tenant exists
|
||||
let tenant_check: Result<(i64,), sqlx::Error> =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM tenants WHERE id = ?")
|
||||
.bind(Tenant::DEFAULT_ID)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
match tenant_check {
|
||||
Ok((1,)) => println!(" ✓ Default tenant exists"),
|
||||
match provider.tenants().find_by_id(Tenant::DEFAULT_ID).await {
|
||||
Ok(Some(_)) => println!(" ✓ Default tenant exists"),
|
||||
_ => {
|
||||
println!(" ✗ Default tenant missing — run `nx9-auth migrate`");
|
||||
ok = false;
|
||||
@@ -386,102 +330,6 @@ async fn run_doctor_checks(config: &Config) -> anyhow::Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
// 8. WAL mode
|
||||
let journal_mode: Result<(String,), sqlx::Error> =
|
||||
sqlx::query_as("PRAGMA journal_mode").fetch_one(&pool).await;
|
||||
match journal_mode {
|
||||
Ok((mode,)) if mode.to_lowercase() == "wal" => println!(" ✓ WAL mode enabled"),
|
||||
Ok((mode,)) => {
|
||||
println!(" ✗ WAL mode not enabled (current mode: {})", mode);
|
||||
ok = false;
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to check journal mode: {}", e);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Foreign Keys
|
||||
let foreign_keys: Result<(i64,), sqlx::Error> =
|
||||
sqlx::query_as("PRAGMA foreign_keys").fetch_one(&pool).await;
|
||||
match foreign_keys {
|
||||
Ok((1,)) => println!(" ✓ Foreign keys constraint enforcement enabled"),
|
||||
Ok((val,)) => {
|
||||
println!(
|
||||
" ✗ Foreign keys constraint enforcement disabled (current value: {})",
|
||||
val
|
||||
);
|
||||
ok = false;
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to check foreign keys: {}", e);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Table existence
|
||||
for table in &["audit_logs", "sessions"] {
|
||||
let table_exists: Result<Option<(String,)>, sqlx::Error> =
|
||||
sqlx::query_as("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
|
||||
.bind(table)
|
||||
.fetch_optional(&pool)
|
||||
.await;
|
||||
match table_exists {
|
||||
Ok(Some(_)) => println!(" ✓ Table '{}' exists", table),
|
||||
Ok(None) => {
|
||||
println!(" ✗ Table '{}' is missing", table);
|
||||
ok = false;
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to check existence of table '{}': {}", table, e);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Database Write Test
|
||||
let write_test: Result<(), sqlx::Error> = async {
|
||||
let mut tx = pool.begin().await?;
|
||||
sqlx::query("CREATE TEMP TABLE doctor_test_write (id INTEGER PRIMARY KEY)")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("INSERT INTO doctor_test_write (id) VALUES (1)")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query("DROP TABLE doctor_test_write")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
match write_test {
|
||||
Ok(()) => {
|
||||
println!(" ✓ Database write test successful (temp table creation and deletion)")
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Database write test failed: {}", e);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 12. Database Integrity Check
|
||||
let integrity_check: Result<(String,), sqlx::Error> = sqlx::query_as("PRAGMA integrity_check")
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
match integrity_check {
|
||||
Ok((res,)) if res.to_lowercase() == "ok" => {
|
||||
println!(" ✓ Database integrity check passed")
|
||||
}
|
||||
Ok((res,)) => {
|
||||
println!(" ✗ Database integrity check failed: {}", res);
|
||||
ok = false;
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ✗ Failed to run database integrity check: {}", e);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
if ok {
|
||||
println!("Doctor result: OK\n");
|
||||
@@ -503,8 +351,7 @@ async fn cmd_doctor(config: &Config) -> anyhow::Result<()> {
|
||||
// ── create-admin ──────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_create_admin(config: &Config, username: &str) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let password = prompt_password_confirmed("Password for admin: ", true)?;
|
||||
|
||||
@@ -529,8 +376,7 @@ async fn cmd_create_admin(config: &Config, username: &str) -> anyhow::Result<()>
|
||||
// ── create-user ───────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_create_user(config: &Config, username: &str) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let password = prompt_password_confirmed("Password: ", false)?;
|
||||
|
||||
@@ -553,8 +399,7 @@ async fn cmd_create_user(config: &Config, username: &str) -> anyhow::Result<()>
|
||||
// ── list-users ────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_list_users(config: &Config) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let users = provider.users().list(Tenant::DEFAULT_ID).await?;
|
||||
|
||||
@@ -590,8 +435,7 @@ async fn cmd_set_status(
|
||||
id_or_username: &str,
|
||||
status: UserStatus,
|
||||
) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let user = resolve_user(&provider, id_or_username).await?;
|
||||
identity_users::update_status(&provider, &user.id, status.as_i32(), None, None, None).await?;
|
||||
@@ -606,8 +450,7 @@ async fn cmd_set_status(
|
||||
// ── reset-password ────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_reset_password(config: &Config, id_or_username: &str) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let user = resolve_user(&provider, id_or_username).await?;
|
||||
let user_roles = provider.roles().list_for_user(&user.id).await?;
|
||||
@@ -631,8 +474,7 @@ async fn cmd_reset_password(config: &Config, id_or_username: &str) -> anyhow::Re
|
||||
// ── create-token ──────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_create_token(config: &Config, user_ref: &str, name: &str) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let user = resolve_user(&provider, user_ref).await?;
|
||||
let (token, raw) = token_security::create_token(
|
||||
@@ -667,8 +509,7 @@ async fn cmd_create_token(config: &Config, user_ref: &str, name: &str) -> anyhow
|
||||
// ── revoke-token ──────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_revoke_token(config: &Config, id: &str) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let token = provider
|
||||
.tokens()
|
||||
@@ -722,7 +563,8 @@ async fn cmd_init(
|
||||
}
|
||||
}
|
||||
|
||||
let db_path = std::path::Path::new(&config.database.path);
|
||||
let sqlite_path = config.database.sqlite_path();
|
||||
let db_path = std::path::Path::new(&sqlite_path);
|
||||
println!("Creating database directory...");
|
||||
if let Some(parent) = db_path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
@@ -738,12 +580,9 @@ async fn cmd_init(
|
||||
}
|
||||
|
||||
// 2. Open DB pool and run migrations
|
||||
println!("Running migrations...");
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool.clone());
|
||||
|
||||
db::run_migrations(&pool).await?;
|
||||
println!("✓ Migrations applied successfully.");
|
||||
println!("Initializing database and migrations...");
|
||||
let (provider, backend, _pool) = db::init_provider(config).await?;
|
||||
println!("✓ Database initialized ({backend}).");
|
||||
|
||||
// 3. Create administrator
|
||||
if skip_admin {
|
||||
@@ -822,7 +661,8 @@ async fn run_init_validation(config: &Config, admin_skipped: bool) -> anyhow::Re
|
||||
println!(" ✓ Configuration");
|
||||
|
||||
// 2. Directories writable
|
||||
let db_path = std::path::Path::new(&config.database.path);
|
||||
let sqlite_path = config.database.sqlite_path();
|
||||
let db_path = std::path::Path::new(&sqlite_path);
|
||||
let mut dirs_ok = true;
|
||||
if let Some(parent) = db_path.parent() {
|
||||
if !parent.as_os_str().is_empty() && std::fs::create_dir_all(parent).is_err() {
|
||||
@@ -842,10 +682,11 @@ async fn run_init_validation(config: &Config, admin_skipped: bool) -> anyhow::Re
|
||||
ok = false;
|
||||
}
|
||||
|
||||
// 3. Database reachable
|
||||
let pool = match db::create_pool(&config.database.path).await {
|
||||
Ok(p) => {
|
||||
// 3. Database & Migrations reachable
|
||||
let provider = match db::init_provider(config).await {
|
||||
Ok((p, _, _)) => {
|
||||
println!(" ✓ Database");
|
||||
println!(" ✓ Migrations");
|
||||
p
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -854,21 +695,7 @@ async fn run_init_validation(config: &Config, admin_skipped: bool) -> anyhow::Re
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Migrations applied
|
||||
let migration_check: Result<(i64,), sqlx::Error> =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM _sqlx_migrations")
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
match migration_check {
|
||||
Ok((count,)) if count > 0 => println!(" ✓ Migrations"),
|
||||
_ => {
|
||||
println!(" ✗ Migrations not applied");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Admin account check
|
||||
let provider = make_provider(pool);
|
||||
// 4. Admin account check
|
||||
let admin_count = provider.users().count_admins().await.unwrap_or(0);
|
||||
if admin_count > 0 {
|
||||
println!(" ✓ Administrator account");
|
||||
@@ -891,7 +718,12 @@ async fn cmd_config_path(config: &Config, json: bool) -> anyhow::Result<()> {
|
||||
.or_else(Config::default_user_config_path)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let database_file = config.database.path.clone();
|
||||
let (database_url, _) = config.database.resolved_url().unwrap_or_else(|_| {
|
||||
(
|
||||
config.database.sqlite_path(),
|
||||
crate::config::DatabaseBackend::Sqlite,
|
||||
)
|
||||
});
|
||||
|
||||
let state_dir = if let Ok(home) = std::env::var("HOME") {
|
||||
std::path::Path::new(&home)
|
||||
@@ -905,7 +737,7 @@ async fn cmd_config_path(config: &Config, json: bool) -> anyhow::Result<()> {
|
||||
if json {
|
||||
let val = serde_json::json!({
|
||||
"config": config_file,
|
||||
"database": database_file,
|
||||
"database": database_url,
|
||||
"state": state_dir,
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&val)?);
|
||||
@@ -913,7 +745,7 @@ async fn cmd_config_path(config: &Config, json: bool) -> anyhow::Result<()> {
|
||||
println!("\nConfig:");
|
||||
println!(" {}", config_file);
|
||||
println!("\nDatabase:");
|
||||
println!(" {}", database_file);
|
||||
println!(" {}", database_url);
|
||||
println!("\nLogs/State:");
|
||||
println!(" {}", state_dir);
|
||||
println!();
|
||||
@@ -928,8 +760,7 @@ async fn cmd_show_user(
|
||||
id_or_username: &str,
|
||||
permissions: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let user = resolve_user(&provider, id_or_username).await?;
|
||||
|
||||
@@ -978,8 +809,7 @@ async fn cmd_show_user(
|
||||
// ── show-token ────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_show_token(config: &Config, id: &str) -> anyhow::Result<()> {
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
let provider = make_provider(pool);
|
||||
let (provider, _backend, _pool) = db::init_provider(config).await?;
|
||||
|
||||
let token = provider
|
||||
.tokens()
|
||||
@@ -1020,70 +850,149 @@ async fn cmd_show_token(config: &Config, id: &str) -> anyhow::Result<()> {
|
||||
// ── backup ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn cmd_backup(config: &Config, path: &std::path::Path) -> anyhow::Result<()> {
|
||||
// 1. Resolve paths to absolute paths
|
||||
let source_path = std::path::Path::new(&config.database.path);
|
||||
let (url, backend) = config.database.resolved_url()?;
|
||||
match backend {
|
||||
crate::config::DatabaseBackend::Sqlite => {
|
||||
let sqlite_path = config.database.sqlite_path();
|
||||
let source_path = std::path::Path::new(&sqlite_path);
|
||||
let abs_source =
|
||||
std::fs::canonicalize(source_path).unwrap_or_else(|_| source_path.to_path_buf());
|
||||
let abs_target = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path)
|
||||
};
|
||||
|
||||
let abs_source =
|
||||
std::fs::canonicalize(source_path).unwrap_or_else(|_| source_path.to_path_buf());
|
||||
let source_dir = abs_source
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid database source path"))?;
|
||||
let source_file_name = abs_source
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid database file name"))?
|
||||
.to_string_lossy();
|
||||
let source_wal = source_dir.join(format!("{}-wal", source_file_name));
|
||||
let source_shm = source_dir.join(format!("{}-shm", source_file_name));
|
||||
|
||||
let abs_target = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path)
|
||||
};
|
||||
if abs_target == abs_source {
|
||||
anyhow::bail!(
|
||||
"Backup destination cannot be the active database file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if abs_target == source_wal {
|
||||
anyhow::bail!(
|
||||
"Backup destination cannot be the active WAL file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if abs_target == source_shm {
|
||||
anyhow::bail!(
|
||||
"Backup destination cannot be the active SHM file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let source_dir = abs_source.parent().unwrap();
|
||||
let source_file_name = abs_source.file_name().unwrap().to_string_lossy();
|
||||
let source_wal = source_dir.join(format!("{}-wal", source_file_name));
|
||||
let source_shm = source_dir.join(format!("{}-shm", source_file_name));
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
|
||||
if abs_target == abs_source {
|
||||
anyhow::bail!(
|
||||
"Backup destination cannot be the active database file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if abs_target == source_wal {
|
||||
anyhow::bail!(
|
||||
"Backup destination cannot be the active WAL file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if abs_target == source_shm {
|
||||
anyhow::bail!(
|
||||
"Backup destination cannot be the active SHM file: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
// 2. Ensure parent directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
#[cfg(feature = "sqlite")]
|
||||
{
|
||||
let pool = db::create_pool(&sqlite_path).await?;
|
||||
let path_str = path.to_string_lossy().replace('\'', "''");
|
||||
let query = format!("VACUUM INTO '{}'", path_str);
|
||||
|
||||
sqlx::query(sqlx::AssertSqlSafe(query))
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"✓ SQLite database backup created successfully at: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
{
|
||||
anyhow::bail!("SQLite database backups require the 'sqlite' feature");
|
||||
}
|
||||
}
|
||||
crate::config::DatabaseBackend::Postgres => {
|
||||
let output = std::process::Command::new("pg_dump")
|
||||
.arg("-Fc")
|
||||
.arg("-d")
|
||||
.arg(&url)
|
||||
.arg("-f")
|
||||
.arg(path)
|
||||
.output()
|
||||
.context(
|
||||
"failed to execute pg_dump (ensure PostgreSQL client tools are installed)",
|
||||
)?;
|
||||
|
||||
if !output.status.success() {
|
||||
let err = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("pg_dump failed: {err}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"✓ PostgreSQL database backup created successfully at: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_restore(config: &Config, path: &std::path::Path) -> anyhow::Result<()> {
|
||||
if !path.exists() {
|
||||
anyhow::bail!("Backup file does not exist: {}", path.display());
|
||||
}
|
||||
|
||||
let (url, backend) = config.database.resolved_url()?;
|
||||
match backend {
|
||||
crate::config::DatabaseBackend::Sqlite => {
|
||||
let sqlite_path = config.database.sqlite_path();
|
||||
let target_path = std::path::Path::new(&sqlite_path);
|
||||
if let Some(parent) = target_path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
std::fs::copy(path, target_path).with_context(|| {
|
||||
format!("failed to restore backup to {}", target_path.display())
|
||||
})?;
|
||||
println!(
|
||||
"✓ SQLite database restored successfully from: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
crate::config::DatabaseBackend::Postgres => {
|
||||
let output = std::process::Command::new("pg_restore")
|
||||
.arg("--clean")
|
||||
.arg("--if-exists")
|
||||
.arg("-d")
|
||||
.arg(&url)
|
||||
.arg(path)
|
||||
.output()
|
||||
.context(
|
||||
"failed to execute pg_restore (ensure PostgreSQL client tools are installed)",
|
||||
)?;
|
||||
|
||||
if !output.status.success() {
|
||||
let err = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("pg_restore failed: {err}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"✓ PostgreSQL database restored successfully from: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Delete target file if it already exists to overwrite
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
|
||||
// 4. Perform SQLite VACUUM INTO
|
||||
// VACUUM INTO is a standard SQL statement supported by SQLite
|
||||
// for transactionally consistent online backups. It is the modern
|
||||
// SQL alternative to the online backup C API, especially on WAL-enabled databases.
|
||||
let pool = db::create_pool(&config.database.path).await?;
|
||||
|
||||
let path_str = path.to_string_lossy().replace('\'', "''");
|
||||
let query = format!("VACUUM INTO '{}'", path_str);
|
||||
|
||||
sqlx::query(sqlx::AssertSqlSafe(query))
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"✓ Database backup created successfully at: {}",
|
||||
path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user