1 //! Receive command from the cli and call the respective function for that command.
11 mpsc::{channel, Sender},
16 use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
20 accessible::is_running_in_accessible_mode,
23 commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
24 error::{Error, FinalError},
25 extension::{self, parse_format},
29 message::{MessageLevel, PrintMessage},
30 to_utf, EscapedPathDisplay, FileVisibilityPolicy,
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!";
44 contents: ZIP_IN_MEMORY_LIMITATION_WARNING.to_string(),
46 level: MessageLevel::Warning,
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!";
60 contents: SEVENZ_IN_MEMORY_LIMITATION_WARNING.to_string(),
62 level: MessageLevel::Warning,
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.
70 /// There are a lot of custom errors to give enough error description and explanation.
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> {
91 MessageLevel::Info => {
93 if is_running_in_accessible_mode() {
94 Some(format!("{}Info:{} {}", *YELLOW, *RESET, msg.contents))
96 Some(format!("{}[INFO]{} {}", *YELLOW, *RESET, msg.contents))
98 } else if !is_running_in_accessible_mode() {
99 Some(format!("{}[INFO]{} {}", *YELLOW, *RESET, msg.contents))
104 MessageLevel::Warning => {
105 if is_running_in_accessible_mode() {
106 Some(format!("{}Warning:{} ", *ORANGE, *RESET))
108 Some(format!("{}[WARNING]{} ", *ORANGE, *RESET))
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) {
127 // TODO: Send this to stderr
130 } else if let Some(msg) = map_message(&msg) {
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();
149 Subcommand::Compress {
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());
161 // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
162 let (formats_from_flag, formats) = match args.format {
164 let parsed_formats = parse_format(&formats)?;
165 (Some(formats), parsed_formats)
167 None => (None, extension::extensions_from_path(&output_path, log_sender.clone())),
170 check::check_invalid_compression_with_non_archive_format(
174 formats_from_flag.as_ref(),
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(()),
183 let level = if fast {
184 Some(1) // Lowest level of compression
186 Some(i16::MAX) // Highest level of compression
191 let compress_result = compress_files(
198 file_visibility_policy,
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
210 contents: format!("Successfully compressed '{}'.", to_utf(&output_path)),
212 level: MessageLevel::Info,
216 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
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);
223 " Ouch failed to delete the file '{}'.",
224 EscapedPathDisplay::new(&output_path)
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.");
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)),
247 output_paths.push(file_name.as_ref());
248 formats.push(format.clone());
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())?
261 output_paths.push(pathbase);
262 formats.push(file_formats);
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())?;
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
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());
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())?
312 formats.push(file_formats);
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() {
325 let formats = extension::flatten_compression_formats(&formats);
326 list_archive_contents(archive_path, formats, list_options, question_policy, log_sender.clone())?;
331 // Drop our sender so when all threads are done, no clones are left
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();