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};
13 check::check_mime_type,
15 commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
16 error::{Error, FinalError},
17 extension::{self, build_archive_file_suggestion, parse_format, Extension},
20 utils::{self, pretty_format_list_of_paths, 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 /// In the context of listing archives, this function checks if `ouch` was told to list
35 /// the contents of a compressed file that is not an archive
36 fn check_for_non_archive_formats(files: &[PathBuf], formats: &[Vec<Extension>]) -> crate::Result<()> {
37 let mut not_archives = files
40 .filter(|(_, formats)| !formats.first().map(Extension::is_archive).unwrap_or(false))
41 .map(|(path, _)| path)
44 if not_archives.peek().is_some() {
45 let not_archives: Vec<_> = not_archives.collect();
46 let error = FinalError::with_title("Cannot list archive contents")
47 .detail("Only archives can have their contents listed")
49 "Files are not archives: {}",
50 pretty_format_list_of_paths(¬_archives)
53 return Err(error.into());
59 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
60 /// to assume everything is OK.
62 /// There are a lot of custom errors to give enough error description and explanation.
65 question_policy: QuestionPolicy,
66 file_visibility_policy: FileVisibilityPolicy,
67 ) -> crate::Result<()> {
69 Subcommand::Compress {
73 // After cleaning, if there are no input files left, exit
75 return Err(FinalError::with_title("No files to compress").into());
78 // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
79 let (formats_from_flag, formats) = match args.format {
81 let parsed_formats = parse_format(&formats)?;
82 (Some(formats), parsed_formats)
84 None => (None, extension::extensions_from_path(&output_path)),
87 let first_format = formats.first().ok_or_else(|| {
88 let output_path = EscapedPathDisplay::new(&output_path);
89 FinalError::with_title(format!("Cannot compress to '{output_path}'."))
90 .detail("You shall supply the compression format")
91 .hint("Try adding supported extensions (see --help):")
92 .hint(format!(" ouch compress <FILES>... {output_path}.tar.gz"))
93 .hint(format!(" ouch compress <FILES>... {output_path}.zip"))
95 .hint("Alternatively, you can overwrite this option by using the '--format' flag:")
96 .hint(format!(" ouch compress <FILES>... {output_path} --format tar.gz"))
99 let is_some_input_a_folder = files.iter().any(|path| path.is_dir());
100 let is_multiple_inputs = files.len() > 1;
102 // If first format is not archive, can't compress folder, or multiple files
103 // Index safety: empty formats should be checked above.
104 if !first_format.is_archive() && (is_some_input_a_folder || is_multiple_inputs) {
105 let first_detail_message = if is_multiple_inputs {
106 "You are trying to compress multiple files."
108 "You are trying to compress a folder."
111 let (from_hint, to_hint) = if let Some(formats) = formats_from_flag {
112 let formats = formats.to_string_lossy();
114 format!("From: --format {formats}"),
115 format!("To: --format tar.{formats}"),
118 // This piece of code creates a suggestion for compressing multiple files
120 // Change from file.bz.xz
122 let suggested_output_path = build_archive_file_suggestion(&output_path, ".tar")
123 .expect("output path should contain a compression format");
126 format!("From: {}", EscapedPathDisplay::new(&output_path)),
127 format!("To: {suggested_output_path}"),
130 let output_path = EscapedPathDisplay::new(&output_path);
132 let error = FinalError::with_title(format!("Cannot compress to '{output_path}'."))
133 .detail(first_detail_message)
135 "The compression format '{first_format}' does not accept multiple files.",
137 .detail("Formats that bundle files into an archive are tar and zip.")
138 .hint(format!("Try inserting 'tar.' or 'zip.' before '{first_format}'."))
142 return Err(error.into());
145 if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
146 let error = FinalError::with_title(format!(
147 "Cannot compress to '{}'.",
148 EscapedPathDisplay::new(&output_path)
150 .detail(format!("Found the format '{format}' in an incorrect position."))
152 "'{format}' can only be used at the start of the file extension."
155 "If you wish to compress multiple files, start the extension with '{format}'."
158 "Otherwise, remove the last '{}' from '{}'.",
160 EscapedPathDisplay::new(&output_path)
163 return Err(error.into());
166 let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
167 Some(writer) => writer,
168 None => return Ok(()),
171 let compress_result = compress_files(
178 file_visibility_policy,
181 if let Ok(true) = compress_result {
182 // this is only printed once, so it doesn't result in much text. On the other hand,
183 // having a final status message is important especially in an accessibility context
184 // as screen readers may not read a commands exit code, making it hard to reason
185 // about whether the command succeeded without such a message
186 info!(accessible, "Successfully compressed '{}'.", to_utf(&output_path));
188 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
190 // if deleting fails, print an extra alert message pointing
191 // out that we left a possibly CORRUPTED file at `output_path`
192 if utils::remove_file_or_dir(&output_path).is_err() {
193 eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
195 " Ouch failed to delete the file '{}'.",
196 EscapedPathDisplay::new(&output_path)
198 eprintln!(" Please delete it manually.");
199 eprintln!(" This file is corrupted if compression didn't finished.");
201 if compress_result.is_err() {
202 eprintln!(" Compression failed for reasons below.");
209 Subcommand::Decompress { files, output_dir } => {
210 let mut output_paths = vec![];
211 let mut formats = vec![];
213 if let Some(format) = args.format {
214 let format = parse_format(&format)?;
215 for path in files.iter() {
216 let file_name = path.file_name().ok_or_else(|| Error::NotFound {
217 error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
219 output_paths.push(file_name.as_ref());
220 formats.push(format.clone());
223 for path in files.iter() {
224 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
225 output_paths.push(file_output_path);
226 formats.push(file_formats);
230 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
234 let files_missing_format: Vec<PathBuf> = files
237 .filter(|(_, formats)| formats.is_empty())
238 .map(|(input_path, _)| PathBuf::from(input_path))
241 if let Some(path) = files_missing_format.first() {
242 let error = FinalError::with_title("Cannot decompress files without extensions")
244 "Files without supported extensions: {}",
245 pretty_format_list_of_paths(&files_missing_format)
247 .detail("Decompression formats are detected automatically by the file extension")
248 .hint("Provide a file with a supported extension:")
249 .hint(" ouch decompress example.tar.gz")
251 .hint("Or overwrite this option with the '--format' flag:")
253 " ouch decompress {} --format tar.gz",
254 EscapedPathDisplay::new(path),
257 return Err(error.into());
260 // The directory that will contain the output files
261 // We default to the current directory if the user didn't specify an output directory with --dir
262 let output_dir = if let Some(dir) = output_dir {
263 utils::create_dir_if_non_existent(&dir)?;
273 .try_for_each(|((input_path, formats), file_name)| {
274 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
285 Subcommand::List { archives: files, tree } => {
286 let mut formats = vec![];
288 if let Some(format) = args.format {
289 let format = parse_format(&format)?;
290 for _ in 0..files.len() {
291 formats.push(format.clone());
294 for path in files.iter() {
295 let file_formats = extension::extensions_from_path(path);
296 formats.push(file_formats);
299 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
304 // Ensure we were not told to list the content of a non-archive compressed file
305 check_for_non_archive_formats(&files, &formats)?;
307 let list_options = ListOptions { tree };
309 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
313 let formats = extension::flatten_compression_formats(&formats);
314 list_archive_contents(archive_path, formats, list_options, question_policy)?;