ci: fix release script
[ouch.git] / src / commands / mod.rs
blobb9763d1fcdb619d8f3004c1102385287d0d6d0f1
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 rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
10 use utils::colors;
12 use crate::{
13     check,
14     cli::Subcommand,
15     commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
16     error::{Error, FinalError},
17     extension::{self, parse_format},
18     info,
19     list::ListOptions,
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 /// Warn the user that (de)compressing this .7z archive might freeze their system.
35 fn warn_user_about_loading_sevenz_in_memory() {
36     const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
37         \tThe format '.7z' is limited and cannot be (de)compressed using encoding streams.\n\
38         \tWhen using '.7z' with other formats, (de)compression must be done in-memory\n\
39         \tCareful, you might run out of RAM if the archive is too large!";
41     warning!("{}", SEVENZ_IN_MEMORY_LIMITATION_WARNING);
44 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
45 /// to assume everything is OK.
46 ///
47 /// There are a lot of custom errors to give enough error description and explanation.
48 pub fn run(
49     args: CliArgs,
50     question_policy: QuestionPolicy,
51     file_visibility_policy: FileVisibilityPolicy,
52 ) -> crate::Result<()> {
53     match args.cmd {
54         Subcommand::Compress {
55             files,
56             output: output_path,
57             level,
58             fast,
59             slow,
60         } => {
61             // After cleaning, if there are no input files left, exit
62             if files.is_empty() {
63                 return Err(FinalError::with_title("No files to compress").into());
64             }
66             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
67             let (formats_from_flag, formats) = match args.format {
68                 Some(formats) => {
69                     let parsed_formats = parse_format(&formats)?;
70                     (Some(formats), parsed_formats)
71                 }
72                 None => (None, extension::extensions_from_path(&output_path)),
73             };
75             check::check_invalid_compression_with_non_archive_format(
76                 &formats,
77                 &output_path,
78                 &files,
79                 formats_from_flag.as_ref(),
80             )?;
81             check::check_archive_formats_position(&formats, &output_path)?;
83             let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
84                 Some(writer) => writer,
85                 None => return Ok(()),
86             };
88             let level = if fast {
89                 Some(1) // Lowest level of compression
90             } else if slow {
91                 Some(i16::MAX) // Highest level of compression
92             } else {
93                 level
94             };
96             let compress_result = compress_files(
97                 files,
98                 formats,
99                 output_file,
100                 &output_path,
101                 args.quiet,
102                 question_policy,
103                 file_visibility_policy,
104                 level,
105             );
107             if let Ok(true) = compress_result {
108                 // this is only printed once, so it doesn't result in much text. On the other hand,
109                 // having a final status message is important especially in an accessibility context
110                 // as screen readers may not read a commands exit code, making it hard to reason
111                 // about whether the command succeeded without such a message
112                 info!(accessible, "Successfully compressed '{}'.", to_utf(&output_path));
113             } else {
114                 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
115                 //
116                 // if deleting fails, print an extra alert message pointing
117                 // out that we left a possibly CORRUPTED file at `output_path`
118                 if utils::remove_file_or_dir(&output_path).is_err() {
119                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
120                     eprintln!(
121                         "  Ouch failed to delete the file '{}'.",
122                         EscapedPathDisplay::new(&output_path)
123                     );
124                     eprintln!("  Please delete it manually.");
125                     eprintln!("  This file is corrupted if compression didn't finished.");
127                     if compress_result.is_err() {
128                         eprintln!("  Compression failed for reasons below.");
129                     }
130                 }
131             }
133             compress_result?;
134         }
135         Subcommand::Decompress { files, output_dir } => {
136             let mut output_paths = vec![];
137             let mut formats = vec![];
139             if let Some(format) = args.format {
140                 let format = parse_format(&format)?;
141                 for path in files.iter() {
142                     let file_name = path.file_name().ok_or_else(|| Error::NotFound {
143                         error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
144                     })?;
145                     output_paths.push(file_name.as_ref());
146                     formats.push(format.clone());
147                 }
148             } else {
149                 for path in files.iter() {
150                     let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
152                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
153                         return Ok(());
154                     }
156                     output_paths.push(pathbase);
157                     formats.push(file_formats);
158                 }
159             }
161             check::check_missing_formats_when_decompressing(&files, &formats)?;
163             // The directory that will contain the output files
164             // We default to the current directory if the user didn't specify an output directory with --dir
165             let output_dir = if let Some(dir) = output_dir {
166                 utils::create_dir_if_non_existent(&dir)?;
167                 dir
168             } else {
169                 PathBuf::from(".")
170             };
172             files
173                 .par_iter()
174                 .zip(formats)
175                 .zip(output_paths)
176                 .try_for_each(|((input_path, formats), file_name)| {
177                     let output_file_path = output_dir.join(file_name); // Path used by single file format archives
178                     decompress_file(
179                         input_path,
180                         formats,
181                         &output_dir,
182                         output_file_path,
183                         question_policy,
184                         args.quiet,
185                     )
186                 })?;
187         }
188         Subcommand::List { archives: files, tree } => {
189             let mut formats = vec![];
191             if let Some(format) = args.format {
192                 let format = parse_format(&format)?;
193                 for _ in 0..files.len() {
194                     formats.push(format.clone());
195                 }
196             } else {
197                 for path in files.iter() {
198                     let mut file_formats = extension::extensions_from_path(path);
200                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
201                         return Ok(());
202                     }
204                     formats.push(file_formats);
205                 }
206             }
208             // Ensure we were not told to list the content of a non-archive compressed file
209             check::check_for_non_archive_formats(&files, &formats)?;
211             let list_options = ListOptions { tree };
213             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
214                 if i > 0 {
215                     println!();
216                 }
217                 let formats = extension::flatten_compression_formats(&formats);
218                 list_archive_contents(archive_path, formats, list_options, question_policy)?;
219             }
220         }
221     }
222     Ok(())