use bkr::cli::{Cli, Commands};
use bkr::config::{get_original_backup_items, ConfigParser};
use bkr::constants::{paths, ui, BackupStatus, CONFIG_TEMPLATE, BAKER_VERSION};
use bkr::daemon::DaemonManager;
use bkr::s3::S3Manager;
use bkr::sync::SyncManager;
use clap::Parser;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let result = match cli.command {
Commands::Init { force } => handle_init(force),
Commands::Backup {
config,
verbose,
dry_run,
no_user_interaction,
} => handle_backup(config.as_deref(), verbose, dry_run, no_user_interaction).await,
Commands::Restore {
config,
verbose,
dry_run,
no_user_interaction,
} => handle_restore(config.as_deref(), verbose, dry_run, no_user_interaction).await,
Commands::List { config } => handle_list(config.as_deref()),
Commands::Validate { config } => handle_validate(config.as_deref()),
Commands::DaemonInstall { config } => handle_daemon_install(config.as_deref()),
Commands::DaemonUninstall { config } => handle_daemon_uninstall(config.as_deref()),
Commands::DaemonStatus { config } => handle_daemon_status(config.as_deref()),
};
if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
async fn handle_backup(
config_path: Option<&str>,
verbose: bool,
dry_run: bool,
no_user_interaction: bool,
) -> bkr::Result<()> {
let start_time = Instant::now();
let mut backup_status = BackupStatus::Success;
let parser = ConfigParser::new(config_path)?;
let config = parser.get_config();
let original_backup_items = get_original_backup_items(config_path);
println!("Starting backup...");
if dry_run {
println!("DRY RUN MODE - No files will be uploaded\n");
}
if no_user_interaction {
println!("NO USER INTERACTION MODE - Using safe defaults\n");
}
let mut missing_paths = Vec::new();
for (alias, item) in &config.to_backup {
let path = Path::new(&item.path);
if !path.exists() {
missing_paths.push(format!("{}: {}", alias, item.path));
} else if !path.is_dir() {
missing_paths.push(format!("{}: {} (not a directory)", alias, item.path));
}
}
if !missing_paths.is_empty() {
eprintln!("The following backup paths do not exist or are not directories:");
for path in &missing_paths {
eprintln!(" - {}", path);
}
std::process::exit(1);
}
let s3_manager = S3Manager::new(&config.storage).await?;
let sync_manager = SyncManager::new(s3_manager, verbose);
let mut total_stats = bkr::SyncStats::default();
for (alias, item) in &config.to_backup {
println!("\n{}", "=".repeat(ui::SEPARATOR_WIDTH));
println!("Backing up: {}", alias);
println!("{}", "=".repeat(ui::SEPARATOR_WIDTH));
let s3_prefix = if let Some(ref prefix) = item.destination_prefix {
format!("{}/{}", prefix, alias)
} else {
alias.clone()
};
let original_path = original_backup_items
.get(alias)
.map(|i| i.path.as_str())
.unwrap_or(&item.path);
let stats = sync_manager
.backup(
alias,
Path::new(&item.path),
&s3_prefix,
original_path,
&item.ignore,
item.destination_prefix.as_deref(),
&item.zstd,
item.zstd_level.unwrap_or(3),
dry_run,
)
.await?;
total_stats.uploaded += stats.uploaded;
total_stats.downloaded += stats.downloaded;
total_stats.deleted += stats.deleted;
total_stats.skipped += stats.skipped;
total_stats.errors += stats.errors;
println!("\nCompleted: {}", alias);
println!(
" Uploaded: {}, Deleted: {}, Skipped: {}, Errors: {}",
stats.uploaded, stats.deleted, stats.skipped, stats.errors
);
}
println!("\n{}", "=".repeat(ui::SEPARATOR_WIDTH));
println!("Backup complete!");
println!("{}", "=".repeat(ui::SEPARATOR_WIDTH));
println!("Total uploaded: {}", total_stats.uploaded);
println!("Total deleted: {}", total_stats.deleted);
println!("Total skipped: {}", total_stats.skipped);
println!("Total errors: {}", total_stats.errors);
if total_stats.errors > 0 {
backup_status = BackupStatus::Failed;
}
let duration = start_time.elapsed().as_secs_f64();
let daemon = DaemonManager::new(config_path);
let daemon_info = daemon.is_installed();
println!("\nUploading backup timestamp...");
let s3_manager = S3Manager::new(&config.storage).await?;
let sync_manager = SyncManager::new(s3_manager, verbose);
sync_manager
.upload_timestamp(&total_stats, duration, &daemon_info, backup_status, BAKER_VERSION, dry_run)
.await?;
if total_stats.errors > 0 {
std::process::exit(1);
}
Ok(())
}
async fn handle_restore(
config_path: Option<&str>,
verbose: bool,
dry_run: bool,
no_user_interaction: bool,
) -> bkr::Result<()> {
let parser = ConfigParser::new(config_path)?;
let config = parser.get_config();
println!("Starting restore...");
if dry_run {
println!("DRY RUN MODE - No files will be downloaded\n");
}
if no_user_interaction {
println!("NO USER INTERACTION MODE - Proceeding automatically\n");
}
let restore_bucket_prefix = config
.storage
.restore
.as_ref()
.and_then(|r| r.bucket_prefix.clone())
.unwrap_or_default();
let restore_ignore_patterns = config
.storage
.restore
.as_ref()
.map(|r| r.ignore.clone())
.unwrap_or_default();
let restore_path = config
.storage
.restore
.as_ref()
.and_then(|r| r.path.clone())
.unwrap_or_else(|| "~/.backups".to_string());
println!(
"Restore source: s3://{}/{}",
config.storage.bucket.name,
if restore_bucket_prefix.is_empty() {
"(root)"
} else {
&restore_bucket_prefix
}
);
println!("Restore destination: {}", restore_path);
if !restore_ignore_patterns.is_empty() {
println!("Ignore patterns: {}", restore_ignore_patterns.join(", "));
}
let s3_manager = S3Manager::new(&config.storage).await?;
let sync_manager = SyncManager::new(s3_manager, verbose);
let stats = sync_manager
.restore(
&restore_bucket_prefix,
Path::new(&restore_path),
&restore_ignore_patterns,
dry_run,
no_user_interaction,
)
.await?;
println!("\n{}", "=".repeat(ui::SEPARATOR_WIDTH));
println!("Restore complete!");
println!("{}", "=".repeat(ui::SEPARATOR_WIDTH));
println!("Downloaded: {}", stats.downloaded);
println!("Deleted: {}", stats.deleted);
println!("Skipped: {}", stats.skipped);
println!("Errors: {}", stats.errors);
println!("\nRestored to: {}", restore_path);
if stats.errors > 0 {
std::process::exit(1);
}
Ok(())
}
fn handle_list(config_path: Option<&str>) -> bkr::Result<()> {
let parser = ConfigParser::new(config_path)?;
let config = parser.get_config();
println!("Backup configuration:");
println!(
"\nBucket: s3://{}/{}",
config.storage.bucket.name,
config.storage.bucket.prefix.as_deref().unwrap_or("")
);
println!("Profile: {}", config.storage.profile);
println!(
"Region: {}",
config.storage.region.as_deref().unwrap_or("default")
);
println!("\nItems to backup:");
for (alias, item) in &config.to_backup {
let exists = Path::new(&item.path).exists();
let status_icon = if exists { "[OK]" } else { "[MISSING]" };
println!("\n {} {}:", status_icon, alias);
println!(" Path: {}", item.path);
if !exists {
println!(" Status: Path does not exist!");
}
if let Some(ref prefix) = item.destination_prefix {
println!(" Destination: {}/{}", prefix, alias);
}
if !item.ignore.is_empty() {
println!(" Ignore patterns: {}", item.ignore.join(", "));
}
if !item.zstd.is_empty() {
println!(" Compress (zstd): {}", item.zstd.join(", "));
println!(" Compression level: {}", item.zstd_level.unwrap_or(3));
}
}
Ok(())
}
fn handle_validate(config_path: Option<&str>) -> bkr::Result<()> {
let parser = ConfigParser::new(config_path)?;
let config = parser.get_config();
println!("Configuration is valid!");
println!("\nConfig summary:");
println!(" Bucket: {}", config.storage.bucket.name);
println!(" Profile: {}", config.storage.profile);
println!(" Backup items: {}", config.to_backup.len());
println!("\nPath validation:");
let mut all_paths_exist = true;
for (alias, item) in &config.to_backup {
let exists = Path::new(&item.path).exists();
let icon = if exists { "[OK]" } else { "[MISSING]" };
println!(" {} {}: {}", icon, alias, item.path);
if !exists {
all_paths_exist = false;
}
}
if !all_paths_exist {
println!("\nWarning: Some backup paths do not exist");
}
Ok(())
}
fn handle_daemon_install(config_path: Option<&str>) -> bkr::Result<()> {
let daemon = DaemonManager::new(config_path);
daemon.install()
}
fn handle_daemon_uninstall(config_path: Option<&str>) -> bkr::Result<()> {
let daemon = DaemonManager::new(config_path);
daemon.uninstall()
}
fn handle_daemon_status(config_path: Option<&str>) -> bkr::Result<()> {
let daemon = DaemonManager::new(config_path);
daemon.status()
}
fn handle_init(force: bool) -> bkr::Result<()> {
let config_path = shellexpand::tilde(paths::DEFAULT_CONFIG).to_string();
let config_path = PathBuf::from(&config_path);
if config_path.exists() && !force {
eprintln!("Configuration file already exists: {}", config_path.display());
eprintln!("Use --force to overwrite");
std::process::exit(1);
}
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&config_path, CONFIG_TEMPLATE)?;
println!("Created configuration file: {}", config_path.display());
println!("\nNext steps:");
println!(" 1. Edit the config file with your S3 bucket and backup paths");
println!(" 2. Run 'bkr validate' to check your configuration");
println!(" 3. Run 'bkr backup --dry-run' to preview what will be backed up");
Ok(())
}