Decompress files in parallel
[ouch.git] / src / commands / decompress.rs
blob03b2992417025b18911cc0e6700a26ec66ef5a2a
1 use std::{
2     io::{self, BufReader, Read},
3     ops::ControlFlow,
4     path::{Path, PathBuf},
5 };
7 use fs_err as fs;
9 use crate::{
10     commands::warn_user_about_loading_zip_in_memory,
11     extension::{
12         split_first_compression_format,
13         CompressionFormat::{self, *},
14         Extension,
15     },
16     info,
17     utils::{self, nice_directory_display, user_wants_to_continue},
18     QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
21 // Decompress a file
23 // File at input_file_path is opened for reading, example: "archive.tar.gz"
24 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
25 // output_dir it's where the file will be decompressed to, this function assumes that the directory exists
26 // output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
27 pub fn decompress_file(
28     input_file_path: &Path,
29     formats: Vec<Extension>,
30     output_dir: &Path,
31     output_file_path: PathBuf,
32     question_policy: QuestionPolicy,
33     quiet: bool,
34 ) -> crate::Result<()> {
35     assert!(output_dir.exists());
36     let reader = fs::File::open(input_file_path)?;
38     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
39     // from decoder chaining.
40     //
41     // This is the only case where we can read and unpack it directly, without having to do
42     // in-memory decompression/copying first.
43     //
44     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
45     if let [Extension {
46         compression_formats: [Zip],
47         ..
48     }] = formats.as_slice()
49     {
50         let zip_archive = zip::ZipArchive::new(reader)?;
51         let files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
52             |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, quiet),
53             output_dir,
54             &output_file_path,
55             question_policy,
56         )? {
57             files
58         } else {
59             return Ok(());
60         };
62         // this is only printed once, so it doesn't result in much text. On the other hand,
63         // having a final status message is important especially in an accessibility context
64         // as screen readers may not read a commands exit code, making it hard to reason
65         // about whether the command succeeded without such a message
66         info!(
67             accessible,
68             "Successfully decompressed archive in {} ({} files).",
69             nice_directory_display(output_dir),
70             files_unpacked
71         );
73         return Ok(());
74     }
76     // Will be used in decoder chaining
77     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
78     let mut reader: Box<dyn Read> = Box::new(reader);
80     // Grab previous decoder and wrap it inside of a new one
81     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
82         let decoder: Box<dyn Read> = match format {
83             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
84             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
85             Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
86             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
87             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
88             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
89             Tar | Zip => unreachable!(),
90         };
91         Ok(decoder)
92     };
94     let (first_extension, extensions) = split_first_compression_format(&formats);
96     for format in extensions.iter().rev() {
97         reader = chain_reader_decoder(format, reader)?;
98     }
100     let files_unpacked = match first_extension {
101         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
102             reader = chain_reader_decoder(&first_extension, reader)?;
104             let mut writer = match utils::ask_to_create_file(&output_file_path, question_policy)? {
105                 Some(file) => file,
106                 None => return Ok(()),
107             };
109             io::copy(&mut reader, &mut writer)?;
111             1
112         }
113         Tar => {
114             if let ControlFlow::Continue(files) = smart_unpack(
115                 |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, quiet),
116                 output_dir,
117                 &output_file_path,
118                 question_policy,
119             )? {
120                 files
121             } else {
122                 return Ok(());
123             }
124         }
125         Zip => {
126             if formats.len() > 1 {
127                 warn_user_about_loading_zip_in_memory();
129                 if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? {
130                     return Ok(());
131                 }
132             }
134             let mut vec = vec![];
135             io::copy(&mut reader, &mut vec)?;
136             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
138             if let ControlFlow::Continue(files) = smart_unpack(
139                 |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, quiet),
140                 output_dir,
141                 &output_file_path,
142                 question_policy,
143             )? {
144                 files
145             } else {
146                 return Ok(());
147             }
148         }
149     };
151     // this is only printed once, so it doesn't result in much text. On the other hand,
152     // having a final status message is important especially in an accessibility context
153     // as screen readers may not read a commands exit code, making it hard to reason
154     // about whether the command succeeded without such a message
155     info!(
156         accessible,
157         "Successfully decompressed archive in {}.",
158         nice_directory_display(output_dir)
159     );
160     info!(accessible, "Files unpacked: {}", files_unpacked);
162     Ok(())
165 /// Unpacks an archive with some heuristics
166 /// - If the archive contains only one file, it will be extracted to the `output_dir`
167 /// - If the archive contains multiple files, it will be extracted to a subdirectory of the
168 ///   output_dir named after the archive (given by `output_file_path`)
169 /// Note: This functions assumes that `output_dir` exists
170 fn smart_unpack(
171     unpack_fn: impl FnOnce(&Path) -> crate::Result<usize>,
172     output_dir: &Path,
173     output_file_path: &Path,
174     question_policy: QuestionPolicy,
175 ) -> crate::Result<ControlFlow<(), usize>> {
176     assert!(output_dir.exists());
177     let temp_dir = tempfile::tempdir_in(output_dir)?;
178     let temp_dir_path = temp_dir.path();
179     info!(
180         accessible,
181         "Created temporary directory {} to hold decompressed elements.",
182         nice_directory_display(temp_dir_path)
183     );
185     let files = unpack_fn(temp_dir_path)?;
187     let root_contains_only_one_element = fs::read_dir(temp_dir_path)?.count() == 1;
188     if root_contains_only_one_element {
189         // Only one file in the root directory, so we can just move it to the output directory
190         let file = fs::read_dir(temp_dir_path)?.next().expect("item exists")?;
191         let file_path = file.path();
192         let file_name = file_path
193             .file_name()
194             .expect("Should be safe because paths in archives should not end with '..'");
195         let correct_path = output_dir.join(file_name);
196         // Before moving, need to check if a file with the same name already exists
197         if !utils::clear_path(&correct_path, question_policy)? {
198             return Ok(ControlFlow::Break(()));
199         }
200         fs::rename(&file_path, &correct_path)?;
201         info!(
202             accessible,
203             "Successfully moved {} to {}.",
204             nice_directory_display(&file_path),
205             nice_directory_display(&correct_path)
206         );
207     } else {
208         // Multiple files in the root directory, so:
209         // Rename the temporary directory to the archive name, which is output_file_path
210         // One case to handle tough is we need to check if a file with the same name already exists
211         if !utils::clear_path(output_file_path, question_policy)? {
212             return Ok(ControlFlow::Break(()));
213         }
214         fs::rename(temp_dir_path, output_file_path)?;
215         info!(
216             accessible,
217             "Successfully moved {} to {}.",
218             nice_directory_display(temp_dir_path),
219             nice_directory_display(output_file_path)
220         );
221     }
223     Ok(ControlFlow::Continue(files))