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`.
6 io::{self, BufReader, BufWriter, Read, Write},
19 CompressionFormat::{self, *},
23 list::{self, FileInArchive, ListOptions},
26 self, concatenate_os_str_list, dir_is_empty, nice_directory_display, to_utf, try_infer_extension,
27 user_wants_to_continue, FileVisibilityPolicy,
29 warning, Opts, QuestionAction, QuestionPolicy, Subcommand,
32 // Message used to advice the user that .zip archives have limitations that require it to load everything into memory at once
33 // and this can lead to out-of-memory scenarios for archives that are big enough.
34 const ZIP_IN_MEMORY_LIMITATION_WARNING: &str =
35 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)
36 \tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.
37 \tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!";
39 // Used in BufReader and BufWriter to perform less syscalls
40 const BUFFER_CAPACITY: usize = 1024 * 64;
42 fn represents_several_files(files: &[PathBuf]) -> bool {
43 let is_non_empty_dir = |path: &PathBuf| {
44 let is_non_empty = || !dir_is_empty(path);
46 path.is_dir().then(is_non_empty).unwrap_or_default()
49 files.iter().any(is_non_empty_dir) || files.len() > 1
52 /// Entrypoint of ouch, receives cli options and matches Subcommand to decide what to do
55 question_policy: QuestionPolicy,
56 file_visibility_policy: FileVisibilityPolicy,
57 ) -> crate::Result<()> {
59 Subcommand::Compress { mut files, output: output_path } => {
60 // If the output_path file exists and is the same as some of the input files, warn the user and skip those inputs (in order to avoid compression recursion)
61 if output_path.exists() {
62 clean_input_files_if_needed(&mut files, &fs::canonicalize(&output_path)?);
64 // After cleaning, if there are no input files left, exit
66 return Err(FinalError::with_title("No files to compress").into());
69 // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
70 let mut formats = extension::extensions_from_path(&output_path);
72 if formats.is_empty() {
73 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
74 .detail("You shall supply the compression format")
75 .hint("Try adding supported extensions (see --help):")
76 .hint(format!(" ouch compress <FILES>... {}.tar.gz", to_utf(&output_path)))
77 .hint(format!(" ouch compress <FILES>... {}.zip", to_utf(&output_path)))
79 .hint("Alternatively, you can overwrite this option by using the '--format' flag:")
80 .hint(format!(" ouch compress <FILES>... {} --format tar.gz", to_utf(&output_path)));
82 return Err(error.into());
85 if !formats.get(0).map(Extension::is_archive).unwrap_or(false) && represents_several_files(&files) {
86 // This piece of code creates a suggestion for compressing multiple files
88 // Change from file.bz.xz
90 let extensions_text: String = formats.iter().map(|format| format.to_string()).collect();
92 let output_path = to_utf(&output_path).to_string();
94 // Breaks if Lzma is .lz or .lzma and not .xz
95 // Or if Bzip is .bz2 and not .bz
96 let extensions_start_position = output_path.rfind(&extensions_text).unwrap();
97 let pos = extensions_start_position - 1;
98 let mut suggested_output_path = output_path.to_string();
99 suggested_output_path.insert_str(pos, ".tar");
101 let error = FinalError::with_title(format!("Cannot compress to '{}'.", output_path))
102 .detail("You are trying to compress multiple files.")
103 .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
104 .detail("The only supported formats that archive files into an archive are .tar and .zip.")
105 .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
106 .hint(format!("From: {}", output_path))
107 .hint(format!("To: {}", suggested_output_path));
109 return Err(error.into());
112 if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
113 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
114 .detail(format!("Found the format '{}' in an incorrect position.", format))
115 .detail(format!("'{}' can only be used at the start of the file extension.", format))
116 .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format))
117 .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
119 return Err(error.into());
122 if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
123 // User does not want to overwrite this file, skip and return without any errors
127 let output_file = fs::File::create(&output_path)?;
129 if !represents_several_files(&files) {
130 // It is possible the file is already partially compressed so we don't want to compress it again
131 // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz`
132 let input_extensions = extension::extensions_from_path(&files[0]);
134 // We calculate the formats that are left if we filter out a sublist at the start of what we have that's the same as the input formats
135 let mut new_formats = Vec::with_capacity(formats.len());
136 for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) {
137 if inp_ext.compression_formats == out_ext.compression_formats {
138 new_formats.push(out_ext.clone());
142 .zip(out_ext.compression_formats.iter())
143 .all(|(inp, out)| inp == out)
145 let new_ext = Extension::new(
146 &out_ext.compression_formats[..inp_ext.compression_formats.len()],
147 &out_ext.display_text,
149 new_formats.push(new_ext);
153 // If the input is a sublist at the start of `formats` then remove the extensions
154 // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want
155 if !input_extensions.is_empty() && new_formats != formats {
157 // We checked above that input_extensions isn't empty, so files[0] has an extension.
159 // Path::extension says: "if there is no file_name, then there is no extension".
160 // Contrapositive statement: "if there is extension, then there is file_name".
162 accessible, // important information
163 "Partial compression detected. Compressing {} into {}",
164 to_utf(files[0].as_path().file_name().unwrap().as_ref()),
167 formats = new_formats;
170 let compress_result =
171 compress_files(files, formats, output_file, &output_path, question_policy, file_visibility_policy);
173 if let Ok(true) = compress_result {
174 // this is only printed once, so it doesn't result in much text. On the other hand,
175 // having a final status message is important especially in an accessibility context
176 // as screen readers may not read a commands exit code, making it hard to reason
177 // about whether the command succeeded without such a message
178 info!(accessible, "Successfully compressed '{}'.", to_utf(&output_path));
180 // If Ok(false) or Err() occurred, delete incomplete file
181 // Print an extra alert message pointing out that we left a possibly
182 // CORRUPTED FILE at `output_path`
183 if let Err(err) = fs::remove_file(&output_path) {
184 eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
185 eprintln!(" Please manually delete '{}'.", to_utf(&output_path));
186 eprintln!(" Compression failed and we could not delete '{}'.", to_utf(&output_path),);
187 eprintln!(" Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED);
193 Subcommand::Decompress { files, output_dir } => {
194 let mut output_paths = vec![];
195 let mut formats = vec![];
197 for path in files.iter() {
198 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
199 output_paths.push(file_output_path);
200 formats.push(file_formats);
203 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
207 let files_missing_format: Vec<PathBuf> = files
210 .filter(|(_, formats)| formats.is_empty())
211 .map(|(input_path, _)| PathBuf::from(input_path))
214 if !files_missing_format.is_empty() {
215 let error = FinalError::with_title("Cannot decompress files without extensions")
217 "Files without supported extensions: {}",
218 concatenate_os_str_list(&files_missing_format)
220 .detail("Decompression formats are detected automatically by the file extension")
221 .hint("Provide a file with a supported extension:")
222 .hint(" ouch decompress example.tar.gz")
224 .hint("Or overwrite this option with the '--format' flag:")
225 .hint(format!(" ouch decompress {} --format tar.gz", to_utf(&files_missing_format[0])));
227 return Err(error.into());
230 // The directory that will contain the output files
231 // We default to the current directory if the user didn't specify an output directory with --dir
232 let output_dir = if let Some(dir) = output_dir {
233 if !utils::clear_path(&dir, question_policy)? {
234 // User doesn't want to overwrite
237 utils::create_dir_if_non_existent(&dir)?;
243 for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
244 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
245 decompress_file(input_path, formats, &output_dir, output_file_path, question_policy)?;
248 Subcommand::List { archives: files, tree } => {
249 let mut formats = vec![];
251 for path in files.iter() {
252 let (_, file_formats) = extension::separate_known_extensions_from_name(path);
253 formats.push(file_formats);
256 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
260 let not_archives: Vec<PathBuf> = files
263 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
264 .map(|(path, _)| path.clone())
267 if !not_archives.is_empty() {
268 let error = FinalError::with_title("Cannot list archive contents")
269 .detail("Only archives can have their contents listed")
270 .detail(format!("Files are not archives: {}", concatenate_os_str_list(¬_archives)));
272 return Err(error.into());
275 let list_options = ListOptions { tree };
277 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
281 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
282 list_archive_contents(archive_path, formats, list_options, question_policy)?;
289 // Compress files into an `output_file`
291 // files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
292 // formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
293 // output_file is the resulting compressed file name, example: "compressed.tar.gz"
295 // Returns Ok(true) if compressed all files successfully, and Ok(false) if user opted to skip files
298 formats: Vec<Extension>,
299 output_file: fs::File,
301 question_policy: QuestionPolicy,
302 file_visibility_policy: FileVisibilityPolicy,
303 ) -> crate::Result<bool> {
304 // The next lines are for displaying the progress bar
305 // If the input files contain a directory, then the total size will be underestimated
306 let (total_input_size, precise) = files
308 .map(|f| (f.metadata().expect("file exists").len(), f.is_file()))
309 .fold((0, true), |(total_size, and_precise), (size, precise)| (total_size + size, and_precise & precise));
311 // NOTE: canonicalize is here to avoid a weird bug:
312 // > If output_file_path is a nested path and it exists and the user overwrite it
313 // >> output_file_path.exists() will always return false (somehow)
314 // - canonicalize seems to fix this
315 let output_file_path = output_file.path().canonicalize()?;
317 let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
319 let mut writer: Box<dyn Write> = Box::new(file_writer);
321 // Grab previous encoder and wrap it inside of a new one
322 let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| -> crate::Result<Box<dyn Write>> {
323 let encoder: Box<dyn Write> = match format {
324 Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
325 Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
326 Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?),
327 Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
328 Snappy => Box::new(snap::write::FrameEncoder::new(encoder)),
330 let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
332 // Encoder::new() can only fail if `level` is invalid, but Default::default()
333 // is guaranteed to be valid
334 Box::new(zstd_encoder.unwrap().auto_finish())
336 Tar | Zip => unreachable!(),
341 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
342 writer = chain_writer_encoder(format, writer)?;
345 match formats[0].compression_formats[0] {
346 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
347 let _progress = Progress::new_accessible_aware(
350 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
353 writer = chain_writer_encoder(&formats[0].compression_formats[0], writer)?;
354 let mut reader = fs::File::open(&files[0]).unwrap();
355 io::copy(&mut reader, &mut writer)?;
358 let mut progress = Progress::new_accessible_aware(
361 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
364 archive::tar::build_archive_from_paths(
367 file_visibility_policy,
368 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
373 if formats.len() > 1 {
374 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
375 eprintln!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
377 // give user the option to continue compressing after warning is shown
378 if !user_wants_to_continue(output_dir, question_policy, QuestionAction::Compression)? {
383 let mut vec_buffer = io::Cursor::new(vec![]);
385 let current_position_fn = {
386 let vec_buffer_ptr = {
387 struct FlyPtr(*const io::Cursor<Vec<u8>>);
388 unsafe impl Send for FlyPtr {}
389 FlyPtr(&vec_buffer as *const _)
392 let vec_buffer_ptr = &vec_buffer_ptr;
393 // Safety: ptr is valid and vec_buffer is still alive
394 unsafe { &*vec_buffer_ptr.0 }.position()
398 let mut progress = Progress::new_accessible_aware(total_input_size, precise, Some(current_position_fn));
400 archive::zip::build_archive_from_paths(
403 file_visibility_policy,
404 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
406 let vec_buffer = vec_buffer.into_inner();
407 io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
416 // File at input_file_path is opened for reading, example: "archive.tar.gz"
417 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
418 // output_dir it's where the file will be decompressed to, this function assumes that the directory exists
419 // output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
421 input_file_path: &Path,
422 formats: Vec<Extension>,
424 output_file_path: PathBuf,
425 question_policy: QuestionPolicy,
426 ) -> crate::Result<()> {
427 assert!(output_dir.exists());
428 let total_input_size = input_file_path.metadata().expect("file exists").len();
429 let reader = fs::File::open(&input_file_path)?;
430 // Zip archives are special, because they require io::Seek, so it requires it's logic separated
431 // from decoder chaining.
433 // This is the only case where we can read and unpack it directly, without having to do
434 // in-memory decompression/copying first.
436 // Any other Zip decompression done can take up the whole RAM and freeze ouch.
437 if formats.len() == 1 && *formats[0].compression_formats == [Zip] {
438 let zip_archive = zip::ZipArchive::new(reader)?;
439 let files = if let ControlFlow::Continue(files) = smart_unpack(
440 Box::new(move |output_dir| {
441 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
442 crate::archive::zip::unpack_archive(
445 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
457 // this is only printed once, so it doesn't result in much text. On the other hand,
458 // having a final status message is important especially in an accessibility context
459 // as screen readers may not read a commands exit code, making it hard to reason
460 // about whether the command succeeded without such a message
463 "Successfully decompressed archive in {} ({} files).",
464 nice_directory_display(output_dir),
471 // Will be used in decoder chaining
472 let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
473 let mut reader: Box<dyn Read> = Box::new(reader);
475 // Grab previous decoder and wrap it inside of a new one
476 let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
477 let decoder: Box<dyn Read> = match format {
478 Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
479 Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
480 Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
481 Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
482 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
483 Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
484 Tar | Zip => unreachable!(),
489 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
490 reader = chain_reader_decoder(format, reader)?;
494 match formats[0].compression_formats[0] {
495 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
496 reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
498 let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
499 if writer.is_none() {
500 // Means that the user doesn't want to overwrite
503 let mut writer = writer.unwrap();
505 let current_position_fn = Box::new({
506 let output_file_path = output_file_path.clone();
507 move || output_file_path.clone().metadata().expect("file exists").len()
509 let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
511 io::copy(&mut reader, &mut writer)?;
512 files_unpacked = vec![output_file_path];
515 files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
516 Box::new(move |output_dir| {
517 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
518 crate::archive::tar::unpack_archive(
521 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
534 if formats.len() > 1 {
535 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
536 eprintln!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
538 // give user the option to continue decompressing after warning is shown
539 if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? {
544 let mut vec = vec![];
545 io::copy(&mut reader, &mut vec)?;
546 let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
548 files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
549 Box::new(move |output_dir| {
550 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
551 crate::archive::zip::unpack_archive(
554 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
568 // this is only printed once, so it doesn't result in much text. On the other hand,
569 // having a final status message is important especially in an accessibility context
570 // as screen readers may not read a commands exit code, making it hard to reason
571 // about whether the command succeeded without such a message
572 info!(accessible, "Successfully decompressed archive in {}.", nice_directory_display(output_dir));
573 info!(accessible, "Files unpacked: {}", files_unpacked.len());
578 // File at input_file_path is opened for reading, example: "archive.tar.gz"
579 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
580 fn list_archive_contents(
582 formats: Vec<CompressionFormat>,
583 list_options: ListOptions,
584 question_policy: QuestionPolicy,
585 ) -> crate::Result<()> {
586 let reader = fs::File::open(&archive_path)?;
588 // Zip archives are special, because they require io::Seek, so it requires it's logic separated
589 // from decoder chaining.
591 // This is the only case where we can read and unpack it directly, without having to do
592 // in-memory decompression/copying first.
594 // Any other Zip decompression done can take up the whole RAM and freeze ouch.
595 if let [Zip] = *formats.as_slice() {
596 let zip_archive = zip::ZipArchive::new(reader)?;
597 let files = crate::archive::zip::list_archive(zip_archive);
598 list::list_files(archive_path, files, list_options)?;
603 // Will be used in decoder chaining
604 let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
605 let mut reader: Box<dyn Read + Send> = Box::new(reader);
607 // Grab previous decoder and wrap it inside of a new one
608 let chain_reader_decoder =
609 |format: &CompressionFormat, decoder: Box<dyn Read + Send>| -> crate::Result<Box<dyn Read + Send>> {
610 let decoder: Box<dyn Read + Send> = match format {
611 Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
612 Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
613 Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
614 Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
615 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
616 Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
617 Tar | Zip => unreachable!(),
622 for format in formats.iter().skip(1).rev() {
623 reader = chain_reader_decoder(format, reader)?;
626 let files: Box<dyn Iterator<Item = crate::Result<FileInArchive>>> = match formats[0] {
627 Tar => Box::new(crate::archive::tar::list_archive(tar::Archive::new(reader))),
629 if formats.len() > 1 {
630 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
631 eprintln!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
633 // give user the option to continue decompressing after warning is shown
634 if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? {
639 let mut vec = vec![];
640 io::copy(&mut reader, &mut vec)?;
641 let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
643 Box::new(crate::archive::zip::list_archive(zip_archive))
645 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
646 panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
649 list::list_files(archive_path, files, list_options)?;
653 /// Unpacks an archive with some heuristics
654 /// - If the archive contains only one file, it will be extracted to the `output_dir`
655 /// - If the archive contains multiple files, it will be extracted to a subdirectory of the output_dir named after the archive (given by `output_file_path`)
656 /// Note: This functions assumes that `output_dir` exists
658 unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
660 output_file_path: &Path,
661 question_policy: QuestionPolicy,
662 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
663 assert!(output_dir.exists());
664 let temp_dir = tempfile::tempdir_in(output_dir)?;
665 let temp_dir_path = temp_dir.path();
668 "Created temporary directory {} to hold decompressed elements.",
669 nice_directory_display(temp_dir_path)
673 let files = unpack_fn(temp_dir_path)?;
675 let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
676 if root_contains_only_one_element {
677 // Only one file in the root directory, so we can just move it to the output directory
678 let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
679 let file_path = file.path();
681 file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
682 let correct_path = output_dir.join(file_name);
683 // One case to handle tough is we need to check if a file with the same name already exists
684 if !utils::clear_path(&correct_path, question_policy)? {
685 return Ok(ControlFlow::Break(()));
687 fs::rename(&file_path, &correct_path)?;
690 "Successfully moved {} to {}.",
691 nice_directory_display(&file_path),
692 nice_directory_display(&correct_path)
695 // Multiple files in the root directory, so:
696 // Rename the temporary directory to the archive name, which is output_file_path
697 // One case to handle tough is we need to check if a file with the same name already exists
698 if !utils::clear_path(output_file_path, question_policy)? {
699 return Ok(ControlFlow::Break(()));
701 fs::rename(&temp_dir_path, &output_file_path)?;
704 "Successfully moved {} to {}.",
705 nice_directory_display(temp_dir_path),
706 nice_directory_display(output_file_path)
709 Ok(ControlFlow::Continue(files))
714 formats: &mut Vec<Vec<Extension>>,
715 question_policy: QuestionPolicy,
716 ) -> crate::Result<ControlFlow<()>> {
717 for (path, format) in files.iter().zip(formats.iter_mut()) {
718 if format.is_empty() {
719 // File with no extension
720 // Try to detect it automatically and prompt the user about it
721 if let Some(detected_format) = try_infer_extension(path) {
722 // Infering the file extension can have unpredicted consequences (e.g. the user just
723 // mistyped, ...) which we should always inform the user about.
724 info!(accessible, "Detected file: `{}` extension as `{}`", path.display(), detected_format);
725 if user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
726 format.push(detected_format);
728 return Ok(ControlFlow::Break(()));
731 } else if let Some(detected_format) = try_infer_extension(path) {
732 // File ending with extension
733 // Try to detect the extension and warn the user if it differs from the written one
734 let outer_ext = format.iter().next_back().unwrap();
735 if outer_ext != &detected_format {
737 "The file extension: `{}` differ from the detected extension: `{}`",
741 if !user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
742 return Ok(ControlFlow::Break(()));
746 // NOTE: If this actually produces no false positives, we can upgrade it in the future
747 // to a warning and ask the user if he wants to continue decompressing.
748 info!(accessible, "Could not detect the extension of `{}`", path.display());
751 Ok(ControlFlow::Continue(()))
754 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
756 while idx < files.len() {
757 if files[idx] == output_path {
758 warning!("The output file and the input file are the same: `{}`, skipping...", output_path.display());