nedots 0.1.2

A tool to manage configuration files/(ne)dots.
use std::{
    io::{Read, Write},
    path::{Path, PathBuf},
};

#[derive(Debug, clap::Args)]
/// Backup & compress local configuration files/(ne)dots - compression method is
/// zstd
pub(crate) struct BackupCmd {
    #[arg(short, long, default_value_t = false)]
    /// Don't archive/compress backups
    no_archive: bool,
}

impl super::Command for BackupCmd {
    fn run(
        &self,
        _: &super::nedots::RootCmd,
        config: &crate::config::Config,
    ) -> Result<(), super::CommandError> {
        backup(
            &config.sources,
            &config.root.join(get_dst()),
            !self.no_archive,
        )
    }
}

pub(crate) fn get_dst() -> String {
    format!("backups/{}", chrono::offset::Local::now().timestamp())
}

pub(crate) fn backup(
    sources: &[PathBuf],
    dst: &Path,
    archive: bool,
) -> Result<(), super::CommandError> {
    std::fs::create_dir_all(dst)?;
    log::debug!("Backing up to {}", dst.to_string_lossy());

    let pb = indicatif::ProgressBar::new_spinner();
    for source in sources {
        let src = Path::new(source);

        pb.tick();
        pb.set_message(format!("Backing up {}...", source.to_string_lossy()));
        crate::cmd::gather::gather(src, &dst.join(src.strip_prefix("/")?))?;
    }
    pb.finish_and_clear();

    if archive {
        archive_dir(dst)?;
    }

    Ok(())
}

fn archive_dir(dst: &Path) -> Result<(), super::CommandError> {
    let archive_name = format!("{}.zip", dst.to_string_lossy());
    log::debug!("Archiving to {}", archive_name);

    // Create the archive.
    let file = std::fs::File::create(archive_name).unwrap();

    // Create the ZipWriter, give it the file.
    let mut zip = zip::ZipWriter::new(file);

    // Set the compression method & destination file permissions.
    let options = zip::write::FileOptions::default()
        .compression_method(zip::CompressionMethod::Zstd)
        .unix_permissions(0o755);

    let pb = indicatif::ProgressBar::new_spinner();
    for entry in dst.read_dir()? {
        let path = entry?.path();
        let name = path.strip_prefix(Path::new(dst)).unwrap();

        pb.tick();
        pb.set_message(format!("Archiving {}...", path.to_string_lossy()));

        compress(&mut zip, options, dst, name, &path)?;
    }

    std::fs::remove_dir_all(dst)?;
    pb.finish_and_clear();

    Ok(())
}

fn compress(
    zip: &mut zip::ZipWriter<std::fs::File>,
    options: zip::write::FileOptions,
    dst: &Path,
    name: &Path,
    path: &Path,
) -> Result<(), super::CommandError> {
    let mut buffer = Vec::new();
    if path.is_file() {
        log::debug!("Compressing {}", name.to_string_lossy());
        zip.start_file(name.to_string_lossy(), options)?;
        let mut f = std::fs::File::open(path)?;
        f.read_to_end(&mut buffer)?;
        zip.write_all(&*buffer)?;
        buffer.clear();
    } else {
        for entry in path.read_dir()? {
            let path = entry?.path();
            let name = path.strip_prefix(Path::new(&dst)).unwrap();
            compress(zip, options, dst, name, &path)?;
        }
    }

    Ok(())
}