//! Local archives of downloaded remote selections (0.1.0 used `archiver`: zip level 9, //! tar, tar.gz level 9). The archive contains the downloaded tree without a root prefix. use crate::core::error::{CoreError, CoreResult}; use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; fn failed(error: impl std::fmt::Display) -> CoreError { CoreError::plain(format!("Archive creation failed: {error}")) } fn create_private(destination: &Path) -> CoreResult { let mut options = std::fs::OpenOptions::new(); options.write(true).create(true).truncate(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600); } options.open(destination).map_err(failed) } /// Sorted recursive walk (directories before their content), no symlinks. fn walk(root: &Path, relative: &Path, out: &mut Vec<(std::path::PathBuf, bool)>) -> CoreResult<()> { let mut entries: Vec<_> = std::fs::read_dir(root.join(relative)).map_err(failed)?.filter_map(Result::ok).collect(); entries.sort_by_key(|e| e.file_name()); for entry in entries { let kind = entry.file_type().map_err(failed)?; let rel = relative.join(entry.file_name()); if kind.is_dir() { out.push((rel.clone(), true)); walk(root, &rel, out)?; } else if kind.is_file() { out.push((rel, false)); } } Ok(()) } fn slash(path: &Path) -> String { path.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::>().join("/") } /// Creates the archive and returns its size in bytes. pub fn create(source: &Path, destination: &Path, format: &str) -> CoreResult { let mut entries = Vec::new(); walk(source, Path::new(""), &mut entries)?; let file = create_private(destination)?; match format { "zip" => { let mut zip = zip::ZipWriter::new(BufWriter::new(file)); let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated).compression_level(Some(9)).large_file(true); for (rel, dir) in &entries { let name = slash(rel); if *dir { zip.add_directory(format!("{name}/"), options).map_err(failed)?; } else { zip.start_file(name, options).map_err(failed)?; std::io::copy(&mut File::open(source.join(rel)).map_err(failed)?, &mut zip).map_err(failed)?; } } let mut inner = zip.finish().map_err(failed)?; inner.flush().map_err(failed)?; } _ => { let writer: Box = if format == "tar.gz" { Box::new(flate2::write::GzEncoder::new(BufWriter::new(file), flate2::Compression::new(9))) } else { Box::new(BufWriter::new(file)) }; let mut tar = tar::Builder::new(writer); tar.follow_symlinks(false); for (rel, dir) in &entries { if *dir { tar.append_dir(rel, source.join(rel)).map_err(failed)?; } else { tar.append_path_with_name(source.join(rel), rel).map_err(failed)?; } } let mut inner = tar.into_inner().map_err(failed)?; inner.flush().map_err(failed)?; drop(inner); } } Ok(std::fs::metadata(destination).map_err(failed)?.len()) } #[cfg(test)] mod tests { use super::*; #[test] fn creates_all_formats() { let dir = tempfile::tempdir().unwrap(); let src = dir.path().join("src"); std::fs::create_dir_all(src.join("a/b")).unwrap(); std::fs::write(src.join("a/b/f.txt"), "hello").unwrap(); std::fs::write(src.join("top.txt"), "x").unwrap(); for format in ["zip", "tar", "tar.gz"] { let out = dir.path().join(format!("out.{format}")); assert!(create(&src, &out, format).unwrap() > 0); } let mut zip = zip::ZipArchive::new(File::open(dir.path().join("out.zip")).unwrap()).unwrap(); let mut text = String::new(); std::io::Read::read_to_string(&mut zip.by_name("a/b/f.txt").unwrap(), &mut text).unwrap(); assert_eq!(text, "hello"); let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(File::open(dir.path().join("out.tar.gz")).unwrap())); let names: Vec = archive.entries().unwrap().map(|e| e.unwrap().path().unwrap().to_string_lossy().to_string()).collect(); assert!(names.contains(&"a/b/f.txt".to_string()) && names.contains(&"top.txt".to_string())); } }