1 //! Core of the crate, where the `compress_files` and `decompress_file` functions are implemented
3 //! Also, where correctly call functions based on the detected `Command`.
7 io::{self, BufReader, BufWriter, Read, Write},
19 CompressionFormat::{self, *},
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<()> {
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.")
41 .hint(format!(" ouch compress ... {}.tar.gz", to_utf(&output_path)))
42 .hint(format!(" ouch compress ... {}.zip", to_utf(&output_path)))
45 return Err(Error::with_reason(reason));
48 if matches!(&formats[0], Bzip | Gzip | Lzma) && files.len() > 1 {
49 // This piece of code creates a sugestion for compressing multiple files
51 // Change from file.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))
74 return Err(Error::with_reason(reason));
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)))
85 return Err(Error::with_reason(reason));
88 if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? {
89 // User does not want to overwrite this file
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());
107 info!("Successfully compressed '{}'.", to_utf(output_path));
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);
122 let files_missing_format: Vec<PathBuf> = files
125 .filter(|(_, formats)| formats.is_empty())
126 .map(|(input_path, _)| PathBuf::from(input_path))
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]));
134 "Dev note: add this error variant and pass the Vec to it, all the files \
135 lacking extension shall be shown: {:#?}.",
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)?;
147 Command::ShowHelp => crate::help_command(),
148 Command::ShowVersion => crate::version_command(),
155 formats: Vec<CompressionFormat>,
156 output_file: fs::File,
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,
168 let mut bufwriter = build_archive_from_paths(&files, file_writer)?;
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)),
180 let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
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())
191 for format in formats.iter().skip(1).rev() {
192 writer = chain_writer_encoder(format, writer);
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)?;
202 let mut writer = archive::tar::build_archive_from_paths(&files, writer)?;
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!");
210 "\tThis is a limitation for formats like '{}'.",
211 formats.iter().map(|format| format.to_string()).collect::<String>()
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)?;
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
231 input_file_path: &Path,
232 formats: Vec<extension::CompressionFormat>,
233 output_folder: Option<&Path>,
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
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.
250 // This is the only case where we can read and unpack it directly, without having to do
251 // in-memory decompression/copying first.
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));
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)?),
278 for format in formats.iter().skip(1).rev() {
279 reader = chain_reader_decoder(format, reader)?;
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));
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));
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.");
303 "The only way is loading everything into the RAM while compressing, and then write everything down."
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));