small refactor and renamings
[ouch.git] / src / commands / decompress.rs
blobaa297f906d8df7a79696b87b999b8cb5f481363a
1 use std::{
2     io::{self, BufReader, Read, Write},
3     ops::ControlFlow,
4     path::{Path, PathBuf},
5 };
7 use fs_err as fs;
9 use crate::{
10     commands::warn_user_about_in_memory_zip_decompression,
11     extension::{
12         CompressionFormat::{self, *},
13         Extension,
14     },
15     info,
16     progress::Progress,
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 ) -> crate::Result<()> {
34     assert!(output_dir.exists());
35     let total_input_size = input_file_path.metadata().expect("file exists").len();
36     let reader = fs::File::open(&input_file_path)?;
37     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
38     // from decoder chaining.
39     //
40     // This is the only case where we can read and unpack it directly, without having to do
41     // in-memory decompression/copying first.
42     //
43     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
44     if formats.len() == 1 && *formats[0].compression_formats == [Zip] {
45         let zip_archive = zip::ZipArchive::new(reader)?;
46         let files = if let ControlFlow::Continue(files) = smart_unpack(
47             Box::new(move |output_dir| {
48                 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
49                 crate::archive::zip::unpack_archive(
50                     zip_archive,
51                     output_dir,
52                     progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
53                 )
54             }),
55             output_dir,
56             &output_file_path,
57             question_policy,
58         )? {
59             files
60         } else {
61             return Ok(());
62         };
64         // this is only printed once, so it doesn't result in much text. On the other hand,
65         // having a final status message is important especially in an accessibility context
66         // as screen readers may not read a commands exit code, making it hard to reason
67         // about whether the command succeeded without such a message
68         info!(
69             accessible,
70             "Successfully decompressed archive in {} ({} files).",
71             nice_directory_display(output_dir),
72             files.len()
73         );
75         return Ok(());
76     }
78     // Will be used in decoder chaining
79     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
80     let mut reader: Box<dyn Read> = Box::new(reader);
82     // Grab previous decoder and wrap it inside of a new one
83     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
84         let decoder: Box<dyn Read> = match format {
85             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
86             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
87             Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
88             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
89             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
90             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
91             Tar | Zip => unreachable!(),
92         };
93         Ok(decoder)
94     };
96     for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
97         reader = chain_reader_decoder(format, reader)?;
98     }
100     let files_unpacked = match formats[0].compression_formats[0] {
101         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
102             reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
104             let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
105             if writer.is_none() {
106                 // Means that the user doesn't want to overwrite
107                 return Ok(());
108             }
109             let mut writer = writer.unwrap();
111             let current_position_fn = Box::new({
112                 let output_file_path = output_file_path.clone();
113                 move || output_file_path.clone().metadata().expect("file exists").len()
114             });
115             let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
117             io::copy(&mut reader, &mut writer)?;
118             vec![output_file_path]
119         }
120         Tar => {
121             if let ControlFlow::Continue(files) = smart_unpack(
122                 Box::new(move |output_dir| {
123                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
124                     crate::archive::tar::unpack_archive(
125                         reader,
126                         output_dir,
127                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
128                     )
129                 }),
130                 output_dir,
131                 &output_file_path,
132                 question_policy,
133             )? {
134                 files
135             } else {
136                 return Ok(());
137             }
138         }
139         Zip => {
140             if formats.len() > 1 {
141                 warn_user_about_in_memory_zip_decompression();
143                 // give user the option to continue decompressing after warning is shown
144                 if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? {
145                     return Ok(());
146                 }
147             }
149             let mut vec = vec![];
150             io::copy(&mut reader, &mut vec)?;
151             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
153             if let ControlFlow::Continue(files) = smart_unpack(
154                 Box::new(move |output_dir| {
155                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
156                     crate::archive::zip::unpack_archive(
157                         zip_archive,
158                         output_dir,
159                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
160                     )
161                 }),
162                 output_dir,
163                 &output_file_path,
164                 question_policy,
165             )? {
166                 files
167             } else {
168                 return Ok(());
169             }
170         }
171     };
173     // this is only printed once, so it doesn't result in much text. On the other hand,
174     // having a final status message is important especially in an accessibility context
175     // as screen readers may not read a commands exit code, making it hard to reason
176     // about whether the command succeeded without such a message
177     info!(accessible, "Successfully decompressed archive in {}.", nice_directory_display(output_dir));
178     info!(accessible, "Files unpacked: {}", files_unpacked.len());
180     Ok(())
183 /// Unpacks an archive with some heuristics
184 /// - If the archive contains only one file, it will be extracted to the `output_dir`
185 /// - 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`)
186 /// Note: This functions assumes that `output_dir` exists
187 fn smart_unpack(
188     unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
189     output_dir: &Path,
190     output_file_path: &Path,
191     question_policy: QuestionPolicy,
192 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
193     assert!(output_dir.exists());
194     let temp_dir = tempfile::tempdir_in(output_dir)?;
195     let temp_dir_path = temp_dir.path();
196     info!(
197         accessible,
198         "Created temporary directory {} to hold decompressed elements.",
199         nice_directory_display(temp_dir_path)
200     );
202     // unpack the files
203     let files = unpack_fn(temp_dir_path)?;
205     let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
206     if root_contains_only_one_element {
207         // Only one file in the root directory, so we can just move it to the output directory
208         let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
209         let file_path = file.path();
210         let file_name =
211             file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
212         let correct_path = output_dir.join(file_name);
213         // One case to handle tough is we need to check if a file with the same name already exists
214         if !utils::clear_path(&correct_path, question_policy)? {
215             return Ok(ControlFlow::Break(()));
216         }
217         fs::rename(&file_path, &correct_path)?;
218         info!(
219             accessible,
220             "Successfully moved {} to {}.",
221             nice_directory_display(&file_path),
222             nice_directory_display(&correct_path)
223         );
224     } else {
225         // Multiple files in the root directory, so:
226         // Rename  the temporary directory to the archive name, which is output_file_path
227         // One case to handle tough is we need to check if a file with the same name already exists
228         if !utils::clear_path(output_file_path, question_policy)? {
229             return Ok(ControlFlow::Break(()));
230         }
231         fs::rename(&temp_dir_path, &output_file_path)?;
232         info!(
233             accessible,
234             "Successfully moved {} to {}.",
235             nice_directory_display(temp_dir_path),
236             nice_directory_display(output_file_path)
237         );
238     }
239     Ok(ControlFlow::Continue(files))