nedots 0.1.3

A tool to manage configuration files/(ne)dots.
use crate::paths::{MakeDirs, RemoveDirs};
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)]
    /// Don't archive/compress backups
    no_archive: bool,
}

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

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

pub(crate) fn backup(sources: &[PathBuf], dst: &Path, archive: bool) -> anyhow::Result<()> {
    dst.make_all_dirs()?;
    log::trace!("Backing up to {}", dst.to_string_lossy());

    for source in sources {
        crate::cmd::gather::gather_file(source, &dst.join(source.strip_prefix("/")?))?;
        log::info!(
            "💽 Backed up {} -> {}",
            console::style(source.display()).blue().bold(),
            console::style(dst.display()).blue().bold()
        );
    }

    if archive {
        archive_dir(dst)?;
    }

    Ok(())
}

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

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

    // 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);

    for entry in dst.read_dir()? {
        let path = entry?.path();
        let name = path.strip_prefix(Path::new(dst))?;

        compress(&mut zip, options, dst, name, &path)?;
        log::info!(
            "💾 Compressed {}",
            console::style(&archive_name).cyan().bold()
        );
    }

    dst.remove_all_dirs()?;
    Ok(())
}

fn compress(
    zip: &mut zip::ZipWriter<std::fs::File>,
    options: zip::write::FileOptions,
    dst: &Path,
    name: &Path,
    path: &Path,
) -> anyhow::Result<()> {
    let mut buffer = Vec::new();
    if path.is_file() {
        log::trace!("Compressing {}", name.display());

        zip.start_file(name.to_string_lossy(), options)?;
        let mut file = std::fs::File::open(path)?;

        file.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(())
}

#[cfg(test)]
mod tests {}