use std::{
ffi::OsString,
io::{Read, Seek},
path::Path,
time::Instant,
};
use zarust::{ArchiveReader, EntryKind, NodeHandle, ROOT_NODE};
use crate::cli::{
Failure, Outcome,
args::Options,
path,
ui::{Palette, Reporter, format_bytes, format_duration},
walk::{Summary, ratio},
};
pub fn list(
reporter: &mut Reporter,
archive: &Path,
subpath: Option<OsString>,
options: &Options,
) -> Outcome {
let reader = super::open_archive(archive)?;
let (root, prefix) = match &subpath {
Some(subpath) => {
let text = subpath.to_string_lossy();
let node = reader.lookup(text.as_bytes()).ok_or_else(|| {
Failure::new(format!("`{text}` is not in `{}`", path::display(archive)))
})?;
(node, text.trim_matches('/').to_owned())
}
None => (ROOT_NODE, String::new()),
};
if reader.entry_kind(root) == Some(EntryKind::File) {
let size = reader.file_size(root)?;
return reporter.line(format_args!("{:>10} {prefix}", format_bytes(size as f64)));
}
let mut summary = Summary::default();
if options.tree {
let Palette { bold, reset, .. } = reporter.palette();
let root_name = if prefix.is_empty() { "." } else { &prefix };
reporter.line(format_args!("{bold}{root_name}{reset}"))?;
print_tree(reporter, &reader, root, &mut String::new(), &mut summary)?;
} else {
let mut prefix = prefix;
print_flat(reporter, &reader, root, &mut prefix, &mut summary)?;
}
let Palette { dim, reset, .. } = reporter.palette();
reporter.info(format_args!("{dim}{}{reset}", summary.describe()))
}
fn print_flat<R: Read + Seek>(
reporter: &mut Reporter,
reader: &ArchiveReader<R>,
directory: NodeHandle,
prefix: &mut String,
summary: &mut Summary,
) -> Outcome {
let palette = reporter.palette();
for entry in reader.directory_entries(directory)? {
let restore = prefix.len();
if !prefix.is_empty() {
prefix.push('/');
}
prefix.push_str(&String::from_utf8_lossy(entry.name));
match entry.kind {
EntryKind::Directory => {
summary.directory();
reporter.line(format_args!(
"{d}{:>10}{r} {c}{prefix}/{r}",
"-",
d = palette.dim,
c = palette.cyan,
r = palette.reset
))?;
print_flat(reporter, reader, entry.handle, prefix, summary)?;
}
EntryKind::File => {
summary.file(entry.size);
reporter.line(format_args!(
"{y}{:>10}{r} {prefix}",
format_bytes(entry.size as f64),
y = palette.yellow,
r = palette.reset
))?;
}
}
prefix.truncate(restore);
}
Ok(())
}
fn print_tree<R: Read + Seek>(
reporter: &mut Reporter,
reader: &ArchiveReader<R>,
directory: NodeHandle,
indent: &mut String,
summary: &mut Summary,
) -> Outcome {
let palette = reporter.palette();
let entries = reader.directory_entries(directory)?;
let last_index = entries.len().saturating_sub(1);
for (index, entry) in entries.iter().enumerate() {
let last = index == last_index;
let branch = if last { "└── " } else { "├── " };
let name = String::from_utf8_lossy(entry.name);
match entry.kind {
EntryKind::Directory => {
summary.directory();
reporter.line(format_args!(
"{d}{indent}{branch}{r}{c}{b}{name}/{r}",
d = palette.dim,
c = palette.cyan,
b = palette.bold,
r = palette.reset
))?;
let restore = indent.len();
indent.push_str(if last { " " } else { "│ " });
print_tree(reporter, reader, entry.handle, indent, summary)?;
indent.truncate(restore);
}
EntryKind::File => {
summary.file(entry.size);
reporter.line(format_args!(
"{d}{indent}{branch}{r}{name} {y}{}{r}",
format_bytes(entry.size as f64),
d = palette.dim,
y = palette.yellow,
r = palette.reset
))?;
}
}
}
Ok(())
}
pub fn info(reporter: &mut Reporter, archive: &Path) -> Outcome {
let reader = super::open_archive(archive)?;
let summary = Summary::of(&reader, ROOT_NODE)?;
let compressed = reader.compressed_data_size();
let p = reporter.palette();
reporter.line(format_args!(
"{c}{b}{}{r}",
path::display(archive),
c = p.cyan,
b = p.bold,
r = p.reset
))?;
let number = |text: String| format!("{}{text}{}", p.yellow, p.reset);
let rows = [
("files", number(summary.files.to_string())),
("directories", number(summary.directories.to_string())),
("contents", number(format_bytes(summary.bytes as f64))),
(
"compressed",
format!(
"{} {d}({:.1}% of contents){r}",
number(format_bytes(compressed as f64)),
ratio(compressed, summary.bytes),
d = p.dim,
r = p.reset
),
),
(
"archive",
number(format_bytes(reader.archive_size() as f64)),
),
];
for (name, value) in rows {
reporter.line(format_args!(
" {d}{name:<13}{r}{value}",
d = p.dim,
r = p.reset
))?;
}
Ok(())
}
pub fn verify(reporter: &mut Reporter, archive: &Path) -> Outcome {
let mut reader = super::open_archive(archive)?;
let started = Instant::now();
let size = reader.archive_size();
reporter.start_bar("verifying", size);
let intact = reader.verify_integrity_with(|read| reporter.advance(read));
reporter.finish_bar();
if !intact.map_err(|error| Failure::path("read", archive, error))? {
return Err(Failure::new(format!(
"`{}` does not match its integrity hash",
path::display(archive)
)));
}
reporter.success(
"ok",
archive,
&format!(
"{} hashed in {}",
format_bytes(size as f64),
format_duration(Some(started.elapsed()))
),
)
}