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 // Used in BufReader and BufWriter to perform less syscalls
33 const BUFFER_CAPACITY: usize = 1024 * 64;
35 fn represents_several_files(files: &[PathBuf]) -> bool {
36 let is_non_empty_dir = |path: &PathBuf| {
37 let is_non_empty = || !dir_is_empty(path);
39 path.is_dir().then(is_non_empty).unwrap_or_default()
42 files.iter().any(is_non_empty_dir) || files.len() > 1
45 /// Entrypoint of ouch, receives cli options and matches Subcommand to decide what to do
48 question_policy: QuestionPolicy,
49 file_visibility_policy: FileVisibilityPolicy,
50 ) -> crate::Result<()> {
52 Subcommand::Compress { mut files, output: output_path } => {
53 // 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)
54 if output_path.exists() {
55 clean_input_files_if_needed(&mut files, &fs::canonicalize(&output_path)?);
57 // After cleaning, if there are no input files left, exit
59 return Err(FinalError::with_title("No files to compress").into());
62 // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
63 let mut formats = extension::extensions_from_path(&output_path);
65 if formats.is_empty() {
66 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
67 .detail("You shall supply the compression format")
68 .hint("Try adding supported extensions (see --help):")
69 .hint(format!(" ouch compress <FILES>... {}.tar.gz", to_utf(&output_path)))
70 .hint(format!(" ouch compress <FILES>... {}.zip", to_utf(&output_path)))
72 .hint("Alternatively, you can overwrite this option by using the '--format' flag:")
73 .hint(format!(" ouch compress <FILES>... {} --format tar.gz", to_utf(&output_path)));
75 return Err(error.into());
78 if !formats.get(0).map(Extension::is_archive).unwrap_or(false) && represents_several_files(&files) {
79 // This piece of code creates a suggestion for compressing multiple files
81 // Change from file.bz.xz
83 let extensions_text: String = formats.iter().map(|format| format.to_string()).collect();
85 let output_path = to_utf(output_path);
87 // Breaks if Lzma is .lz or .lzma and not .xz
88 // Or if Bzip is .bz2 and not .bz
89 let extensions_start_position = output_path.rfind(&extensions_text).unwrap();
90 let pos = extensions_start_position - 1;
91 let mut suggested_output_path = output_path.clone();
92 suggested_output_path.insert_str(pos, ".tar");
94 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
95 .detail("You are trying to compress multiple files.")
96 .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
97 .detail("The only supported formats that archive files into an archive are .tar and .zip.")
98 .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
99 .hint(format!("From: {}", output_path))
100 .hint(format!("To: {}", suggested_output_path));
102 return Err(error.into());
105 if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
106 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
107 .detail(format!("Found the format '{}' in an incorrect position.", format))
108 .detail(format!("'{}' can only be used at the start of the file extension.", format))
109 .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format))
110 .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
112 return Err(error.into());
115 if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
116 // User does not want to overwrite this file, skip and return without any errors
120 let output_file = fs::File::create(&output_path)?;
122 if !represents_several_files(&files) {
123 // It's possible the file is already partially compressed so we don't want to compress it again
124 // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz`
125 let input_extensions = extension::extensions_from_path(&files[0]);
127 // 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
128 let mut new_formats = Vec::with_capacity(formats.len());
129 for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) {
130 if inp_ext.compression_formats == out_ext.compression_formats {
131 new_formats.push(out_ext.clone());
135 .zip(out_ext.compression_formats.iter())
136 .all(|(inp, out)| inp == out)
138 let new_ext = Extension::new(
139 &out_ext.compression_formats[..inp_ext.compression_formats.len()],
140 &out_ext.display_text,
142 new_formats.push(new_ext);
146 // If the input is a sublist at the start of `formats` then remove the extensions
147 // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want
148 if !input_extensions.is_empty() && new_formats != formats {
150 // We checked above that input_extensions isn't empty, so files[0] has an extension.
152 // Path::extension says: "if there is no file_name, then there is no extension".
153 // Contrapositive statement: "if there is extension, then there is file_name".
155 accessible, // important information
156 "Partial compression detected. Compressing {} into {}",
157 to_utf(files[0].as_path().file_name().unwrap()),
160 formats = new_formats;
163 let compress_result =
164 compress_files(files, formats, output_file, &output_path, question_policy, file_visibility_policy);
166 if let Ok(true) = compress_result {
167 // this is only printed once, so it doesn't result in much text. On the other hand,
168 // having a final status message is important especially in an accessibility context
169 // as screen readers may not read a commands exit code, making it hard to reason
170 // about whether the command succeeded without such a message
171 info!(accessible, "Successfully compressed '{}'.", to_utf(output_path));
173 // If Ok(false) or Err() occurred, delete incomplete file
174 // Print an extra alert message pointing out that we left a possibly
175 // CORRUPTED FILE at `output_path`
176 if let Err(err) = fs::remove_file(&output_path) {
177 eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
178 eprintln!(" Please manually delete '{}'.", to_utf(&output_path));
179 eprintln!(" Compression failed and we could not delete '{}'.", to_utf(&output_path),);
180 eprintln!(" Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED);
186 Subcommand::Decompress { files, output_dir } => {
187 let mut output_paths = vec![];
188 let mut formats = vec![];
190 for path in files.iter() {
191 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
192 output_paths.push(file_output_path);
193 formats.push(file_formats);
196 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
200 let files_missing_format: Vec<PathBuf> = files
203 .filter(|(_, formats)| formats.is_empty())
204 .map(|(input_path, _)| PathBuf::from(input_path))
207 if !files_missing_format.is_empty() {
208 let error = FinalError::with_title("Cannot decompress files without extensions")
210 "Files without supported extensions: {}",
211 concatenate_os_str_list(&files_missing_format)
213 .detail("Decompression formats are detected automatically by the file extension")
214 .hint("Provide a file with a supported extension:")
215 .hint(" ouch decompress example.tar.gz")
217 .hint("Or overwrite this option with the '--format' flag:")
218 .hint(format!(" ouch decompress {} --format tar.gz", to_utf(&files_missing_format[0])));
220 return Err(error.into());
223 // The directory that will contain the output files
224 // We default to the current directory if the user didn't specify an output directory with --dir
225 let output_dir = if let Some(dir) = output_dir {
226 if !utils::clear_path(&dir, question_policy)? {
227 // User doesn't want to overwrite
230 utils::create_dir_if_non_existent(&dir)?;
236 for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
237 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
238 decompress_file(input_path, formats, &output_dir, output_file_path, question_policy)?;
241 Subcommand::List { archives: files, tree } => {
242 let mut formats = vec![];
244 for path in files.iter() {
245 let (_, file_formats) = extension::separate_known_extensions_from_name(path);
246 formats.push(file_formats);
249 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
253 let not_archives: Vec<PathBuf> = files
256 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
257 .map(|(path, _)| path.clone())
260 if !not_archives.is_empty() {
261 let error = FinalError::with_title("Cannot list archive contents")
262 .detail("Only archives can have their contents listed")
263 .detail(format!("Files are not archives: {}", concatenate_os_str_list(¬_archives)));
265 return Err(error.into());
268 let list_options = ListOptions { tree };
270 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
274 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
275 list_archive_contents(archive_path, formats, list_options, question_policy)?;
282 // Compress files into an `output_file`
284 // files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
285 // formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
286 // output_file is the resulting compressed file name, example: "compressed.tar.gz"
288 // Returns Ok(true) if compressed all files successfully, and Ok(false) if user opted to skip files
291 formats: Vec<Extension>,
292 output_file: fs::File,
294 question_policy: QuestionPolicy,
295 file_visibility_policy: FileVisibilityPolicy,
296 ) -> crate::Result<bool> {
297 // The next lines are for displaying the progress bar
298 // If the input files contain a directory, then the total size will be underestimated
299 let (total_input_size, precise) = files
301 .map(|f| (f.metadata().expect("file exists").len(), f.is_file()))
302 .fold((0, true), |(total_size, and_precise), (size, precise)| (total_size + size, and_precise & precise));
303 //NOTE: canonicalize is here to avoid a weird bug:
304 // > If output_file_path is a nested path and it exists and the user overwrite it
305 // >> output_file_path.exists() will always return false (somehow)
306 // - canonicalize seems to fix this
307 let output_file_path = output_file.path().canonicalize()?;
309 let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
311 let mut writer: Box<dyn Write> = Box::new(file_writer);
313 // Grab previous encoder and wrap it inside of a new one
314 let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| -> crate::Result<Box<dyn Write>> {
315 let encoder: Box<dyn Write> = match format {
316 Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
317 Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
318 Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?),
319 Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
320 Snappy => Box::new(snap::write::FrameEncoder::new(encoder)),
322 let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
324 // Encoder::new() can only fail if `level` is invalid, but Default::default()
325 // is guaranteed to be valid
326 Box::new(zstd_encoder.unwrap().auto_finish())
328 Tar | Zip => unreachable!(),
333 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
334 writer = chain_writer_encoder(format, writer)?;
337 match formats[0].compression_formats[0] {
338 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
339 let _progress = Progress::new_accessible_aware(
342 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
345 writer = chain_writer_encoder(&formats[0].compression_formats[0], writer)?;
346 let mut reader = fs::File::open(&files[0]).unwrap();
347 io::copy(&mut reader, &mut writer)?;
350 let mut progress = Progress::new_accessible_aware(
353 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
356 archive::tar::build_archive_from_paths(
359 file_visibility_policy,
360 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
365 if formats.len() > 1 {
366 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
368 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
369 \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
370 \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
373 // give user the option to continue compressing after warning is shown
374 if !user_wants_to_continue(output_dir, question_policy, QuestionAction::Compression)? {
379 let mut vec_buffer = io::Cursor::new(vec![]);
381 let current_position_fn = {
382 let vec_buffer_ptr = {
383 struct FlyPtr(*const io::Cursor<Vec<u8>>);
384 unsafe impl Send for FlyPtr {}
385 FlyPtr(&vec_buffer as *const _)
388 let vec_buffer_ptr = &vec_buffer_ptr;
389 // Safety: ptr is valid and vec_buffer is still alive
390 unsafe { &*vec_buffer_ptr.0 }.position()
394 let mut progress = Progress::new_accessible_aware(total_input_size, precise, Some(current_position_fn));
396 archive::zip::build_archive_from_paths(
399 file_visibility_policy,
400 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
402 let vec_buffer = vec_buffer.into_inner();
403 io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
412 // File at input_file_path is opened for reading, example: "archive.tar.gz"
413 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
414 // output_dir it's where the file will be decompressed to, this function assumes that the directory exists
415 // output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
417 input_file_path: &Path,
418 formats: Vec<Extension>,
420 output_file_path: PathBuf,
421 question_policy: QuestionPolicy,
422 ) -> crate::Result<()> {
423 assert!(output_dir.exists());
424 let total_input_size = input_file_path.metadata().expect("file exists").len();
425 let reader = fs::File::open(&input_file_path)?;
426 // Zip archives are special, because they require io::Seek, so it requires it's logic separated
427 // from decoder chaining.
429 // This is the only case where we can read and unpack it directly, without having to do
430 // in-memory decompression/copying first.
432 // Any other Zip decompression done can take up the whole RAM and freeze ouch.
433 if formats.len() == 1 && *formats[0].compression_formats == [Zip] {
434 let zip_archive = zip::ZipArchive::new(reader)?;
435 let files = if let ControlFlow::Continue(files) = smart_unpack(
436 Box::new(move |output_dir| {
437 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
438 crate::archive::zip::unpack_archive(
441 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
453 // this is only printed once, so it doesn't result in much text. On the other hand,
454 // having a final status message is important especially in an accessibility context
455 // as screen readers may not read a commands exit code, making it hard to reason
456 // about whether the command succeeded without such a message
459 "Successfully decompressed archive in {} ({} files).",
460 nice_directory_display(output_dir),
467 // Will be used in decoder chaining
468 let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
469 let mut reader: Box<dyn Read> = Box::new(reader);
471 // Grab previous decoder and wrap it inside of a new one
472 let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
473 let decoder: Box<dyn Read> = match format {
474 Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
475 Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
476 Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
477 Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
478 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
479 Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
480 Tar | Zip => unreachable!(),
485 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
486 reader = chain_reader_decoder(format, reader)?;
490 match formats[0].compression_formats[0] {
491 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
492 reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
494 let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
495 if writer.is_none() {
496 // Means that the user doesn't want to overwrite
499 let mut writer = writer.unwrap();
501 let current_position_fn = Box::new({
502 let output_file_path = output_file_path.clone();
503 move || output_file_path.clone().metadata().expect("file exists").len()
505 let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
507 io::copy(&mut reader, &mut writer)?;
508 files_unpacked = vec![output_file_path];
511 files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
512 Box::new(move |output_dir| {
513 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
514 crate::archive::tar::unpack_archive(
517 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
530 if formats.len() > 1 {
531 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
533 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
534 \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
535 \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
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);
632 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
633 \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
634 \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
637 // give user the option to continue decompressing after warning is shown
638 if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? {
643 let mut vec = vec![];
644 io::copy(&mut reader, &mut vec)?;
645 let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
647 Box::new(crate::archive::zip::list_archive(zip_archive))
649 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
650 panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
653 list::list_files(archive_path, files, list_options)?;
657 /// Unpacks an archive with some heuristics
658 /// - If the archive contains only one file, it will be extracted to the `output_dir`
659 /// - 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`)
660 /// Note: This functions assumes that `output_dir` exists
662 unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
664 output_file_path: &Path,
665 question_policy: QuestionPolicy,
666 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
667 assert!(output_dir.exists());
668 let temp_dir = tempfile::tempdir_in(output_dir)?;
669 let temp_dir_path = temp_dir.path();
672 "Created temporary directory {} to hold decompressed elements.",
673 nice_directory_display(temp_dir_path)
677 let files = unpack_fn(temp_dir_path)?;
679 let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
680 if root_contains_only_one_element {
681 // Only one file in the root directory, so we can just move it to the output directory
682 let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
683 let file_path = file.path();
685 file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
686 let correct_path = output_dir.join(file_name);
687 // One case to handle tough is we need to check if a file with the same name already exists
688 if !utils::clear_path(&correct_path, question_policy)? {
689 return Ok(ControlFlow::Break(()));
691 fs::rename(&file_path, &correct_path)?;
694 "Successfully moved {} to {}.",
695 nice_directory_display(&file_path),
696 nice_directory_display(&correct_path)
699 // Multiple files in the root directory, so:
700 // Rename the temporary directory to the archive name, which is output_file_path
701 // One case to handle tough is we need to check if a file with the same name already exists
702 if !utils::clear_path(output_file_path, question_policy)? {
703 return Ok(ControlFlow::Break(()));
705 fs::rename(&temp_dir_path, &output_file_path)?;
708 "Successfully moved {} to {}.",
709 nice_directory_display(&temp_dir_path),
710 nice_directory_display(&output_file_path)
713 Ok(ControlFlow::Continue(files))
718 formats: &mut Vec<Vec<Extension>>,
719 question_policy: QuestionPolicy,
720 ) -> crate::Result<ControlFlow<()>> {
721 for (path, format) in files.iter().zip(formats.iter_mut()) {
722 if format.is_empty() {
723 // File with no extension
724 // Try to detect it automatically and prompt the user about it
725 if let Some(detected_format) = try_infer_extension(path) {
726 // Infering the file extension can have unpredicted consequences (e.g. the user just
727 // mistyped, ...) which we should always inform the user about.
728 info!(accessible, "Detected file: `{}` extension as `{}`", path.display(), detected_format);
729 if user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
730 format.push(detected_format);
732 return Ok(ControlFlow::Break(()));
735 } else if let Some(detected_format) = try_infer_extension(path) {
736 // File ending with extension
737 // Try to detect the extension and warn the user if it differs from the written one
738 let outer_ext = format.iter().next_back().unwrap();
739 if outer_ext != &detected_format {
741 "The file extension: `{}` differ from the detected extension: `{}`",
745 if !user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
746 return Ok(ControlFlow::Break(()));
750 // NOTE: If this actually produces no false positives, we can upgrade it in the future
751 // to a warning and ask the user if he wants to continue decompressing.
752 info!(accessible, "Could not detect the extension of `{}`", path.display());
755 Ok(ControlFlow::Continue(()))
758 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
760 while idx < files.len() {
761 if files[idx] == output_path {
762 warning!("The output file and the input file are the same: `{}`, skipping...", output_path.display());