fix some errors + warnings
[ouch.git] / src / commands / mod.rs
blobe1d90fc7594edd5c27c4da7db12c78ece1cd95bb
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::{
8     ops::ControlFlow,
9     path::PathBuf,
10     sync::{
11         mpsc::{channel, Sender},
12         Arc, Condvar, Mutex,
13     },
16 use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
17 use utils::colors;
19 use crate::{
20     accessible::is_running_in_accessible_mode,
21     check,
22     cli::Subcommand,
23     commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
24     error::{Error, FinalError},
25     extension::{self, parse_format},
26     list::ListOptions,
27     utils::{
28         self,
29         message::{MessageLevel, PrintMessage},
30         to_utf, EscapedPathDisplay, FileVisibilityPolicy,
31     },
32     CliArgs, QuestionPolicy,
35 /// Warn the user that (de)compressing this .zip archive might freeze their system.
36 fn warn_user_about_loading_zip_in_memory(log_sender: Sender<PrintMessage>) {
37     const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
38         \tThe format '.zip' is limited and cannot be (de)compressed using encoding streams.\n\
39         \tWhen using '.zip' with other formats, (de)compression must be done in-memory\n\
40         \tCareful, you might run out of RAM if the archive is too large!";
42     log_sender
43         .send(PrintMessage {
44             contents: ZIP_IN_MEMORY_LIMITATION_WARNING.to_string(),
45             accessible: true,
46             level: MessageLevel::Warning,
47         })
48         .unwrap();
51 /// Warn the user that (de)compressing this .7z archive might freeze their system.
52 fn warn_user_about_loading_sevenz_in_memory(log_sender: Sender<PrintMessage>) {
53     const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
54         \tThe format '.7z' is limited and cannot be (de)compressed using encoding streams.\n\
55         \tWhen using '.7z' with other formats, (de)compression must be done in-memory\n\
56         \tCareful, you might run out of RAM if the archive is too large!";
58     log_sender
59         .send(PrintMessage {
60             contents: SEVENZ_IN_MEMORY_LIMITATION_WARNING.to_string(),
61             accessible: true,
62             level: MessageLevel::Warning,
63         })
64         .unwrap();
67 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
68 /// to assume everything is OK.
69 ///
70 /// There are a lot of custom errors to give enough error description and explanation.
71 pub fn run(
72     args: CliArgs,
73     question_policy: QuestionPolicy,
74     file_visibility_policy: FileVisibilityPolicy,
75 ) -> crate::Result<()> {
76     let (log_sender, log_receiver) = channel::<PrintMessage>();
78     let pair = Arc::new((Mutex::new(false), Condvar::new()));
79     let pair2 = Arc::clone(&pair);
81     // Log received messages until all senders are dropped
82     rayon::spawn(move || {
83         use utils::colors::{ORANGE, RESET, YELLOW};
85         const BUFFER_SIZE: usize = 10;
86         let mut buffer = Vec::<String>::with_capacity(BUFFER_SIZE);
88         // TODO: Move this out to utils
89         fn map_message(msg: &PrintMessage) -> Option<String> {
90             match msg.level {
91                 MessageLevel::Info => {
92                     if msg.accessible {
93                         if is_running_in_accessible_mode() {
94                             Some(format!("{}Info:{} {}", *YELLOW, *RESET, msg.contents))
95                         } else {
96                             Some(format!("{}[INFO]{} {}", *YELLOW, *RESET, msg.contents))
97                         }
98                     } else if !is_running_in_accessible_mode() {
99                         Some(format!("{}[INFO]{} {}", *YELLOW, *RESET, msg.contents))
100                     } else {
101                         None
102                     }
103                 }
104                 MessageLevel::Warning => {
105                     if is_running_in_accessible_mode() {
106                         Some(format!("{}Warning:{} ", *ORANGE, *RESET))
107                     } else {
108                         Some(format!("{}[WARNING]{} ", *ORANGE, *RESET))
109                     }
110                 }
111             }
112         }
114         loop {
115             let msg = log_receiver.recv();
117             // Senders are still active
118             if let Ok(msg) = msg {
119                 // Print messages if buffer is full otherwise append to it
120                 if buffer.len() == BUFFER_SIZE {
121                     let mut tmp = buffer.join("\n");
123                     if let Some(msg) = map_message(&msg) {
124                         tmp.push_str(&msg);
125                     }
127                     // TODO: Send this to stderr
128                     println!("{}", tmp);
129                     buffer.clear();
130                 } else if let Some(msg) = map_message(&msg) {
131                     buffer.push(msg);
132                 }
133             } else {
134                 // All senders have been dropped
135                 // TODO: Send this to stderr
136                 println!("{}", buffer.join("\n"));
138                 // Wake up the main thread
139                 let (lock, cvar) = &*pair2;
140                 let mut flushed = lock.lock().unwrap();
141                 *flushed = true;
142                 cvar.notify_one();
143                 break;
144             }
145         }
146     });
148     match args.cmd {
149         Subcommand::Compress {
150             files,
151             output: output_path,
152             level,
153             fast,
154             slow,
155         } => {
156             // After cleaning, if there are no input files left, exit
157             if files.is_empty() {
158                 return Err(FinalError::with_title("No files to compress").into());
159             }
161             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
162             let (formats_from_flag, formats) = match args.format {
163                 Some(formats) => {
164                     let parsed_formats = parse_format(&formats)?;
165                     (Some(formats), parsed_formats)
166                 }
167                 None => (None, extension::extensions_from_path(&output_path, log_sender.clone())),
168             };
170             check::check_invalid_compression_with_non_archive_format(
171                 &formats,
172                 &output_path,
173                 &files,
174                 formats_from_flag.as_ref(),
175             )?;
176             check::check_archive_formats_position(&formats, &output_path)?;
178             let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
179                 Some(writer) => writer,
180                 None => return Ok(()),
181             };
183             let level = if fast {
184                 Some(1) // Lowest level of compression
185             } else if slow {
186                 Some(i16::MAX) // Highest level of compression
187             } else {
188                 level
189             };
191             let compress_result = compress_files(
192                 files,
193                 formats,
194                 output_file,
195                 &output_path,
196                 args.quiet,
197                 question_policy,
198                 file_visibility_policy,
199                 level,
200                 log_sender.clone(),
201             );
203             if let Ok(true) = compress_result {
204                 // this is only printed once, so it doesn't result in much text. On the other hand,
205                 // having a final status message is important especially in an accessibility context
206                 // as screen readers may not read a commands exit code, making it hard to reason
207                 // about whether the command succeeded without such a message
208                 log_sender
209                     .send(PrintMessage {
210                         contents: format!("Successfully compressed '{}'.", to_utf(&output_path)),
211                         accessible: true,
212                         level: MessageLevel::Info,
213                     })
214                     .unwrap();
215             } else {
216                 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
217                 //
218                 // if deleting fails, print an extra alert message pointing
219                 // out that we left a possibly CORRUPTED file at `output_path`
220                 if utils::remove_file_or_dir(&output_path).is_err() {
221                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
222                     eprintln!(
223                         "  Ouch failed to delete the file '{}'.",
224                         EscapedPathDisplay::new(&output_path)
225                     );
226                     eprintln!("  Please delete it manually.");
227                     eprintln!("  This file is corrupted if compression didn't finished.");
229                     if compress_result.is_err() {
230                         eprintln!("  Compression failed for reasons below.");
231                     }
232                 }
233             }
235             compress_result?;
236         }
237         Subcommand::Decompress { files, output_dir } => {
238             let mut output_paths = vec![];
239             let mut formats = vec![];
241             if let Some(format) = args.format {
242                 let format = parse_format(&format)?;
243                 for path in files.iter() {
244                     let file_name = path.file_name().ok_or_else(|| Error::NotFound {
245                         error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
246                     })?;
247                     output_paths.push(file_name.as_ref());
248                     formats.push(format.clone());
249                 }
250             } else {
251                 for path in files.iter() {
252                     let (pathbase, mut file_formats) =
253                         extension::separate_known_extensions_from_name(path, log_sender.clone());
255                     if let ControlFlow::Break(_) =
256                         check::check_mime_type(path, &mut file_formats, question_policy, log_sender.clone())?
257                     {
258                         return Ok(());
259                     }
261                     output_paths.push(pathbase);
262                     formats.push(file_formats);
263                 }
264             }
266             check::check_missing_formats_when_decompressing(&files, &formats)?;
268             // The directory that will contain the output files
269             // We default to the current directory if the user didn't specify an output directory with --dir
270             let output_dir = if let Some(dir) = output_dir {
271                 utils::create_dir_if_non_existent(&dir, log_sender.clone())?;
272                 dir
273             } else {
274                 PathBuf::from(".")
275             };
277             files
278                 .par_iter()
279                 .zip(formats)
280                 .zip(output_paths)
281                 .try_for_each(|((input_path, formats), file_name)| {
282                     let output_file_path = output_dir.join(file_name); // Path used by single file format archives
283                     decompress_file(
284                         input_path,
285                         formats,
286                         &output_dir,
287                         output_file_path,
288                         question_policy,
289                         args.quiet,
290                         log_sender.clone(),
291                     )
292                 })?;
293         }
294         Subcommand::List { archives: files, tree } => {
295             let mut formats = vec![];
297             if let Some(format) = args.format {
298                 let format = parse_format(&format)?;
299                 for _ in 0..files.len() {
300                     formats.push(format.clone());
301                 }
302             } else {
303                 for path in files.iter() {
304                     let mut file_formats = extension::extensions_from_path(path, log_sender.clone());
306                     if let ControlFlow::Break(_) =
307                         check::check_mime_type(path, &mut file_formats, question_policy, log_sender.clone())?
308                     {
309                         return Ok(());
310                     }
312                     formats.push(file_formats);
313                 }
314             }
316             // Ensure we were not told to list the content of a non-archive compressed file
317             check::check_for_non_archive_formats(&files, &formats)?;
319             let list_options = ListOptions { tree };
321             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
322                 if i > 0 {
323                     println!();
324                 }
325                 let formats = extension::flatten_compression_formats(&formats);
326                 list_archive_contents(archive_path, formats, list_options, question_policy, log_sender.clone())?;
327             }
328         }
329     }
331     // Drop our sender so when all threads are done, no clones are left
332     drop(log_sender);
334     // Prevent the main thread from exiting until the background thread handling the
335     // logging has set `flushed` to true.
336     let (lock, cvar) = &*pair;
337     let guard = lock.lock().unwrap();
338     let _flushed = cvar.wait(guard).unwrap();
340     Ok(())