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