1 //! Receive command from the cli and call the respective function for that command.
7 use std::{ops::ControlFlow, path::PathBuf};
9 use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
15 commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
16 error::{Error, FinalError},
17 extension::{self, parse_format},
20 utils::{self, to_utf, EscapedPathDisplay, FileVisibilityPolicy},
21 warning, CliArgs, QuestionPolicy,
24 /// Warn the user that (de)compressing this .zip archive might freeze their system.
25 fn warn_user_about_loading_zip_in_memory() {
26 const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
27 \tThe format '.zip' is limited and cannot be (de)compressed using encoding streams.\n\
28 \tWhen using '.zip' with other formats, (de)compression must be done in-memory\n\
29 \tCareful, you might run out of RAM if the archive is too large!";
31 warning!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
34 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
35 /// to assume everything is OK.
37 /// There are a lot of custom errors to give enough error description and explanation.
40 question_policy: QuestionPolicy,
41 file_visibility_policy: FileVisibilityPolicy,
42 ) -> crate::Result<()> {
44 Subcommand::Compress {
51 // After cleaning, if there are no input files left, exit
53 return Err(FinalError::with_title("No files to compress").into());
56 // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
57 let (formats_from_flag, formats) = match args.format {
59 let parsed_formats = parse_format(&formats)?;
60 (Some(formats), parsed_formats)
62 None => (None, extension::extensions_from_path(&output_path)),
65 check::check_invalid_compression_with_non_archive_format(
69 formats_from_flag.as_ref(),
71 check::check_archive_formats_position(&formats, &output_path)?;
73 let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
74 Some(writer) => writer,
75 None => return Ok(()),
79 Some(1) // Lowest level of compression
81 Some(i16::MAX) // Highest level of compression
86 let compress_result = compress_files(
93 file_visibility_policy,
97 if let Ok(true) = compress_result {
98 // this is only printed once, so it doesn't result in much text. On the other hand,
99 // having a final status message is important especially in an accessibility context
100 // as screen readers may not read a commands exit code, making it hard to reason
101 // about whether the command succeeded without such a message
102 info!(accessible, "Successfully compressed '{}'.", to_utf(&output_path));
104 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
106 // if deleting fails, print an extra alert message pointing
107 // out that we left a possibly CORRUPTED file at `output_path`
108 if utils::remove_file_or_dir(&output_path).is_err() {
109 eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
111 " Ouch failed to delete the file '{}'.",
112 EscapedPathDisplay::new(&output_path)
114 eprintln!(" Please delete it manually.");
115 eprintln!(" This file is corrupted if compression didn't finished.");
117 if compress_result.is_err() {
118 eprintln!(" Compression failed for reasons below.");
125 Subcommand::Decompress { files, output_dir } => {
126 let mut output_paths = vec![];
127 let mut formats = vec![];
129 if let Some(format) = args.format {
130 let format = parse_format(&format)?;
131 for path in files.iter() {
132 let file_name = path.file_name().ok_or_else(|| Error::NotFound {
133 error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
135 output_paths.push(file_name.as_ref());
136 formats.push(format.clone());
139 for path in files.iter() {
140 let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
142 if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
146 output_paths.push(pathbase);
147 formats.push(file_formats);
151 check::check_missing_formats_when_decompressing(&files, &formats)?;
153 // The directory that will contain the output files
154 // We default to the current directory if the user didn't specify an output directory with --dir
155 let output_dir = if let Some(dir) = output_dir {
156 utils::create_dir_if_non_existent(&dir)?;
166 .try_for_each(|((input_path, formats), file_name)| {
167 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
178 Subcommand::List { archives: files, tree } => {
179 let mut formats = vec![];
181 if let Some(format) = args.format {
182 let format = parse_format(&format)?;
183 for _ in 0..files.len() {
184 formats.push(format.clone());
187 for path in files.iter() {
188 let mut file_formats = extension::extensions_from_path(path);
190 if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
194 formats.push(file_formats);
198 // Ensure we were not told to list the content of a non-archive compressed file
199 check::check_for_non_archive_formats(&files, &formats)?;
201 let list_options = ListOptions { tree };
203 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
207 let formats = extension::flatten_compression_formats(&formats);
208 list_archive_contents(archive_path, formats, list_options, question_policy)?;