refactor: improve code formatting in `mod.rs` and `logger.rs`
[ouch.git] / src / commands / mod.rs
blob0bcb9b339172025c91ca5f8952076519d274727d
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<()> {
56     if let Some(threads) = args.threads {
57         rayon::ThreadPoolBuilder::new()
58             .num_threads(threads)
59             .build_global()
60             .unwrap();
61     }
63     match args.cmd {
64         Subcommand::Compress {
65             files,
66             output: output_path,
67             level,
68             fast,
69             slow,
70         } => {
71             // After cleaning, if there are no input files left, exit
72             if files.is_empty() {
73                 return Err(FinalError::with_title("No files to compress").into());
74             }
76             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
77             let (formats_from_flag, formats) = match args.format {
78                 Some(formats) => {
79                     let parsed_formats = parse_format_flag(&formats)?;
80                     (Some(formats), parsed_formats)
81                 }
82                 None => (None, extension::extensions_from_path(&output_path)),
83             };
85             check::check_invalid_compression_with_non_archive_format(
86                 &formats,
87                 &output_path,
88                 &files,
89                 formats_from_flag.as_ref(),
90             )?;
91             check::check_archive_formats_position(&formats, &output_path)?;
93             let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
94                 Some(writer) => writer,
95                 None => return Ok(()),
96             };
98             let level = if fast {
99                 Some(1) // Lowest level of compression
100             } else if slow {
101                 Some(i16::MAX) // Highest level of compression
102             } else {
103                 level
104             };
106             let compress_result = compress_files(
107                 files,
108                 formats,
109                 output_file,
110                 &output_path,
111                 args.quiet,
112                 question_policy,
113                 file_visibility_policy,
114                 level,
115             );
117             if let Ok(true) = compress_result {
118                 // this is only printed once, so it doesn't result in much text. On the other hand,
119                 // having a final status message is important especially in an accessibility context
120                 // as screen readers may not read a commands exit code, making it hard to reason
121                 // about whether the command succeeded without such a message
122                 info_accessible(format!("Successfully compressed '{}'", path_to_str(&output_path)));
123             } else {
124                 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
125                 //
126                 // if deleting fails, print an extra alert message pointing
127                 // out that we left a possibly CORRUPTED file at `output_path`
128                 if utils::remove_file_or_dir(&output_path).is_err() {
129                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
130                     eprintln!(
131                         "  Ouch failed to delete the file '{}'.",
132                         EscapedPathDisplay::new(&output_path)
133                     );
134                     eprintln!("  Please delete it manually.");
135                     eprintln!("  This file is corrupted if compression didn't finished.");
137                     if compress_result.is_err() {
138                         eprintln!("  Compression failed for reasons below.");
139                     }
140                 }
141             }
143             compress_result.map(|_| ())
144         }
145         Subcommand::Decompress {
146             files,
147             output_dir,
148             remove,
149         } => {
150             let mut output_paths = vec![];
151             let mut formats = vec![];
153             if let Some(format) = args.format {
154                 let format = parse_format_flag(&format)?;
155                 for path in files.iter() {
156                     let file_name = path.file_name().ok_or_else(|| Error::NotFound {
157                         error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
158                     })?;
159                     output_paths.push(file_name.as_ref());
160                     formats.push(format.clone());
161                 }
162             } else {
163                 for path in files.iter() {
164                     let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
166                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
167                         return Ok(());
168                     }
170                     output_paths.push(pathbase);
171                     formats.push(file_formats);
172                 }
173             }
175             check::check_missing_formats_when_decompressing(&files, &formats)?;
177             // The directory that will contain the output files
178             // We default to the current directory if the user didn't specify an output directory with --dir
179             let output_dir = if let Some(dir) = output_dir {
180                 utils::create_dir_if_non_existent(&dir)?;
181                 dir
182             } else {
183                 PathBuf::from(".")
184             };
186             files
187                 .par_iter()
188                 .zip(formats)
189                 .zip(output_paths)
190                 .try_for_each(|((input_path, formats), file_name)| {
191                     // Path used by single file format archives
192                     let output_file_path = if is_path_stdin(file_name) {
193                         output_dir.join("stdin-output")
194                     } else {
195                         output_dir.join(file_name)
196                     };
197                     decompress_file(DecompressOptions {
198                         input_file_path: input_path,
199                         formats,
200                         output_dir: &output_dir,
201                         output_file_path,
202                         question_policy,
203                         quiet: args.quiet,
204                         password: args.password.as_deref().map(|str| {
205                             <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
206                         }),
207                         remove,
208                     })
209                 })
210         }
211         Subcommand::List { archives: files, tree } => {
212             let mut formats = vec![];
214             if let Some(format) = args.format {
215                 let format = parse_format_flag(&format)?;
216                 for _ in 0..files.len() {
217                     formats.push(format.clone());
218                 }
219             } else {
220                 for path in files.iter() {
221                     let mut file_formats = extension::extensions_from_path(path);
223                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
224                         return Ok(());
225                     }
227                     formats.push(file_formats);
228                 }
229             }
231             // Ensure we were not told to list the content of a non-archive compressed file
232             check::check_for_non_archive_formats(&files, &formats)?;
234             let list_options = ListOptions { tree };
236             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
237                 if i > 0 {
238                     println!();
239                 }
240                 let formats = extension::flatten_compression_formats(&formats);
241                 list_archive_contents(
242                     archive_path,
243                     formats,
244                     list_options,
245                     question_policy,
246                     args.password
247                         .as_deref()
248                         .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")),
249                 )?;
250             }
252             Ok(())
253         }
254     }