use chamber_backup::BackupManager;
use chamber_import_export::{detect_format_from_extension, import_items};
use chamber_vault::{BackupConfig, Vault};
use clap::Subcommand;
use color_eyre::eyre::eyre;
use std::path::{Path, PathBuf};
#[derive(Subcommand, Debug)]
pub enum BackupCommand {
Configure {
#[arg(long)]
enable: Option<bool>,
#[arg(long)]
interval: Option<u64>,
#[arg(long)]
max_backups: Option<usize>,
#[arg(long)]
backup_dir: Option<PathBuf>,
#[arg(long)]
format: Option<String>,
#[arg(long)]
compress: Option<bool>,
#[arg(long)]
verify: Option<bool>,
},
Now {
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
},
List {
#[arg(long, short)]
verbose: bool,
},
Restore {
backup_path: PathBuf,
#[arg(long, short)]
yes: bool,
},
Verify {
backup_path: PathBuf,
},
Status,
Cleanup {
#[arg(long)]
keep: Option<usize>,
#[arg(long)]
dry_run: bool,
},
}
pub fn handle_backup_command(vault: Vault, cmd: BackupCommand) -> color_eyre::Result<()> {
match cmd {
BackupCommand::Configure {
enable,
interval,
max_backups,
backup_dir,
format,
compress,
verify,
} => handle_configure(
&vault,
enable,
interval,
max_backups,
backup_dir,
format,
compress,
verify,
),
BackupCommand::Now { output, force } => handle_backup_now(vault, output, force),
BackupCommand::List { verbose } => handle_list_backups(vault, verbose),
BackupCommand::Restore { backup_path, yes } => handle_restore_backup(vault, &backup_path, yes),
BackupCommand::Verify { backup_path } => handle_verify_backup(&backup_path),
BackupCommand::Status => handle_backup_status(vault),
BackupCommand::Cleanup { keep, dry_run } => handle_cleanup_backups(vault, keep, dry_run),
}
}
#[allow(clippy::too_many_arguments)]
fn handle_configure(
vault: &Vault,
enable: Option<bool>,
interval: Option<u64>,
max_backups: Option<usize>,
backup_dir: Option<PathBuf>,
format: Option<String>,
compress: Option<bool>,
verify: Option<bool>,
) -> color_eyre::Result<()> {
let mut config = vault.get_backup_config().unwrap_or_default();
let mut changed = false;
if let Some(enabled) = enable {
config.enabled = enabled;
changed = true;
println!("Automatic backups {}", if enabled { "enabled" } else { "disabled" });
}
if let Some(hours) = interval {
if hours == 0 {
return Err(eyre!("Backup interval must be greater than 0"));
}
config.interval_hours = hours;
changed = true;
println!("Backup interval set to {hours} hours");
}
if let Some(max) = max_backups {
if max == 0 {
return Err(eyre!("Max backups must be greater than 0"));
}
config.max_backups = max;
changed = true;
println!("Maximum backups set to {max}");
}
if let Some(dir) = backup_dir {
config.backup_dir.clone_from(&dir);
changed = true;
println!("Backup directory set to: {}", dir.display());
}
if let Some(fmt) = format {
match fmt.as_str() {
"json" | "csv" | "backup" => {
config.format.clone_from(&fmt);
changed = true;
println!("Backup format set to: {fmt}");
}
_ => return Err(eyre!("Invalid format '{}'. Use: json, csv, or backup", fmt)),
}
}
if let Some(comp) = compress {
config.compress = comp;
changed = true;
println!("Compression {}", if comp { "enabled" } else { "disabled" });
}
if let Some(ver) = verify {
config.verify_after_backup = ver;
changed = true;
println!("Backup verification {}", if ver { "enabled" } else { "disabled" });
}
if changed {
vault.set_backup_config(&config)?;
println!("Backup configuration updated successfully");
} else {
println!("No configuration changes specified");
print_backup_config(&vault.get_backup_config().unwrap_or_default());
}
Ok(())
}
fn handle_backup_now(mut vault: Vault, output: Option<PathBuf>, force: bool) -> color_eyre::Result<()> {
if !vault.is_unlocked() {
let password = rpassword::prompt_password("Enter master password: ")?;
vault.unlock(&password)?;
}
let config = if let Some(custom_path) = output {
let mut config = vault.get_backup_config().unwrap_or_default();
config.backup_dir = custom_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf();
config
} else {
vault.get_backup_config().unwrap_or_default()
};
let mut backup_manager = BackupManager::new(vault, config);
println!("Creating backup...");
let backup_path = if force {
backup_manager.force_backup()?
} else {
backup_manager
.backup_if_needed()?
.ok_or_else(|| eyre!("No backup needed (use --force to create anyway)"))?
};
println!("✅ Backup created successfully: {}", backup_path.display());
let metadata = std::fs::metadata(&backup_path)?;
println!(" Size: {} bytes", metadata.len());
println!(
" Created: {}",
chrono::DateTime::<chrono::Utc>::from(metadata.created()?).format("%Y-%m-%d %H:%M:%S UTC")
);
Ok(())
}
fn handle_list_backups(vault: Vault, verbose: bool) -> color_eyre::Result<()> {
let config = vault.get_backup_config().unwrap_or_default();
let backup_manager = BackupManager::new(vault, config);
let backups = backup_manager.find_all_backups()?;
if backups.is_empty() {
println!("No backups found in: {}", backup_manager.config.backup_dir.display());
return Ok(());
}
println!("Found {} backup(s):", backups.len());
println!();
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::expect_used)]
for (i, backup_path) in backups.iter().enumerate() {
let metadata = std::fs::metadata(backup_path)?;
let size = metadata.len();
let modified = metadata.modified()?;
let datetime = chrono::DateTime::<chrono::Utc>::from(modified);
println!(
"{}. {}",
i + 1,
backup_path
.file_name()
.expect("Unable to get the file name")
.to_string_lossy()
);
println!(" Path: {}", backup_path.display());
println!(" Size: {} bytes ({:.2} MB)", size, size as f64 / 1_048_576.0);
println!(" Date: {}", datetime.format("%Y-%m-%d %H:%M:%S UTC"));
if verbose {
if let Some(timestamp) = backup_manager.extract_timestamp_from_filename(backup_path) {
println!(
" Timestamp: {}",
timestamp.format(&time::format_description::well_known::Rfc3339)?
);
}
}
println!();
}
Ok(())
}
fn handle_restore_backup(mut vault: Vault, backup_path: &Path, skip_confirmation: bool) -> color_eyre::Result<()> {
if !backup_path.exists() {
return Err(eyre!("Backup file not found: {}", backup_path.display()));
}
println!("Verifying backup integrity...");
verify_backup_file(backup_path)?;
println!("✅ Backup verification passed");
if !skip_confirmation {
println!("⚠️ WARNING: This will replace all current vault data!");
print!("Are you sure you want to restore from backup? (y/N): ");
std::io::Write::flush(&mut std::io::stdout())?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Restore cancelled");
return Ok(());
}
}
let password = rpassword::prompt_password("Enter master password: ")?;
vault.unlock(&password)?;
let format = chamber_import_export::detect_format_from_extension(backup_path)
.unwrap_or(chamber_import_export::ExportFormat::Json);
println!("Importing backup data...");
let items = chamber_import_export::import_items(backup_path, &format)?;
println!("Found {} items in backup", items.len());
let existing_items = vault.list_items()?;
for item in existing_items {
vault.delete_item(item.id)?;
}
let mut imported_count = 0;
for item in items {
match vault.create_item(&item) {
Ok(()) => imported_count += 1,
Err(e) => println!("⚠️ Failed to import '{}': {}", item.name, e),
}
}
println!("✅ Successfully imported {imported_count} items from backup");
Ok(())
}
fn handle_verify_backup(backup_path: &PathBuf) -> color_eyre::Result<()> {
println!("Verifying backup: {}", backup_path.display());
verify_backup_file(backup_path)?;
println!("✅ Backup verification passed");
let metadata = std::fs::metadata(backup_path)?;
println!(" Size: {} bytes", metadata.len());
if let Some(format) = chamber_import_export::detect_format_from_extension(backup_path) {
if let Ok(items) = chamber_import_export::import_items(backup_path, &format) {
println!(" Items: {}", items.len());
}
}
Ok(())
}
fn handle_backup_status(vault: Vault) -> color_eyre::Result<()> {
let config = vault.get_backup_config().unwrap_or_default();
println!("🔒 Backup Configuration Status");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
print_backup_config(&config);
let backup_manager = BackupManager::new(vault, config);
if let Ok(Some(last_backup)) = backup_manager.find_most_recent_backup() {
println!();
println!("📁 Most Recent Backup:");
println!(" File: {}", last_backup.display());
if let Ok(metadata) = std::fs::metadata(&last_backup) {
let modified = metadata.modified()?;
let datetime = chrono::DateTime::<chrono::Utc>::from(modified);
println!(" Date: {}", datetime.format("%Y-%m-%d %H:%M:%S UTC"));
println!(" Size: {} bytes", metadata.len());
}
} else {
println!();
println!("📁 No backups found");
}
Ok(())
}
fn handle_cleanup_backups(vault: Vault, keep: Option<usize>, dry_run: bool) -> color_eyre::Result<()> {
let mut config = vault.get_backup_config().unwrap_or_default();
if let Some(keep_count) = keep {
config.max_backups = keep_count;
}
let backup_manager = BackupManager::new(vault, config.clone());
let all_backups = backup_manager.find_all_backups()?;
if all_backups.len() <= config.max_backups {
println!(
"No cleanup needed. Found {} backups, keeping {}",
all_backups.len(),
config.max_backups
);
return Ok(());
}
let to_delete = all_backups.len() - config.max_backups;
println!(
"Found {} backups, will {} {} oldest backup(s)",
all_backups.len(),
if dry_run { "identify" } else { "delete" },
to_delete
);
let mut sorted_backups = all_backups;
sorted_backups.sort_by(|a, b| {
let time_a = backup_manager
.extract_timestamp_from_filename(a)
.unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
let time_b = backup_manager
.extract_timestamp_from_filename(b)
.unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
time_a.cmp(&time_b)
});
for (i, backup_path) in sorted_backups.iter().take(to_delete).enumerate() {
if dry_run {
println!(" [{}] Would delete: {}", i + 1, backup_path.display());
} else {
match std::fs::remove_file(backup_path) {
Ok(()) => println!(" [{}] Deleted: {}", i + 1, backup_path.display()),
Err(e) => println!(" [{}] Failed to delete {}: {}", i + 1, backup_path.display(), e),
}
}
}
if dry_run {
println!("Dry run completed. Use without --dry-run to actually delete files.");
} else {
println!("✅ Cleanup completed");
}
Ok(())
}
fn print_backup_config(config: &BackupConfig) {
println!(
" Status: {}",
if config.enabled { "✅ Enabled" } else { "❌ Disabled" }
);
println!(" Directory: {}", config.backup_dir.display());
println!(" Interval: {} hours", config.interval_hours);
println!(" Max backups: {}", config.max_backups);
println!(" Format: {}", config.format);
println!(
" Compression: {}",
if config.compress { "✅ Enabled" } else { "❌ Disabled" }
);
println!(
" Verification: {}",
if config.verify_after_backup {
"✅ Enabled"
} else {
"❌ Disabled"
}
);
}
fn verify_backup_file(backup_path: &std::path::Path) -> color_eyre::Result<()> {
if !backup_path.exists() {
return Err(eyre!("Backup file does not exist"));
}
let metadata = std::fs::metadata(backup_path)?;
if metadata.len() == 0 {
return Err(eyre!("Backup file is empty"));
}
if let Some(format) = detect_format_from_extension(backup_path) {
import_items(backup_path, &format)?;
} else {
return Err(eyre!("Unable to detect backup format"));
}
Ok(())
}