use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use zesven::{
Archive, ArchivePath, ExtractOptions, MemoryLimit, TestOptions, WriteOptions, Writer,
read::{OverwritePolicy, PreserveMetadata, Threads},
};
use crate::exit_codes::{ExitCode, error_to_exit_code};
use crate::file_selector::FileSelector;
use crate::output::create_formatter;
use crate::password::{get_or_prompt_password, get_password};
use crate::progress::{CliProgress, SimpleProgress, WriteProgress};
use crate::{CompressionMethod, OutputFormat, OverwriteMode};
pub struct ExtractConfig<'a> {
pub archive_path: &'a Path,
pub output_dir: &'a Path,
pub include: &'a [String],
pub exclude: &'a [String],
pub overwrite: OverwriteMode,
pub password: Option<String>,
pub preserve_metadata: bool,
pub format: OutputFormat,
pub quiet: bool,
pub thread_count: usize,
}
pub struct CreateConfig<'a> {
pub archive_path: &'a Path,
pub files: &'a [PathBuf],
pub method: CompressionMethod,
pub level: u8,
pub solid: bool,
pub password: Option<String>,
pub encrypt_headers: bool,
pub deterministic: bool,
pub exclude: &'a [String],
pub recursive: bool,
pub format: OutputFormat,
pub quiet: bool,
pub thread_count: usize,
pub memory_limit: Option<u64>,
}
pub fn extract(config: &ExtractConfig<'_>) -> ExitCode {
let formatter = create_formatter(config.format);
let archive = match open_archive(config.archive_path, config.password.clone()) {
Ok(a) => a,
Err(code) => return code,
};
let info = archive.info();
if info.has_encrypted_entries || info.has_encrypted_header {
let _pwd = get_password(config.password.clone(), true);
}
let selector = match FileSelector::new(config.include, config.exclude) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: {}", e);
return ExitCode::BadArgs;
}
};
let overwrite_policy = match config.overwrite {
OverwriteMode::Always => OverwritePolicy::Overwrite,
OverwriteMode::Never => OverwritePolicy::Skip,
OverwriteMode::Prompt => {
OverwritePolicy::Skip
}
};
let mut prompt_all_yes = false;
let mut prompt_all_no = false;
let threads = match config.thread_count {
0 => Threads::Auto,
n => Threads::count_or_single(n),
};
let metadata = if config.preserve_metadata {
PreserveMetadata::all()
} else {
PreserveMetadata::none()
};
let options = ExtractOptions::new()
.overwrite(overwrite_policy)
.threads(threads)
.preserve_metadata(metadata);
if let Err(e) = std::fs::create_dir_all(config.output_dir) {
eprintln!("Error creating output directory: {}", e);
return ExitCode::IoError;
}
let progress = CliProgress::new(info.entry_count as u64, config.quiet);
if !config.quiet {
progress.set_message("Extracting...");
}
let mut archive = archive;
let result = if matches!(config.overwrite, OverwriteMode::Prompt) {
extract_with_prompts(
&mut archive,
config.output_dir,
&selector,
&options,
&mut prompt_all_yes,
&mut prompt_all_no,
&progress,
)
} else {
archive.extract(config.output_dir, &selector, &options)
};
let result = match result {
Ok(r) => r,
Err(e) => {
progress.finish_with_message("Failed");
eprintln!("Error: {}", e);
return error_to_exit_code(&e);
}
};
progress.finish();
print!("{}", formatter.format_extract_result(&result));
if result.is_ok() {
ExitCode::Success
} else {
ExitCode::Warning
}
}
pub fn create(config: &CreateConfig<'_>) -> ExitCode {
let destination = match resolve_destination(config.archive_path) {
Ok(destination) => destination,
Err(e) => {
eprintln!("Error: {}: {}", config.archive_path.display(), e);
return ExitCode::IoError;
}
};
let files = match collect_files(config, &destination) {
Ok(files) => files,
Err(code) => return code,
};
let mut scratch = match ScratchArchive::create(&destination) {
Ok(scratch) => scratch,
Err(e) => {
eprintln!(
"Error: could not start writing beside {}: {}",
destination.display(),
e
);
return ExitCode::IoError;
}
};
let code = create_inner(config, scratch.take_file(), &files);
if !matches!(code, ExitCode::Success) {
return code;
}
match scratch.commit(&destination) {
Ok(()) => code,
Err(e) => {
eprintln!(
"Error: could not move the finished archive to {}: {}",
destination.display(),
e,
);
ExitCode::IoError
}
}
}
fn resolve_destination(path: &Path) -> std::io::Result<PathBuf> {
let mut current = path.to_path_buf();
for _ in 0..40 {
let metadata = match std::fs::symlink_metadata(¤t) {
Ok(metadata) => metadata,
Err(_) => break,
};
if !metadata.file_type().is_symlink() {
break;
}
let target = match std::fs::read_link(¤t) {
Ok(target) => target,
Err(_) => break,
};
current = if target.is_absolute() {
target
} else {
match current.parent() {
Some(parent) => parent.join(target),
None => target,
}
};
}
if std::fs::symlink_metadata(¤t)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
{
return Err(std::io::Error::other(format!(
"too many levels of symbolic links: {}",
path.display()
)));
}
Ok(current)
}
struct ScratchArchive {
path: PathBuf,
file: Option<std::fs::File>,
committed: bool,
}
impl ScratchArchive {
fn create(destination: &Path) -> std::io::Result<Self> {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
let path = destination.with_extension(format!(
"{}part-{}-{}",
destination
.extension()
.map(|e| format!("{}.", e.to_string_lossy()))
.unwrap_or_default(),
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed),
));
let file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)?;
Ok(Self {
path,
file: Some(file),
committed: false,
})
}
fn take_file(&mut self) -> std::fs::File {
self.file
.take()
.expect("the handle is taken once, by the writer that fills it")
}
fn commit(&mut self, destination: &Path) -> std::io::Result<()> {
self.file.take();
if let Ok(existing) = std::fs::metadata(destination) {
std::fs::set_permissions(&self.path, existing.permissions())?;
}
std::fs::rename(&self.path, destination)?;
self.committed = true;
Ok(())
}
}
impl Drop for ScratchArchive {
fn drop(&mut self) {
self.file.take();
if self.committed {
return;
}
if let Err(e) = std::fs::remove_file(&self.path) {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!("Warning: could not remove {}: {}", self.path.display(), e);
}
}
}
}
fn is_same_file(a: &Path, b: &Path) -> bool {
let resolve = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
resolve(a) == resolve(b)
}
fn collect_files(
config: &CreateConfig<'_>,
destination: &Path,
) -> Result<Vec<(PathBuf, String)>, ExitCode> {
let exclude_selector = match FileSelector::new(&[], config.exclude) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: {}", e);
return Err(ExitCode::BadArgs);
}
};
let mut all_files: Vec<(std::path::PathBuf, String)> = Vec::new();
for path in config.files {
if path.is_dir() {
if config.recursive {
for entry in WalkDir::new(path).follow_links(false) {
let entry = match entry {
Ok(e) => e,
Err(e) => {
eprintln!("Error: {}", e);
return Err(ExitCode::IoError);
}
};
let rel_path = entry
.path()
.strip_prefix(path)
.unwrap_or(entry.path())
.to_string_lossy()
.to_string();
if rel_path.is_empty() {
continue;
}
if !exclude_selector.matches(&rel_path) {
continue;
}
if is_same_file(entry.path(), destination) {
continue;
}
all_files.push((entry.path().to_path_buf(), rel_path));
}
} else {
eprintln!(
"Error: {} is a directory, use -r for recursive",
path.display()
);
return Err(ExitCode::BadArgs);
}
} else if path.is_file() {
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
if exclude_selector.matches(&name) && !is_same_file(path, destination) {
all_files.push((path.clone(), name));
}
} else {
eprintln!("Error: {} does not exist", path.display());
return Err(ExitCode::BadArgs);
}
}
if all_files.is_empty() {
eprintln!("Error: No files to add to archive");
return Err(ExitCode::BadArgs);
}
all_files.sort_by(|(_, a), (_, b)| a.cmp(b));
Ok(all_files)
}
fn create_inner(
config: &CreateConfig<'_>,
scratch_file: std::fs::File,
all_files: &[(PathBuf, String)],
) -> ExitCode {
let _formatter = create_formatter(config.format);
let pwd = if config.password.is_some() {
get_or_prompt_password(config.password.clone(), true)
} else {
None
};
let mut options = match WriteOptions::new()
.method(config.method.into())
.level(config.level as u32)
{
Ok(opts) => opts.deterministic(config.deterministic),
Err(e) => {
eprintln!("Error: {}", e);
return ExitCode::BadArgs;
}
};
options = options.threads(match config.thread_count {
0 => Threads::Auto,
n => Threads::count_or_single(n),
});
if let Some(bytes) = config.memory_limit {
options = options.memory_limit(MemoryLimit::bytes_or_auto(bytes));
}
if config.solid {
options = options.solid();
}
#[cfg(feature = "aes")]
if let Some(ref p) = pwd {
options = options
.password(p.as_str())
.encrypt_header(config.encrypt_headers);
}
let progress = WriteProgress::new(all_files.len() as u64, config.quiet);
let watcher = progress.clone();
let mut writer = match Writer::create(std::io::BufWriter::new(scratch_file)) {
Ok(w) => w.options(options).progress(watcher),
Err(e) => {
eprintln!("Error creating archive: {}", e);
return error_to_exit_code(&e);
}
};
for (disk_path, archive_name) in all_files {
let archive_path = match ArchivePath::new(archive_name) {
Ok(p) => p,
Err(e) => {
progress.finish_with_message("Failed");
eprintln!("Error: Invalid path {}: {}", archive_name, e);
return error_to_exit_code(&e);
}
};
if let Err(e) = writer.add_path(disk_path, archive_path) {
progress.finish_with_message("Failed");
eprintln!("Error: Failed to add {}: {}", disk_path.display(), e);
return error_to_exit_code(&e);
}
}
let result = match writer.finish() {
Ok(r) => r,
Err(e) => {
progress.finish_with_message("Failed");
eprintln!("Error finalizing archive: {}", e);
return error_to_exit_code(&e);
}
};
progress.finish();
if !config.quiet {
println!(
"Created archive with {} files ({} -> {})",
result.entries_written,
crate::output::humanize_bytes(result.total_size),
crate::output::humanize_bytes(result.compressed_size)
);
println!(
"Compression ratio: {:.1}% (saved {:.1}%)",
result.compression_ratio() * 100.0,
result.space_savings() * 100.0
);
}
ExitCode::Success
}
pub fn list(
archive_path: &Path,
technical: bool,
password: Option<String>,
format: OutputFormat,
_quiet: bool,
) -> ExitCode {
let formatter = create_formatter(format);
let archive = match open_archive(archive_path, password) {
Ok(a) => a,
Err(code) => return code,
};
let entries = archive.entries();
print!("{}", formatter.format_list(entries, technical));
ExitCode::Success
}
pub fn test(
archive_path: &Path,
password: Option<String>,
include: &[String],
format: OutputFormat,
quiet: bool,
thread_count: usize,
) -> ExitCode {
let formatter = create_formatter(format);
let mut archive = match open_archive(archive_path, password) {
Ok(a) => a,
Err(code) => return code,
};
let selector = match FileSelector::new(include, &[]) {
Ok(s) => s,
Err(e) => {
eprintln!("Error: {}", e);
return ExitCode::BadArgs;
}
};
let threads = match thread_count {
0 => Threads::Auto,
n => Threads::count_or_single(n),
};
let options = TestOptions::new().threads(threads);
let info = archive.info();
let progress = SimpleProgress::new(info.entry_count as u64, quiet);
if !quiet {
progress.set_message("Testing...");
}
let result = match archive.test(&selector, &options) {
Ok(r) => r,
Err(e) => {
progress.finish_with_message("Failed");
eprintln!("Error: {}", e);
return error_to_exit_code(&e);
}
};
progress.finish();
print!("{}", formatter.format_test_result(&result));
if result.is_ok() {
ExitCode::Success
} else {
ExitCode::BadArchive
}
}
pub fn info(
archive_path: &Path,
password: Option<String>,
format: OutputFormat,
_quiet: bool,
) -> ExitCode {
let formatter = create_formatter(format);
let archive = match open_archive(archive_path, password) {
Ok(a) => a,
Err(code) => return code,
};
let info = archive.info();
print!("{}", formatter.format_info(info));
ExitCode::Success
}
fn open_archive(
path: &Path,
password: Option<String>,
) -> Result<Archive<zesven::read::ArchiveSource>, ExitCode> {
let archive = if let Some(pwd) = password {
#[cfg(feature = "aes")]
{
Archive::open_path_with_password(path, pwd).map_err(|e| {
eprintln!("Error opening archive: {}", e);
error_to_exit_code(&e)
})?
}
#[cfg(not(feature = "aes"))]
{
let _ = pwd;
eprintln!("Error: AES encryption support not enabled");
return Err(ExitCode::FatalError);
}
} else {
Archive::open_path(path).map_err(|e| {
eprintln!("Error opening archive: {}", e);
error_to_exit_code(&e)
})?
};
Ok(archive)
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum OverwriteResponse {
Yes,
No,
YesAll,
NoAll,
}
fn prompt_overwrite(path: &Path) -> OverwriteResponse {
use dialoguer::{Select, theme::ColorfulTheme};
let items = &[
"Yes - overwrite this file",
"No - skip this file",
"Yes to all - overwrite all existing files",
"No to all - skip all existing files",
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(format!("File exists: {}", path.display()))
.items(items)
.default(1) .interact();
match selection {
Ok(0) => OverwriteResponse::Yes,
Ok(1) => OverwriteResponse::No,
Ok(2) => OverwriteResponse::YesAll,
Ok(3) => OverwriteResponse::NoAll,
_ => OverwriteResponse::No, }
}
fn extract_with_prompts<R: std::io::Read + std::io::Seek>(
archive: &mut Archive<R>,
output_dir: &Path,
selector: &crate::file_selector::FileSelector,
_options: &ExtractOptions,
all_yes: &mut bool,
all_no: &mut bool,
progress: &CliProgress,
) -> zesven::Result<zesven::read::ExtractResult> {
use zesven::read::{EntrySelector, ExtractResult};
let mut result = ExtractResult::default();
let entries_to_extract: Vec<usize> = archive
.entries()
.iter()
.enumerate()
.filter(|(_, e)| selector.select(e))
.map(|(idx, _)| idx)
.collect();
for idx in entries_to_extract {
let entry = &archive.entries()[idx];
let entry_path = entry.path.as_str().to_string();
let is_directory = entry.is_directory;
if is_directory {
let dir_path = output_dir.join(&entry_path);
if let Err(e) = std::fs::create_dir_all(&dir_path) {
result.entries_failed += 1;
result.failures.push((entry_path.clone(), e.to_string()));
} else {
result.entries_extracted += 1;
}
progress.inc(1);
continue;
}
let file_path = output_dir.join(&entry_path);
if file_path.exists() {
if *all_no {
result.entries_skipped += 1;
progress.inc(1);
continue;
}
if !*all_yes {
let response = prompt_overwrite(&file_path);
match response {
OverwriteResponse::Yes => {}
OverwriteResponse::No => {
result.entries_skipped += 1;
progress.inc(1);
continue;
}
OverwriteResponse::YesAll => {
*all_yes = true;
}
OverwriteResponse::NoAll => {
*all_no = true;
result.entries_skipped += 1;
progress.inc(1);
continue;
}
}
}
}
if let Some(parent) = file_path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
result.entries_failed += 1;
result.failures.push((entry_path.clone(), e.to_string()));
progress.inc(1);
continue;
}
}
match archive.extract_entry_to_vec_by_index(idx) {
Ok(data) => match std::fs::write(&file_path, &data) {
Ok(()) => {
result.entries_extracted += 1;
result.bytes_extracted += data.len() as u64;
}
Err(e) => {
result.entries_failed += 1;
result.failures.push((entry_path.clone(), e.to_string()));
}
},
Err(e) => {
result.entries_failed += 1;
result.failures.push((entry_path.clone(), e.to_string()));
}
}
progress.inc(1);
}
Ok(result)
}