bkr 1.0.0

Backup and restore tool for syncing files to AWS S3 with native zstd compression
Documentation
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();

    // Get original backup items with unexpanded paths
    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");
    }

    // Validate that all backup paths exist before starting
    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();

    // Backup each item
    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()
        };

        // Get the original path from the raw config (preserves ~ and env vars)
        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;
    }

    // Calculate duration
    let duration = start_time.elapsed().as_secs_f64();

    // Get daemon status
    let daemon = DaemonManager::new(config_path);
    let daemon_info = daemon.is_installed();

    // Upload timestamp file
    println!("\nUploading backup timestamp...");

    // Create a new S3Manager for timestamp upload (to ensure fresh connection)
    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");
    }

    // Use restore-specific bucket prefix (can be different from backup prefix)
    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);

    // Simple restore: sync from S3 prefix to local directory
    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());

    // Check if paths exist
    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<()> {
    // Expand ~ to home directory
    let config_path = shellexpand::tilde(paths::DEFAULT_CONFIG).to_string();
    let config_path = PathBuf::from(&config_path);

    // Check if config already exists
    if config_path.exists() && !force {
        eprintln!("Configuration file already exists: {}", config_path.display());
        eprintln!("Use --force to overwrite");
        std::process::exit(1);
    }

    // Create parent directory if needed
    if let Some(parent) = config_path.parent() {
        fs::create_dir_all(parent)?;
    }

    // Write template
    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(())
}