refactor(cli): move thread pool setup to command execution, use thread::spawn instead...
[ouch.git] / src / commands / mod.rs
blob9baf6662fdeecb3f87402aeb152713a9a973b817
1 //! Receive command from the cli and call the respective function for that command.
3 mod compress;
4 mod decompress;
5 mod list;
7 use std::{ops::ControlFlow, path::PathBuf};
9 use bstr::ByteSlice;
10 use decompress::DecompressOptions;
11 use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
12 use utils::colors;
14 use crate::{
15     check,
16     cli::Subcommand,
17     commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
18     error::{Error, FinalError},
19     extension::{self, parse_format_flag},
20     list::ListOptions,
21     utils::{
22         self, colors::*, is_path_stdin, logger::info_accessible, path_to_str, EscapedPathDisplay, FileVisibilityPolicy,
23     },
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.
49 ///
50 /// There are a lot of custom errors to give enough error description and explanation.
51 pub fn run(
52     args: CliArgs,
53     question_policy: QuestionPolicy,
54     file_visibility_policy: FileVisibilityPolicy,
55 ) -> crate::Result<()> {
57     if let Some(threads) = args.threads {
58         rayon::ThreadPoolBuilder::new()
59             .num_threads(threads)
60             .build_global()
61             .unwrap();
62     }
64     match args.cmd {
65         Subcommand::Compress {
66             files,
67             output: output_path,
68             level,
69             fast,
70             slow,
71         } => {
72             // After cleaning, if there are no input files left, exit
73             if files.is_empty() {
74                 return Err(FinalError::with_title("No files to compress").into());
75             }
77             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
78             let (formats_from_flag, formats) = match args.format {
79                 Some(formats) => {
80                     let parsed_formats = parse_format_flag(&formats)?;
81                     (Some(formats), parsed_formats)
82                 }
83                 None => (None, extension::extensions_from_path(&output_path)),
84             };
86             check::check_invalid_compression_with_non_archive_format(
87                 &formats,
88                 &output_path,
89                 &files,
90                 formats_from_flag.as_ref(),
91             )?;
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(()),
97             };
99             let level = if fast {
100                 Some(1) // Lowest level of compression
101             } else if slow {
102                 Some(i16::MAX) // Highest level of compression
103             } else {
104                 level
105             };
107             let compress_result = compress_files(
108                 files,
109                 formats,
110                 output_file,
111                 &output_path,
112                 args.quiet,
113                 question_policy,
114                 file_visibility_policy,
115                 level,
116             );
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)));
124             } else {
125                 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
126                 //
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);
131                     eprintln!(
132                         "  Ouch failed to delete the file '{}'.",
133                         EscapedPathDisplay::new(&output_path)
134                     );
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.");
140                     }
141                 }
142             }
144             compress_result.map(|_| ())
145         }
146         Subcommand::Decompress {
147             files,
148             output_dir,
149             remove,
150         } => {
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)),
159                     })?;
160                     output_paths.push(file_name.as_ref());
161                     formats.push(format.clone());
162                 }
163             } else {
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)? {
168                         return Ok(());
169                     }
171                     output_paths.push(pathbase);
172                     formats.push(file_formats);
173                 }
174             }
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)?;
182                 dir
183             } else {
184                 PathBuf::from(".")
185             };
187             files
188                 .par_iter()
189                 .zip(formats)
190                 .zip(output_paths)
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")
195                     } else {
196                         output_dir.join(file_name)
197                     };
198                     decompress_file(DecompressOptions {
199                         input_file_path: input_path,
200                         formats,
201                         output_dir: &output_dir,
202                         output_file_path,
203                         question_policy,
204                         quiet: args.quiet,
205                         password: args.password.as_deref().map(|str| {
206                             <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
207                         }),
208                         remove,
209                     })
210                 })
211         }
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());
219                 }
220             } else {
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)? {
225                         return Ok(());
226                     }
228                     formats.push(file_formats);
229                 }
230             }
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() {
238                 if i > 0 {
239                     println!();
240                 }
241                 let formats = extension::flatten_compression_formats(&formats);
242                 list_archive_contents(
243                     archive_path,
244                     formats,
245                     list_options,
246                     question_policy,
247                     args.password
248                         .as_deref()
249                         .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")),
250                 )?;
251             }
253             Ok(())
254         }
255     }