cli: avoid data-dir initialization for version; create db parent dirs; redact generated passwords in CLI output

- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization.
- Create parent directories when an explicit --database path is provided.
- Redact printed generated administrator passwords; announce file path or redact instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
thakaresandCopilot committed 2026-08-16 16:26:24 +05:30
commit 2ac6c81dfe
140 files changed
+31342

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
//! Backup and Restore engine for consistent SQLite snapshots and manifests.
use crate::error::{ApiError, ApiResult};
use chrono::Utc;
use nx9_wg_core::types::audit::AuditEventType;
use nx9_wg_core::types::backup::{BackupFileEntry, BackupManifest, BackupMeta};
use nx9_wg_db::Store;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use uuid::Uuid;
/// Backup and restore management service.
pub struct BackupService;
impl BackupService {
/// Create a consistent, atomic SQLite snapshot backup and manifest.
pub async fn create_backup(
store: &Store,
backup_dir: &Path,
description: Option<&str>,
actor: &str,
ip_address: Option<&str>,
) -> ApiResult<(BackupMeta, PathBuf)> {
if !backup_dir.exists() {
std::fs::create_dir_all(backup_dir).map_err(|e| {
ApiError::Internal(format!("Failed to create backup directory: {e}"))
})?;
}
let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let filename = format!("nx9-backup-{timestamp}.db");
let backup_path = backup_dir.join(&filename);
let backup_path_str = backup_path.to_string_lossy().to_string();
// 1. Perform atomic SQLite VACUUM INTO
store.vacuum_into(&backup_path_str).await?;
// 2. Read bytes to compute checksum and size
let bytes = std::fs::read(&backup_path)
.map_err(|e| ApiError::Internal(format!("Failed to read created backup file: {e}")))?;
let size_bytes = bytes.len() as i64;
let hash_bytes = Sha256::digest(&bytes);
let checksum = hash_bytes
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let now = Utc::now().naive_utc();
let backup_id = Uuid::new_v4();
let meta = BackupMeta {
id: backup_id,
filename: filename.clone(),
size_bytes,
checksum: checksum.clone(),
schema_version: "1".to_string(),
encrypted: false,
description: description.map(|s| s.to_string()),
created_at: now,
};
// 3. Write manifest file
let manifest = BackupManifest {
version: env!("CARGO_PKG_VERSION").to_string(),
schema_version: "1".to_string(),
created_at: now,
checksum: checksum.clone(),
encrypted: false,
files: vec![BackupFileEntry {
path: filename.clone(),
size_bytes: size_bytes as u64,
checksum: checksum.clone(),
}],
notes: description.map(|s| s.to_string()),
};
let manifest_path = backup_dir.join(format!("nx9-backup-{timestamp}.manifest.json"));
let manifest_json = serde_json::to_string_pretty(&manifest)
.map_err(|e| ApiError::Internal(format!("Failed to serialize backup manifest: {e}")))?;
std::fs::write(&manifest_path, manifest_json)
.map_err(|e| ApiError::Internal(format!("Failed to write backup manifest: {e}")))?;
// 4. Save metadata in SQLite
store.create_backup_meta(&meta).await?;
// 5. Audit event
let _ = store
.record_audit(
AuditEventType::BackupCreate,
actor,
Some("backup"),
Some(&backup_id.to_string()),
Some(&format!("Created backup '{filename}' ({size_bytes} bytes)")),
None,
ip_address,
)
.await;
Ok((meta, backup_path))
}
/// Verify the integrity and SQLite magic header of a backup file.
pub fn verify_backup(backup_file: &Path, expected_checksum: Option<&str>) -> ApiResult<bool> {
if !backup_file.exists() {
return Err(ApiError::NotFound(format!(
"Backup file '{}' not found",
backup_file.display()
)));
}
let bytes = std::fs::read(backup_file).map_err(|e| {
ApiError::Internal(format!("Failed to read backup file for verification: {e}"))
})?;
if bytes.len() < 100 {
return Ok(false);
}
// Verify SQLite 3 header magic
if &bytes[0..16] != b"SQLite format 3\0" {
return Ok(false);
}
// Verify checksum if supplied
if let Some(expected) = expected_checksum {
let hash_bytes = Sha256::digest(&bytes);
let calculated = hash_bytes
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
if calculated.to_lowercase() != expected.to_lowercase() {
return Ok(false);
}
}
Ok(true)
}
/// Restore database from a verified backup file with safety pre-restore backup snapshot.
pub async fn restore_backup(
store: &Store,
backup_file: &Path,
active_db_path: &Path,
safety_dir: &Path,
actor: &str,
ip_address: Option<&str>,
) -> ApiResult<()> {
// 1. Verify backup file before touching active DB
let is_valid = Self::verify_backup(backup_file, None)?;
if !is_valid {
return Err(ApiError::Validation(
"Backup file failed verification: invalid SQLite format or corrupted data"
.to_string(),
));
}
// 2. Create pre-restore safety snapshot of the active database
if active_db_path.exists() {
if !safety_dir.exists() {
let _ = std::fs::create_dir_all(safety_dir);
}
let safety_name = format!(
"pre-restore-safety-{}.bak",
Utc::now().format("%Y%m%d-%H%M%S")
);
let safety_path = safety_dir.join(safety_name);
let _ = store.vacuum_into(&safety_path.to_string_lossy()).await;
}
// 3. Record audit event before closing pool
let _ = store
.record_audit(
AuditEventType::BackupRestore,
actor,
Some("backup"),
None,
Some(&format!(
"Database restore initiated from '{}'",
backup_file.display()
)),
None,
ip_address,
)
.await;
// 4. Close store pool to release file locks
store.close().await;
// 5. Clean up existing active database and WAL, SHM, and journal files
let wal_path = PathBuf::from(format!("{}-wal", active_db_path.display()));
let shm_path = PathBuf::from(format!("{}-shm", active_db_path.display()));
let journal_path = PathBuf::from(format!("{}-journal", active_db_path.display()));
let _ = std::fs::remove_file(wal_path);
let _ = std::fs::remove_file(shm_path);
let _ = std::fs::remove_file(journal_path);
let _ = std::fs::remove_file(active_db_path);
// 6. Copy backup file to active database location
std::fs::copy(backup_file, active_db_path)
.map_err(|e| ApiError::Internal(format!("Failed to restore database file: {e}")))?;
Ok(())
}
}