use std::io::Write;
use std::path::Path;
use anyhow::{Context, Result};
use flate2::Compression;
use flate2::write::GzEncoder;
use ignore::WalkBuilder;
use tar::Builder as TarBuilder;
pub fn build_archive_bytes(source_dir: &Path) -> Result<Vec<u8>> {
let mut buf: Vec<u8> = Vec::new();
write_archive(source_dir, &mut buf)?;
Ok(buf)
}
pub fn write_archive(source_dir: &Path, w: impl Write) -> Result<()> {
let encoder = GzEncoder::new(w, Compression::fast());
let mut archive = TarBuilder::new(encoder);
let walker = WalkBuilder::new(source_dir)
.hidden(false)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.filter_entry(|entry: &ignore::DirEntry| {
let name = entry.file_name().to_string_lossy();
name != ".git" && name != ".harmont"
})
.build();
for entry in walker {
let entry: ignore::DirEntry = entry.context("walking source directory")?;
let entry_path = entry.path();
if entry_path.is_file() {
let relative = entry_path.strip_prefix(source_dir).unwrap_or(entry_path);
archive
.append_path_with_name(entry_path, relative)
.with_context(|| format!("adding {}", entry_path.display()))?;
}
}
archive
.into_inner()
.context("finishing gzip stream")?
.finish()
.context("finalizing gzip")?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use std::fs;
use super::*;
#[test]
fn build_archive_emits_nonempty_gzip_for_simple_tree() {
let tmp = tempfile::tempdir().unwrap();
fs::write(tmp.path().join("hello.txt"), b"hi").unwrap();
let bytes = build_archive_bytes(tmp.path()).unwrap();
assert!(bytes.len() > 2);
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
}
#[test]
fn build_archive_skips_dot_git() {
use flate2::read::GzDecoder;
use std::io::Read;
let tmp = tempfile::tempdir().unwrap();
fs::create_dir(tmp.path().join(".git")).unwrap();
fs::write(tmp.path().join(".git/HEAD"), b"ref: refs/heads/main").unwrap();
fs::write(tmp.path().join("kept.txt"), b"k").unwrap();
let bytes = build_archive_bytes(tmp.path()).unwrap();
let mut gz = GzDecoder::new(&bytes[..]);
let mut tar_bytes = Vec::new();
gz.read_to_end(&mut tar_bytes).unwrap();
let mut ar = tar::Archive::new(&tar_bytes[..]);
let names: Vec<String> = ar
.entries()
.unwrap()
.map(|e| e.unwrap().path().unwrap().display().to_string())
.collect();
assert!(names.iter().any(|n| n == "kept.txt"), "got: {names:?}");
assert!(
!names.iter().any(|n| n.starts_with(".git")),
"got: {names:?}"
);
}
}