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,
args::Options,
ui::{Reporter, counted, display_path, format_bytes, format_duration},
};
const READ_BUFFER: usize = 256 * 1024;
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",
display_path(input)
)));
}
let output = match output {
Some(path) => path,
None => default_output(input),
};
prepare_output(&output, options.force)?;
let started = Instant::now();
let plan = plan(input, &output)?;
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&output)
.map_err(|error| {
Failure::new(format!(
"cannot create `{}`: {error}",
display_path(&output)
))
})?;
let mut writer = ArchiveWriter::new(BufWriter::new(file));
reporter.start_bar("packing", plan.bytes);
if let Err(failure) = write_entries(reporter, &mut writer, input, &plan.entries) {
reporter.finish_bar();
let _ = fs::remove_file(&output);
return Err(failure);
}
let result = writer
.finish()
.and_then(|mut inner| inner.flush().map_err(Into::into));
reporter.finish_bar();
if let Err(error) = result {
let _ = fs::remove_file(&output);
return Err(Failure::new(format!("cannot finalize archive: {error}")));
}
let packed = fs::metadata(&output).map(|meta| meta.len()).unwrap_or(0);
summarize(reporter, &output, &plan, packed, started)
}
fn summarize(
reporter: &mut Reporter,
output: &Path,
plan: &Plan,
packed: u64,
started: Instant,
) -> Outcome {
let palette = reporter.palette();
reporter.status(
palette.green,
"packed",
format_args!(
"{bold}{}{reset}",
display_path(output),
bold = palette.bold,
reset = palette.reset
),
)?;
let ratio = if plan.bytes == 0 {
100.0
} else {
packed as f64 * 100.0 / plan.bytes as f64
};
reporter.sub(format_args!(
"{}, {} · {} → {} ({ratio:.0}%) in {}",
counted(plan.files, "file", "files"),
counted(plan.directories, "directory", "directories"),
format_bytes(plan.bytes as f64),
format_bytes(packed as f64),
format_duration(Some(started.elapsed())),
))?;
Ok(())
}
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");
input.parent().unwrap_or_else(|| Path::new("")).join(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",
display_path(output)
)));
}
if output.exists() {
if !force {
return Err(Failure::new(format!(
"`{}` already exists (use --force to overwrite)",
display_path(output)
)));
}
fs::remove_file(output).map_err(|error| {
Failure::new(format!(
"cannot replace `{}`: {error}",
display_path(output)
))
})?;
}
if let Some(parent) = output
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent).map_err(|error| {
Failure::new(format!("cannot create `{}`: {error}", display_path(parent)))
})?;
}
Ok(())
}
struct Plan {
entries: Vec<Entry>,
files: u64,
directories: u64,
bytes: u64,
}
struct Entry {
source: PathBuf,
archive_path: Vec<u8>,
is_directory: bool,
}
struct OutputFilter {
name: OsString,
canonical: PathBuf,
}
impl OutputFilter {
fn new(output: &Path) -> Self {
Self {
name: output.file_name().unwrap_or_default().to_os_string(),
canonical: fs::canonicalize(output).unwrap_or_else(|_| output.to_path_buf()),
}
}
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 plan(root: &Path, output: &Path) -> Result<Plan, Failure> {
let mut plan = Plan {
entries: Vec::new(),
files: 0,
directories: 0,
bytes: 0,
};
walk(root, root, &OutputFilter::new(output), &mut plan)?;
Ok(plan)
}
fn walk(
root: &Path,
directory: &Path,
output: &OutputFilter,
plan: &mut Plan,
) -> Result<(), Failure> {
let listing = fs::read_dir(directory).map_err(|error| {
Failure::new(format!(
"cannot read `{}`: {error}",
display_path(directory)
))
})?;
for entry in listing {
let entry = entry.map_err(|error| {
Failure::new(format!(
"cannot read `{}`: {error}",
display_path(directory)
))
})?;
let source = entry.path();
if output.matches(&source) {
continue;
}
let kind = entry.file_type().map_err(|error| {
Failure::new(format!("cannot stat `{}`: {error}", display_path(&source)))
})?;
let relative = source.strip_prefix(root).unwrap_or(&source);
let archive_path = archive_path(relative)?;
if kind.is_dir() {
plan.directories += 1;
plan.entries.push(Entry {
source: source.clone(),
archive_path,
is_directory: true,
});
walk(root, &source, output, plan)?;
} else if kind.is_file() {
let size = entry
.metadata()
.map_err(|error| {
Failure::new(format!("cannot stat `{}`: {error}", display_path(&source)))
})?
.len();
plan.files += 1;
plan.bytes = plan
.bytes
.checked_add(size)
.ok_or_else(|| Failure::new("input exceeds the supported archive size"))?;
plan.entries.push(Entry {
source,
archive_path,
is_directory: false,
});
}
}
Ok(())
}
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 shown = entry.source.strip_prefix(root).unwrap_or(&entry.source);
if entry.is_directory {
writer
.make_dir(&entry.archive_path, false)
.map_err(|error| {
Failure::new(format!("cannot add `{}`: {error}", display_path(shown)))
})?;
continue;
}
reporter.detail(format_args!("{}", display_path(shown)))?;
let source = File::open(&entry.source).map_err(|error| {
Failure::new(format!(
"cannot open `{}`: {error}",
display_path(&entry.source)
))
})?;
copy_into(reporter, writer, &entry.archive_path, source, &mut buffer).map_err(|error| {
Failure::new(format!("cannot add `{}`: {error}", display_path(shown)))
})?;
}
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);
}
}
fn archive_path(path: &Path) -> Result<Vec<u8>, Failure> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(path.as_os_str().as_bytes().to_vec())
}
#[cfg(not(unix))]
{
let text = path.to_str().ok_or_else(|| {
Failure::new(format!("`{}` is not valid Unicode", display_path(path)))
})?;
Ok(text.replace('\\', "/").into_bytes())
}
}