use crate::paths::{MakeDirs, RemoveDirs};
use std::{
io::{Read, Write},
path::{Path, PathBuf},
};
#[derive(Debug, clap::Args)]
pub(crate) struct BackupCmd {
#[arg(short, long)]
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);
let file = std::fs::File::create(&archive_name)?;
let mut zip = zip::ZipWriter::new(file);
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 {}