cockup 0.2.3

Yet another backup tool for various configurations
mod brew;
mod cli;
mod config;
mod files;
mod hooks;
mod ingredients;
mod manifest;
mod report;
use clap::{CommandFactory, Parser};
use cli::{CacheCommand, Commands, ConfigArgs, IngredientCommand};
use ingredients::Mode;
fn backup_initialized(path: &std::path::Path) -> Result<bool, String> {
    let destination = config::destination(path)?;
    match std::fs::symlink_metadata(&destination) {
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            report::warning(&format!(
                "Backup is not initialized: {}",
                destination.display()
            ));
            Ok(false)
        }
        Err(error) => Err(format!("{}: {error}", destination.display())),
        Ok(_) if destination.is_dir() => Ok(true),
        Ok(_) => Err(format!(
            "Backup destination is not a directory: {}",
            destination.display()
        )),
    }
}
fn snapshot(path: &std::path::Path) -> Result<Mode, String> {
    let destination = config::destination(path)?;
    let ingredients = manifest::Manifest::load(&destination)?
        .map(|manifest| manifest.ingredients)
        .unwrap_or_default();
    Ok(Mode::Snapshot(ingredients))
}
fn configured(
    args: &ConfigArgs,
    dry_run: bool,
    mode: Mode,
) -> Result<Option<config::Config>, String> {
    let cfg = config::load(&args.config_file, mode)?;
    if !dry_run && !args.approve_hooks && cfg.all_hooks().next().is_some() {
        report::warning(
            "Hooks detected in configuration. Ensure commands are safe before execution.",
        );
        if !report::confirm()? {
            return Ok(None);
        }
    }
    Ok(Some(cfg))
}
fn run() -> Result<(), String> {
    if std::env::args_os().len() == 1 {
        cli::Cli::command()
            .print_help()
            .map_err(|e| e.to_string())?;
        println!();
        return Ok(());
    }
    match cli::Cli::parse().command {
        Commands::Backup(args) => {
            if let Some(cfg) = configured(
                &args.config,
                args.dry_run,
                if args.dry_run {
                    Mode::ReadOnly
                } else {
                    Mode::Update
                },
            )? {
                files::execute(&cfg, false, args.dry_run, files::RestorePolicy::Ask)?;
            }
        }
        Commands::Restore(args) => {
            let mode = snapshot(&args.file.config.config_file)?;
            if let Some(cfg) = configured(&args.file.config, args.file.dry_run, mode)? {
                let policy = if args.r#override {
                    files::RestorePolicy::Override
                } else if args.skip_existing {
                    files::RestorePolicy::SkipExisting
                } else {
                    files::RestorePolicy::Ask
                };
                files::execute(&cfg, true, args.file.dry_run, policy)?;
            }
        }
        Commands::Verify { config_file } => {
            if backup_initialized(&config_file)? {
                let mode = snapshot(&config_file)?;
                let cfg = config::load(&config_file, mode)?;
                files::verify(&cfg)?;
            }
        }
        Commands::Diff {
            config_file,
            summary,
        } => {
            if backup_initialized(&config_file)? {
                let mode = snapshot(&config_file)?;
                let cfg = config::load(&config_file, mode)?;
                files::diff(&cfg, summary)?;
            }
        }
        Commands::Hook { config, name } => {
            if let Some(cfg) = configured(&config, false, Mode::ReadOnly)? {
                hooks::select(&cfg, name.as_deref())?;
            }
        }
        Commands::Detect { casks } => brew::detect(casks)?,
        Commands::Ingredient { command } => match command {
            IngredientCommand::Show { name } => ingredients::show(&name)?,
            IngredientCommand::Search { query } => ingredients::search(&query)?,
        },
        Commands::Cache { command } => match command {
            CacheCommand::Status => ingredients::cache_status()?,
            CacheCommand::List => ingredients::cache_list()?,
            CacheCommand::Show { name } => ingredients::status(&name)?,
            CacheCommand::Refresh {
                name,
                ingredients: all_ingredients,
                catalog,
            } => {
                ingredients::refresh(name.as_deref(), all_ingredients, catalog)?;
            }
            CacheCommand::Delete {
                names,
                catalog,
                ingredients: all_ingredients,
            } => {
                if catalog {
                    ingredients::delete_catalog()?;
                } else if all_ingredients {
                    ingredients::delete_all()?;
                } else {
                    ingredients::delete(&names)?;
                }
            }
            CacheCommand::Clean => ingredients::clean()?,
        },
        Commands::Template => print!("{}", include_str!("template.yaml")),
        Commands::Completions { shell } => {
            let mut command = cli::Cli::command();
            clap_complete::generate(shell, &mut command, "cockup", &mut std::io::stdout());
        }
    }
    Ok(())
}
fn main() {
    if let Err(error) = run() {
        report::error(&error);
        std::process::exit(1);
    }
}