use std::{
ffi::OsString,
fs::{self, File, OpenOptions},
io::{BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
time::Instant,
};
use zarust::ArchiveWriter;
use crate::cli::{
Failure, Outcome, READ_BUFFER,
args::Options,
path,
ui::{Reporter, format_bytes, format_duration},
walk::{Summary, ratio},
};
pub fn run(
reporter: &mut Reporter,
input: &Path,
output: Option<PathBuf>,
options: &Options,
) -> Outcome {
if !input.is_dir() {
return Err(Failure::new(format!(
"`{}` is not a directory",
path::display(input)
)));
}
let output = output.unwrap_or_else(|| default_output(input));
prepare_output(&output, options.force)?;
let started = Instant::now();
let plan = Plan::of(input, &output)?;
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&output)
.map_err(|error| Failure::path("create", &output, error))?;
let mut writer = ArchiveWriter::new(BufWriter::new(file));
reporter.start_bar("packing", plan.summary.bytes);
let result = write_entries(reporter, &mut writer, input, &plan.entries)
.and_then(|()| writer.finish().map_err(Failure::from))
.and_then(|mut inner| inner.flush().map_err(Failure::from));
reporter.finish_bar();
if let Err(failure) = result {
let _ = fs::remove_file(&output);
return Err(failure);
}
let packed = fs::metadata(&output).map(|meta| meta.len()).unwrap_or(0);
reporter.success(
"packed",
&output,
&format!(
"{} → {} ({:.0}%) in {}",
plan.summary.describe(),
format_bytes(packed as f64),
ratio(packed, plan.summary.bytes),
format_duration(Some(started.elapsed())),
),
)
}
fn default_output(input: &Path) -> PathBuf {
let mut name = input
.canonicalize()
.ok()
.as_deref()
.and_then(Path::file_name)
.unwrap_or_else(|| input.file_name().unwrap_or_default())
.to_os_string();
name.push(".zar");
path::sibling(input, name)
}
fn prepare_output(output: &Path, force: bool) -> Result<(), Failure> {
if output.is_dir() {
return Err(Failure::new(format!(
"`{}` is a directory, not an archive path",
path::display(output)
)));
}
if output.exists() {
if !force {
return Err(Failure::new(format!(
"`{}` already exists (use --force to overwrite)",
path::display(output)
)));
}
fs::remove_file(output).map_err(|error| Failure::path("replace", output, error))?;
}
if let Some(parent) = output
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent).map_err(|error| Failure::path("create", parent, error))?;
}
Ok(())
}
struct Plan {
entries: Vec<Entry>,
summary: Summary,
}
struct Entry {
relative: PathBuf,
is_directory: bool,
}
impl Plan {
fn of(root: &Path, output: &Path) -> Result<Self, Failure> {
let mut plan = Self {
entries: Vec::new(),
summary: Summary::default(),
};
plan.walk(root, root, &OutputFilter::new(output))?;
Ok(plan)
}
fn walk(
&mut self,
root: &Path,
directory: &Path,
output: &OutputFilter,
) -> Result<(), Failure> {
let listing =
fs::read_dir(directory).map_err(|error| Failure::path("read", directory, error))?;
for entry in listing {
let entry = entry.map_err(|error| Failure::path("read", directory, error))?;
let source = entry.path();
if output.matches(&source) {
continue;
}
let kind = entry
.file_type()
.map_err(|error| Failure::path("stat", &source, error))?;
let relative = source.strip_prefix(root).unwrap_or(&source).to_path_buf();
if kind.is_dir() {
self.summary.directory();
self.entries.push(Entry {
relative,
is_directory: true,
});
self.walk(root, &source, output)?;
} else if kind.is_file() {
let size = entry
.metadata()
.map_err(|error| Failure::path("stat", &source, error))?
.len();
if self.summary.bytes.checked_add(size).is_none() {
return Err(Failure::new("input exceeds the supported archive size"));
}
self.summary.file(size);
self.entries.push(Entry {
relative,
is_directory: false,
});
}
}
Ok(())
}
}
struct OutputFilter {
name: OsString,
canonical: PathBuf,
}
impl OutputFilter {
fn new(output: &Path) -> Self {
let name = output.file_name().unwrap_or_default().to_os_string();
let parent = output.parent().unwrap_or_else(|| Path::new("."));
let canonical = fs::canonicalize(parent)
.unwrap_or_else(|_| parent.to_path_buf())
.join(&name);
Self { name, canonical }
}
fn matches(&self, source: &Path) -> bool {
source.file_name() == Some(self.name.as_os_str())
&& fs::canonicalize(source).is_ok_and(|resolved| resolved == self.canonical)
}
}
fn write_entries<W: Write>(
reporter: &mut Reporter,
writer: &mut ArchiveWriter<W>,
root: &Path,
entries: &[Entry],
) -> Result<(), Failure> {
let mut buffer = vec![0_u8; READ_BUFFER];
for entry in entries {
let archive_path = path::to_archive(&entry.relative)?;
if entry.is_directory {
writer
.make_dir(&archive_path, false)
.map_err(|error| Failure::path("add", &entry.relative, error))?;
continue;
}
if reporter.is_verbose() {
reporter.detail(format_args!("{}", path::display(&entry.relative)))?;
}
let source = root.join(&entry.relative);
let file = File::open(&source).map_err(|error| Failure::path("open", &source, error))?;
copy_into(reporter, writer, &archive_path, file, &mut buffer)
.map_err(|error| Failure::path("add", &entry.relative, error))?;
}
Ok(())
}
fn copy_into<W: Write>(
reporter: &mut Reporter,
writer: &mut ArchiveWriter<W>,
archive_path: &[u8],
source: File,
buffer: &mut [u8],
) -> zarust::Result<()> {
writer.start_file(archive_path)?;
let mut source = BufReader::with_capacity(READ_BUFFER, source);
loop {
let read = source.read(buffer)?;
if read == 0 {
return Ok(());
}
writer.append_data(&buffer[..read])?;
reporter.advance(read as u64);
}
}