3 io::{self, BufReader, BufWriter, Read, Write},
15 CompressionFormat::{self, *},
21 pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
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.")
33 .hint(format!(" ouch compress ... {}.tar.gz", to_utf(&output_path)))
34 .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path)))
38 if matches!(&formats[0], Bzip | Gzip | Lzma) && files.len() > 1 {
39 // This piece of code creates a sugestion for compressing multiple files
41 // Change from file.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))
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)))
74 if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? {
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))
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());
97 info!("Successfully compressed '{}'.", to_utf(output_path));
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);
112 let files_missing_format: Vec<PathBuf> = files
115 .filter(|(_, formats)| formats.is_empty())
116 .map(|(input_path, _)| PathBuf::from(input_path))
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]));
124 "Dev note: add this error variant and pass the Vec to it, all the files \
125 lacking extension shall be shown: {:#?}.",
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)?;
137 Command::ShowHelp => crate::help_command(),
138 Command::ShowVersion => crate::version_command(),
145 formats: Vec<CompressionFormat>,
146 output_file: fs::File,
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,
158 let mut bufwriter = build_archive_from_paths(&files, file_writer)?;
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)),
174 for format in formats.iter().skip(1).rev() {
175 writer = chain_writer_encoder(format, writer);
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)?;
185 let mut writer = archive::tar::build_archive_from_paths(&files, writer)?;
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!");
193 "\tThis is a limitation for formats like '{}'.",
194 formats.iter().map(|format| format.to_string()).collect::<String>()
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)?;
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
214 input_file_path: &Path,
215 formats: Vec<extension::CompressionFormat>,
216 output_folder: Option<&Path>,
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
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.
233 // This is the only case where we can read and unpack it directly, without having to do
234 // in-memory decompression/copying first.
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));
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)),
260 for format in formats.iter().skip(1).rev() {
261 reader = chain_reader_decoder(format, reader);
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));
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));
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.");
285 "The only way is loading everything into the RAM while compressing, and then write everything down."
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));