forge_backup 1.2.0

A program to backup all the user home folders to an S3 bucket.
Documentation
use clap::Parser;
use forge_backup::{
    args::Args,
    backup,
    config::Config,
    error::{AppResult, MailerError},
    mailer::MailgunMailer,
};
use std::io::{self, Write};
use which::which;

#[tokio::main]
async fn main() {
    let args = Args::parse();
    if !check_prerequisites() {
        println!("Ensure that zip and aws are both installed an in the PATH");
        std::process::exit(1);
    }

    let mut config = match Config::load() {
        Ok(config) => config,
        Err(error) => {
            eprintln!("ERROR: Unable to load config");
            eprintln!("{error}");

            let config = Config::fallback();
            println!("Using fallback config: \n{config}");
            config
        }
    };

    config.update_from_args(args);

    println!("Starting backup...");
    io::stdout().flush().unwrap();
    match backup::run(&config) {
        Ok((successes, errors)) => {
            if config.notify_on_success {
                match send_email(&successes, &errors, &config).await {
                    Ok(_) => println!("Email sent."),
                    Err(_) => eprintln!("Error sending email."),
                }
            } else {
                eprintln!("\nSuccessful: \n{successes}");
                eprintln!("\nErrors: \n{errors}");
            }
        }
        Err(e) => {
            eprintln!("Error Returned.");
            eprintln!("{e}");
        }
    }
}

async fn send_email(success: &str, errors: &str, config: &Config) -> AppResult<()> {
    if !config.mailgun_api_base.starts_with("http") {
        return Err(
            MailerError::ConfigError("api_base does not start with http".to_owned()).into(),
        );
    }

    if config
        .mailgun_api_base
        .chars()
        .filter(|c| *c == '.')
        .count()
        == 0
    {
        return Err(
            MailerError::ConfigError("api_base does not contain any dots".to_owned()).into(),
        );
    }

    if config.mailgun_api_key.len() < 35 {
        return Err(MailerError::ConfigError("api_key is too short".to_owned()).into());
    }

    if config.mailgun_domain.chars().filter(|c| *c == '.').count() == 0 {
        return Err(MailerError::ConfigError(
            "mailgun_domain does not contain any dots".to_owned(),
        )
        .into());
    }

    let mailer = MailgunMailer::create(config);

    let subject = if errors.is_empty() {
        format!("SUCCESS: {} - Backup Successful", config.hostname)
    } else {
        format!("ERROR: {} - Backup had error(s).", config.hostname)
    };

    let mut body = String::new();

    body.push_str(format!("HOSTNAME: {}\n\nForge backup status\n\n", config.hostname).as_str());

    body.push_str(format!("SUCESS:\n{success}\n\nERROR:\n{errors}\n\n").as_str());

    mailer.send(&subject, &body).await?;

    Ok(())
}

fn check_prerequisites() -> bool {
    let prereqs = ["zip", "aws"];
    let mut passed = true;

    prereqs.iter().for_each(|&cmd| {
        if which(cmd).is_err() {
            println!("Missing prerequisite: '{cmd}'");
            passed = false;
        }
    });

    passed
}