1 //! Receive command from the cli and call the respective function for that command.
7 use std::{ops::ControlFlow, path::PathBuf};
10 use decompress::DecompressOptions;
11 use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
17 commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
18 error::{Error, FinalError},
19 extension::{self, parse_format_flag},
22 self, colors::*, is_path_stdin, logger::info_accessible, path_to_str, EscapedPathDisplay, FileVisibilityPolicy,
24 CliArgs, QuestionPolicy,
27 /// Warn the user that (de)compressing this .zip archive might freeze their system.
28 fn warn_user_about_loading_zip_in_memory() {
29 const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n \
30 The format '.zip' is limited by design and cannot be (de)compressed with encoding streams.\n \
31 When chaining '.zip' with other formats, all (de)compression needs to be done in-memory\n \
32 Careful, you might run out of RAM if the archive is too large!";
34 eprintln!("{}[WARNING]{}: {ZIP_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
37 /// Warn the user that (de)compressing this .7z archive might freeze their system.
38 fn warn_user_about_loading_sevenz_in_memory() {
39 const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n \
40 The format '.7z' is limited by design and cannot be (de)compressed with encoding streams.\n \
41 When chaining '.7z' with other formats, all (de)compression needs to be done in-memory\n \
42 Careful, you might run out of RAM if the archive is too large!";
44 eprintln!("{}[WARNING]{}: {SEVENZ_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
47 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
48 /// to assume everything is OK.
50 /// There are a lot of custom errors to give enough error description and explanation.
53 question_policy: QuestionPolicy,
54 file_visibility_policy: FileVisibilityPolicy,
55 ) -> crate::Result<()> {
57 if let Some(threads) = args.threads {
58 rayon::ThreadPoolBuilder::new()
65 Subcommand::Compress {
72 // After cleaning, if there are no input files left, exit
74 return Err(FinalError::with_title("No files to compress").into());
77 // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
78 let (formats_from_flag, formats) = match args.format {
80 let parsed_formats = parse_format_flag(&formats)?;
81 (Some(formats), parsed_formats)
83 None => (None, extension::extensions_from_path(&output_path)),
86 check::check_invalid_compression_with_non_archive_format(
90 formats_from_flag.as_ref(),
92 check::check_archive_formats_position(&formats, &output_path)?;
94 let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
95 Some(writer) => writer,
96 None => return Ok(()),
100 Some(1) // Lowest level of compression
102 Some(i16::MAX) // Highest level of compression
107 let compress_result = compress_files(
114 file_visibility_policy,
118 if let Ok(true) = compress_result {
119 // this is only printed once, so it doesn't result in much text. On the other hand,
120 // having a final status message is important especially in an accessibility context
121 // as screen readers may not read a commands exit code, making it hard to reason
122 // about whether the command succeeded without such a message
123 info_accessible(format!("Successfully compressed '{}'", path_to_str(&output_path)));
125 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
127 // if deleting fails, print an extra alert message pointing
128 // out that we left a possibly CORRUPTED file at `output_path`
129 if utils::remove_file_or_dir(&output_path).is_err() {
130 eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
132 " Ouch failed to delete the file '{}'.",
133 EscapedPathDisplay::new(&output_path)
135 eprintln!(" Please delete it manually.");
136 eprintln!(" This file is corrupted if compression didn't finished.");
138 if compress_result.is_err() {
139 eprintln!(" Compression failed for reasons below.");
144 compress_result.map(|_| ())
146 Subcommand::Decompress {
151 let mut output_paths = vec![];
152 let mut formats = vec![];
154 if let Some(format) = args.format {
155 let format = parse_format_flag(&format)?;
156 for path in files.iter() {
157 let file_name = path.file_name().ok_or_else(|| Error::NotFound {
158 error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
160 output_paths.push(file_name.as_ref());
161 formats.push(format.clone());
164 for path in files.iter() {
165 let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
167 if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
171 output_paths.push(pathbase);
172 formats.push(file_formats);
176 check::check_missing_formats_when_decompressing(&files, &formats)?;
178 // The directory that will contain the output files
179 // We default to the current directory if the user didn't specify an output directory with --dir
180 let output_dir = if let Some(dir) = output_dir {
181 utils::create_dir_if_non_existent(&dir)?;
191 .try_for_each(|((input_path, formats), file_name)| {
192 // Path used by single file format archives
193 let output_file_path = if is_path_stdin(file_name) {
194 output_dir.join("stdin-output")
196 output_dir.join(file_name)
198 decompress_file(DecompressOptions {
199 input_file_path: input_path,
201 output_dir: &output_dir,
205 password: args.password.as_deref().map(|str| {
206 <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
212 Subcommand::List { archives: files, tree } => {
213 let mut formats = vec![];
215 if let Some(format) = args.format {
216 let format = parse_format_flag(&format)?;
217 for _ in 0..files.len() {
218 formats.push(format.clone());
221 for path in files.iter() {
222 let mut file_formats = extension::extensions_from_path(path);
224 if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
228 formats.push(file_formats);
232 // Ensure we were not told to list the content of a non-archive compressed file
233 check::check_for_non_archive_formats(&files, &formats)?;
235 let list_options = ListOptions { tree };
237 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
241 let formats = extension::flatten_compression_formats(&formats);
242 list_archive_contents(
249 .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")),