mod compress;
mod decompress;
mod list;
use bstr::ByteSlice;
use decompress::DecompressOptions;
use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use utils::colors;
use crate::{
CliArgs, INITIAL_CURRENT_DIR, QuestionPolicy, Result,
check::{self, CheckFileSignatureControlFlow},
cli::Subcommand,
commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
error::{Error, FinalError},
extension::{self, parse_format_flag},
info_accessible,
list::ListOptions,
utils::{
self, BytesFmt, FileVisibilityPolicy, NoQuotePathFmt, PathFmt, QuestionAction, canonicalize, colors::*,
file_size, is_path_stdin,
},
};
fn warn_user_about_loading_zip_in_memory() {
const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n \
The format '.zip' is limited by design and cannot be (de)compressed with encoding streams.\n \
When chaining '.zip' with other formats, all (de)compression needs to be done in-memory\n \
Careful, you might run out of RAM if the archive is too large!";
eprintln!("{}[WARNING]{}: {ZIP_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
}
fn warn_user_about_loading_sevenz_in_memory() {
const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n \
The format '.7z' is limited by design and cannot be (de)compressed with encoding streams.\n \
When chaining '.7z' with other formats, all (de)compression needs to be done in-memory\n \
Careful, you might run out of RAM if the archive is too large!";
eprintln!("{}[WARNING]{}: {SEVENZ_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
}
pub fn run(args: CliArgs, question_policy: QuestionPolicy, file_visibility_policy: FileVisibilityPolicy) -> Result<()> {
if let Some(threads) = args.threads {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build_global()
.unwrap();
}
match args.cmd {
Subcommand::Compress {
files,
output: output_path,
level,
fast,
slow,
follow_symlinks,
} => {
if files.is_empty() {
return Err(FinalError::with_title("No files to compress").into());
}
let (formats_from_flag, formats) = match args.format {
Some(formats) => {
let parsed_formats = parse_format_flag(&formats)?;
(Some(formats), parsed_formats)
}
None => (None, extension::extensions_from_path(&output_path)?),
};
check::check_invalid_compression_with_non_archive_format(
&formats,
&output_path,
&files,
formats_from_flag.as_deref(),
)?;
check::check_archive_formats_position(&formats, &output_path)?;
let (output_file, output_path) = match utils::create_file_or_prompt_on_conflict(
&output_path,
question_policy,
QuestionAction::Compression,
)? {
Some(writer) => writer,
None => return Ok(()),
};
let level = if fast {
Some(1) } else if slow {
Some(i16::MAX) } else {
level
};
let compress_result = compress_files(
files,
formats,
output_file,
&output_path,
follow_symlinks,
question_policy,
file_visibility_policy,
level,
);
if let Ok(true) = compress_result {
info_accessible!("Output file size: {}", BytesFmt(file_size(&output_path)?));
info_accessible!("Successfully compressed to {}", PathFmt(&output_path));
} else {
if utils::remove_file_or_dir(&output_path).is_err() {
eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
eprintln!(" Ouch failed to delete the file {}", PathFmt(&output_path));
eprintln!(" Please delete it manually.");
eprintln!(" This file is corrupted if compression didn't finished.");
if compress_result.is_err() {
eprintln!(" Compression failed for reasons below.");
}
}
}
compress_result.map(|_| ())
}
Subcommand::Decompress {
files,
output_dir,
remove,
} => {
let mut files_output_paths: Vec<_> = vec![];
let mut files_extensions: Vec<Vec<_>> = vec![];
if let Some(format) = args.format {
let format = parse_format_flag(&format)?;
for path in files.iter() {
let file_name = path.file_name().ok_or_else(|| Error::Custom {
reason: FinalError::with_title(format!("{} does not have a file name", PathFmt(path))),
})?;
files_output_paths.push(file_name.into());
files_extensions.push(format.clone());
}
} else {
for path in files.iter() {
let (output_path, mut extensions) = extension::separate_known_extensions_from_name(path)?;
let mut output_path = output_path.to_owned();
match check::check_file_signature(path, &extensions, question_policy)? {
CheckFileSignatureControlFlow::HaltProgram => return Ok(()),
CheckFileSignatureControlFlow::Continue => {}
CheckFileSignatureControlFlow::ChangeToDetectedExtension {
new_extension,
new_path_filename,
} => {
extensions = vec![new_extension];
output_path = output_path.with_file_name(new_path_filename);
}
}
files_output_paths.push(output_path);
files_extensions.push(extensions);
}
}
check::check_missing_formats_when_decompressing(&files, &files_extensions)?;
let output_dir = if let Some(dir) = output_dir {
utils::create_dir_if_non_existent(&dir)?;
canonicalize(&dir)?
} else {
INITIAL_CURRENT_DIR.clone()
};
files
.par_iter()
.zip(files_extensions)
.zip(files_output_paths)
.try_for_each(|((input_path, formats), file_name)| {
let output_file_path = if is_path_stdin(&file_name) {
output_dir.join("ouch-output")
} else {
output_dir.join(file_name)
};
decompress_file(DecompressOptions {
input_file_path: input_path,
formats,
output_dir: &output_dir,
output_file_path,
question_policy,
password: args.password.as_deref().map(|str| {
<[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
}),
remove,
})
.map_err(|err| match err {
Error::IoError { reason } => Error::Custom {
reason: FinalError::with_title(format!(
"Failed to decompress {}",
NoQuotePathFmt(input_path)
))
.detail(reason),
},
other => other,
})
})
}
Subcommand::List { archives: files, tree } => {
let mut formats = vec![];
if let Some(format) = args.format {
let format = parse_format_flag(&format)?;
for _ in 0..files.len() {
formats.push(format.clone());
}
} else {
for path in files.iter() {
let mut extensions = extension::extensions_from_path(path)?;
match check::check_file_signature(path, &extensions, question_policy)? {
CheckFileSignatureControlFlow::HaltProgram => return Ok(()),
CheckFileSignatureControlFlow::Continue => {}
CheckFileSignatureControlFlow::ChangeToDetectedExtension { new_extension, .. } => {
extensions = vec![new_extension]
}
}
formats.push(extensions);
}
}
check::check_for_non_archive_formats(&files, &formats)?;
let list_options = ListOptions { tree };
for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
if i > 0 {
println!();
}
let formats = extension::flatten_compression_formats(&formats);
list_archive_contents(
archive_path,
formats,
list_options,
question_policy,
args.password
.as_deref()
.map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")),
)?;
}
Ok(())
}
}
}