Refactor `FinalError::display_and_crash` into Error::Custom
[ouch.git] / src / commands.rs
blob098d5a25e8f6c8f602af8044de23500cf415b94d
1 //! Core of the crate, where the `compress_files` and `decompress_file` functions are implemented
2 //!
3 //! Also, where correctly call functions based on the detected `Command`.
5 use std::{
6     fs,
7     io::{self, BufReader, BufWriter, Read, Write},
8     path::{Path, PathBuf},
9 };
11 use utils::colors;
13 use crate::{
14     archive,
15     cli::Command,
16     error::FinalError,
17     extension::{
18         self,
19         CompressionFormat::{self, *},
20     },
21     info, oof, utils,
22     utils::to_utf,
23     Error,
26 // Used in BufReader and BufWriter to perform less syscalls
27 const BUFFER_CAPACITY: usize = 1024 * 64;
29 pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
30     match command {
31         Command::Compress { files, output_path } => {
32             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
33             let formats = extension::extensions_from_path(&output_path);
35             if formats.is_empty() {
36                 let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
37                     .detail("You shall supply the compression format via the extension.")
38                     .hint("Try adding something like .tar.gz or .zip to the output file.")
39                     .hint("")
40                     .hint("Examples:")
41                     .hint(format!("  ouch compress ... {}.tar.gz", to_utf(&output_path)))
42                     .hint(format!("  ouch compress ... {}.zip", to_utf(&output_path)))
43                     .into_owned();
45                 return Err(Error::with_reason(reason));
46             }
48             if matches!(&formats[0], Bzip | Gzip | Lzma) && files.len() > 1 {
49                 // This piece of code creates a sugestion for compressing multiple files
50                 // It says:
51                 // Change from file.bz.xz
52                 // To          file.tar.bz.xz
53                 let extensions_text: String = formats.iter().map(|format| format.to_string()).collect();
55                 let output_path = to_utf(output_path);
57                 // Breaks if Lzma is .lz or .lzma and not .xz
58                 // Or if Bzip is .bz2 and not .bz
59                 let extensions_start_position = output_path.rfind(&extensions_text).unwrap();
60                 let pos = extensions_start_position;
61                 let empty_range = pos..pos;
62                 let mut suggested_output_path = output_path.clone();
63                 suggested_output_path.replace_range(empty_range, ".tar");
65                 let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
66                     .detail("You are trying to compress multiple files.")
67                     .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
68                     .detail("The only supported formats that bundle files into an archive are .tar and .zip.")
69                     .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
70                     .hint(format!("From: {}", output_path))
71                     .hint(format!(" To : {}", suggested_output_path))
72                     .into_owned();
74                 return Err(Error::with_reason(reason));
75             }
77             if let Some(format) = formats.iter().skip(1).position(|format| matches!(format, Tar | Zip)) {
78                 let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
79                     .detail(format!("Found the format '{}' in an incorrect position.", format))
80                     .detail(format!("{} can only be used at the start of the file extension.", format))
81                     .hint(format!("If you wish to compress multiple files, start the extension with {}.", format))
82                     .hint(format!("Otherwise, remove {} from '{}'.", format, to_utf(&output_path)))
83                     .into_owned();
85                 return Err(Error::with_reason(reason));
86             }
88             if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? {
89                 // User does not want to overwrite this file
90                 return Ok(());
91             }
93             let output_file = fs::File::create(&output_path)?;
94             let compress_result = compress_files(files, formats, output_file, flags);
96             // If any error occurred, delete incomplete file
97             if compress_result.is_err() {
98                 // Print an extra alert message pointing out that we left a possibly
99                 // CORRUPTED FILE at `output_path`
100                 if let Err(err) = fs::remove_file(&output_path) {
101                     eprintln!("{red}FATAL ERROR:\n", red = colors::red());
102                     eprintln!("  Please manually delete '{}'.", to_utf(&output_path));
103                     eprintln!("  Compression failed and we could not delete '{}'.", to_utf(&output_path),);
104                     eprintln!("  Error:{reset} {}{red}.{reset}\n", err, reset = colors::reset(), red = colors::red());
105                 }
106             } else {
107                 info!("Successfully compressed '{}'.", to_utf(output_path));
108             }
110             compress_result?;
111         }
112         Command::Decompress { files, output_folder } => {
113             let mut output_paths = vec![];
114             let mut formats = vec![];
116             for path in files.iter() {
117                 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
118                 output_paths.push(file_output_path);
119                 formats.push(file_formats);
120             }
122             let files_missing_format: Vec<PathBuf> = files
123                 .iter()
124                 .zip(&formats)
125                 .filter(|(_, formats)| formats.is_empty())
126                 .map(|(input_path, _)| PathBuf::from(input_path))
127                 .collect();
129             // Error
130             if !files_missing_format.is_empty() {
131                 eprintln!("Some file you asked ouch to decompress lacks a supported extension.");
132                 eprintln!("Could not decompress {}.", to_utf(&files_missing_format[0]));
133                 todo!(
134                     "Dev note: add this error variant and pass the Vec to it, all the files \
135                      lacking extension shall be shown: {:#?}.",
136                     files_missing_format
137                 );
138             }
140             // From Option<PathBuf> to Option<&Path>
141             let output_folder = output_folder.as_ref().map(|path| path.as_ref());
143             for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
144                 decompress_file(input_path, formats, output_folder, file_name, flags)?;
145             }
146         }
147         Command::ShowHelp => crate::help_command(),
148         Command::ShowVersion => crate::version_command(),
149     }
150     Ok(())
153 fn compress_files(
154     files: Vec<PathBuf>,
155     formats: Vec<CompressionFormat>,
156     output_file: fs::File,
157     _flags: &oof::Flags,
158 ) -> crate::Result<()> {
159     let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
161     if formats.len() == 1 {
162         let build_archive_from_paths = match formats[0] {
163             Tar => archive::tar::build_archive_from_paths,
164             Zip => archive::zip::build_archive_from_paths,
165             _ => unreachable!(),
166         };
168         let mut bufwriter = build_archive_from_paths(&files, file_writer)?;
169         bufwriter.flush()?;
170     } else {
171         let mut writer: Box<dyn Write> = Box::new(file_writer);
173         // Grab previous encoder and wrap it inside of a new one
174         let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| {
175             let encoder: Box<dyn Write> = match format {
176                 Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
177                 Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
178                 Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
179                 Zstd => {
180                     let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
181                     // Safety:
182                     //     Encoder::new() can only fail if `level` is invalid, but Default::default()
183                     //     is guaranteed to be valid
184                     Box::new(zstd_encoder.unwrap().auto_finish())
185                 }
186                 _ => unreachable!(),
187             };
188             encoder
189         };
191         for format in formats.iter().skip(1).rev() {
192             writer = chain_writer_encoder(format, writer);
193         }
195         match formats[0] {
196             Gzip | Bzip | Lzma | Zstd => {
197                 writer = chain_writer_encoder(&formats[0], writer);
198                 let mut reader = fs::File::open(&files[0]).unwrap();
199                 io::copy(&mut reader, &mut writer)?;
200             }
201             Tar => {
202                 let mut writer = archive::tar::build_archive_from_paths(&files, writer)?;
203                 writer.flush()?;
204             }
205             Zip => {
206                 eprintln!("{yellow}Warning:{reset}", yellow = colors::yellow(), reset = colors::reset());
207                 eprintln!("\tCompressing .zip entirely in memory.");
208                 eprintln!("\tIf the file is too big, your pc might freeze!");
209                 eprintln!(
210                     "\tThis is a limitation for formats like '{}'.",
211                     formats.iter().map(|format| format.to_string()).collect::<String>()
212                 );
213                 eprintln!("\tThe design of .zip makes it impossible to compress via stream.");
215                 let mut vec_buffer = io::Cursor::new(vec![]);
216                 archive::zip::build_archive_from_paths(&files, &mut vec_buffer)?;
217                 let vec_buffer = vec_buffer.into_inner();
218                 io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
219             }
220         }
221     }
223     Ok(())
226 // File at input_file_path is opened for reading, example: "archive.tar.gz"
227 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
228 // output_folder it's where the file will be decompressed to
229 // file_name is only used when extracting single file formats, no archive formats like .tar or .zip
230 fn decompress_file(
231     input_file_path: &Path,
232     formats: Vec<extension::CompressionFormat>,
233     output_folder: Option<&Path>,
234     file_name: &Path,
235     flags: &oof::Flags,
236 ) -> crate::Result<()> {
237     // TODO: improve error message
238     let reader = fs::File::open(&input_file_path)?;
240     // Output path is used by single file formats
241     let output_path =
242         if let Some(output_folder) = output_folder { output_folder.join(file_name) } else { file_name.to_path_buf() };
244     // Output folder is used by archive file formats (zip and tar)
245     let output_folder = output_folder.unwrap_or_else(|| Path::new("."));
247     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
248     // from decoder chaining.
249     //
250     // This is the only case where we can read and unpack it directly, without having to do
251     // in-memory decompression/copying first.
252     //
253     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
254     if let [Zip] = *formats.as_slice() {
255         utils::create_dir_if_non_existent(output_folder)?;
256         let zip_archive = zip::ZipArchive::new(reader)?;
257         let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?;
258         info!("Successfully uncompressed bundle in '{}'.", to_utf(output_folder));
259         return Ok(());
260     }
262     // Will be used in decoder chaining
263     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
264     let mut reader: Box<dyn Read> = Box::new(reader);
266     // Grab previous decoder and wrap it inside of a new one
267     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
268         let decoder: Box<dyn Read> = match format {
269             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
270             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
271             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
272             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
273             _ => unreachable!(),
274         };
275         Ok(decoder)
276     };
278     for format in formats.iter().skip(1).rev() {
279         reader = chain_reader_decoder(format, reader)?;
280     }
282     match formats[0] {
283         Gzip | Bzip | Lzma | Zstd => {
284             reader = chain_reader_decoder(&formats[0], reader)?;
286             // TODO: improve error treatment
287             let mut writer = fs::File::create(&output_path)?;
289             io::copy(&mut reader, &mut writer)?;
290             info!("Successfully uncompressed bundle in '{}'.", to_utf(output_path));
291         }
292         Tar => {
293             utils::create_dir_if_non_existent(output_folder)?;
294             let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?;
295             info!("Successfully uncompressed bundle in '{}'.", to_utf(output_folder));
296         }
297         Zip => {
298             utils::create_dir_if_non_existent(output_folder)?;
300             eprintln!("Compressing first into .zip.");
301             eprintln!("Warning: .zip archives with extra extensions have a downside.");
302             eprintln!(
303                 "The only way is loading everything into the RAM while compressing, and then write everything down."
304             );
305             eprintln!("this means that by compressing .zip with extra compression formats, you can run out of RAM if the file is too large!");
307             let mut vec = vec![];
308             io::copy(&mut reader, &mut vec)?;
309             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
311             let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?;
313             info!("Successfully uncompressed bundle in '{}'.", to_utf(output_folder));
314         }
315     }
317     Ok(())