2 io::{self, BufReader, Read, Write},
10 commands::warn_user_about_in_memory_zip_decompression,
12 CompressionFormat::{self, *},
17 utils::{self, nice_directory_display, user_wants_to_continue},
18 QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
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>,
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.
40 // This is the only case where we can read and unpack it directly, without having to do
41 // in-memory decompression/copying first.
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(
52 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
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
70 "Successfully decompressed archive in {} ({} files).",
71 nice_directory_display(output_dir),
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!(),
96 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
97 reader = chain_reader_decoder(format, reader)?;
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
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()
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]
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(
127 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
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)? {
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(
159 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
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());
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
188 unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
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();
198 "Created temporary directory {} to hold decompressed elements.",
199 nice_directory_display(temp_dir_path)
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();
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(()));
217 fs::rename(&file_path, &correct_path)?;
220 "Successfully moved {} to {}.",
221 nice_directory_display(&file_path),
222 nice_directory_display(&correct_path)
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(()));
231 fs::rename(&temp_dir_path, &output_file_path)?;
234 "Successfully moved {} to {}.",
235 nice_directory_display(temp_dir_path),
236 nice_directory_display(output_file_path)
239 Ok(ControlFlow::Continue(files))