chore: simplify code after feature stabilization
[ouch.git] / src / commands / mod.rs
blobc4cd8e0125470f4c7fcada436dc855afb4be109a
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 rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
11 use utils::colors;
13 use crate::{
14     check,
15     cli::Subcommand,
16     commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
17     error::{Error, FinalError},
18     extension::{self, parse_format_flag},
19     list::ListOptions,
20     utils::{
21         self, colors::*, is_path_stdin, logger::info_accessible, path_to_str, EscapedPathDisplay, FileVisibilityPolicy,
22     },
23     CliArgs, QuestionPolicy,
26 /// Warn the user that (de)compressing this .zip archive might freeze their system.
27 fn warn_user_about_loading_zip_in_memory() {
28     const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n  \
29         The format '.zip' is limited by design and cannot be (de)compressed with encoding streams.\n  \
30         When chaining '.zip' with other formats, all (de)compression needs to be done in-memory\n  \
31         Careful, you might run out of RAM if the archive is too large!";
33     eprintln!("{}[WARNING]{}: {ZIP_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
36 /// Warn the user that (de)compressing this .7z archive might freeze their system.
37 fn warn_user_about_loading_sevenz_in_memory() {
38     const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n  \
39         The format '.7z' is limited by design and cannot be (de)compressed with encoding streams.\n  \
40         When chaining '.7z' with other formats, all (de)compression needs to be done in-memory\n  \
41         Careful, you might run out of RAM if the archive is too large!";
43     eprintln!("{}[WARNING]{}: {SEVENZ_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
46 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
47 /// to assume everything is OK.
48 ///
49 /// There are a lot of custom errors to give enough error description and explanation.
50 pub fn run(
51     args: CliArgs,
52     question_policy: QuestionPolicy,
53     file_visibility_policy: FileVisibilityPolicy,
54 ) -> crate::Result<()> {
55     match args.cmd {
56         Subcommand::Compress {
57             files,
58             output: output_path,
59             level,
60             fast,
61             slow,
62         } => {
63             // After cleaning, if there are no input files left, exit
64             if files.is_empty() {
65                 return Err(FinalError::with_title("No files to compress").into());
66             }
68             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
69             let (formats_from_flag, formats) = match args.format {
70                 Some(formats) => {
71                     let parsed_formats = parse_format_flag(&formats)?;
72                     (Some(formats), parsed_formats)
73                 }
74                 None => (None, extension::extensions_from_path(&output_path)),
75             };
77             check::check_invalid_compression_with_non_archive_format(
78                 &formats,
79                 &output_path,
80                 &files,
81                 formats_from_flag.as_ref(),
82             )?;
83             check::check_archive_formats_position(&formats, &output_path)?;
85             let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
86                 Some(writer) => writer,
87                 None => return Ok(()),
88             };
90             let level = if fast {
91                 Some(1) // Lowest level of compression
92             } else if slow {
93                 Some(i16::MAX) // Highest level of compression
94             } else {
95                 level
96             };
98             let compress_result = compress_files(
99                 files,
100                 formats,
101                 output_file,
102                 &output_path,
103                 args.quiet,
104                 question_policy,
105                 file_visibility_policy,
106                 level,
107             );
109             if let Ok(true) = compress_result {
110                 // this is only printed once, so it doesn't result in much text. On the other hand,
111                 // having a final status message is important especially in an accessibility context
112                 // as screen readers may not read a commands exit code, making it hard to reason
113                 // about whether the command succeeded without such a message
114                 info_accessible(format!("Successfully compressed '{}'.", path_to_str(&output_path)));
115             } else {
116                 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
117                 //
118                 // if deleting fails, print an extra alert message pointing
119                 // out that we left a possibly CORRUPTED file at `output_path`
120                 if utils::remove_file_or_dir(&output_path).is_err() {
121                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
122                     eprintln!(
123                         "  Ouch failed to delete the file '{}'.",
124                         EscapedPathDisplay::new(&output_path)
125                     );
126                     eprintln!("  Please delete it manually.");
127                     eprintln!("  This file is corrupted if compression didn't finished.");
129                     if compress_result.is_err() {
130                         eprintln!("  Compression failed for reasons below.");
131                     }
132                 }
133             }
135             compress_result.map(|_| ())
136         }
137         Subcommand::Decompress { files, output_dir } => {
138             let mut output_paths = vec![];
139             let mut formats = vec![];
141             if let Some(format) = args.format {
142                 let format = parse_format_flag(&format)?;
143                 for path in files.iter() {
144                     let file_name = path.file_name().ok_or_else(|| Error::NotFound {
145                         error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
146                     })?;
147                     output_paths.push(file_name.as_ref());
148                     formats.push(format.clone());
149                 }
150             } else {
151                 for path in files.iter() {
152                     let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
154                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
155                         return Ok(());
156                     }
158                     output_paths.push(pathbase);
159                     formats.push(file_formats);
160                 }
161             }
163             check::check_missing_formats_when_decompressing(&files, &formats)?;
165             // The directory that will contain the output files
166             // We default to the current directory if the user didn't specify an output directory with --dir
167             let output_dir = if let Some(dir) = output_dir {
168                 utils::create_dir_if_non_existent(&dir)?;
169                 dir
170             } else {
171                 PathBuf::from(".")
172             };
174             files
175                 .par_iter()
176                 .zip(formats)
177                 .zip(output_paths)
178                 .try_for_each(|((input_path, formats), file_name)| {
179                     // Path used by single file format archives
180                     let output_file_path = if is_path_stdin(file_name) {
181                         output_dir.join("stdin-output")
182                     } else {
183                         output_dir.join(file_name)
184                     };
185                     decompress_file(
186                         input_path,
187                         formats,
188                         &output_dir,
189                         output_file_path,
190                         question_policy,
191                         args.quiet,
192                         args.password.as_deref().map(|str| {
193                             <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
194                         }),
195                     )
196                 })
197         }
198         Subcommand::List { archives: files, tree } => {
199             let mut formats = vec![];
201             if let Some(format) = args.format {
202                 let format = parse_format_flag(&format)?;
203                 for _ in 0..files.len() {
204                     formats.push(format.clone());
205                 }
206             } else {
207                 for path in files.iter() {
208                     let mut file_formats = extension::extensions_from_path(path);
210                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
211                         return Ok(());
212                     }
214                     formats.push(file_formats);
215                 }
216             }
218             // Ensure we were not told to list the content of a non-archive compressed file
219             check::check_for_non_archive_formats(&files, &formats)?;
221             let list_options = ListOptions { tree };
223             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
224                 if i > 0 {
225                     println!();
226                 }
227                 let formats = extension::flatten_compression_formats(&formats);
228                 list_archive_contents(
229                     archive_path,
230                     formats,
231                     list_options,
232                     question_policy,
233                     args.password
234                         .as_deref()
235                         .map(|str| <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")),
236                 )?;
237             }
239             Ok(())
240         }
241     }