refactor(cli): move thread pool setup to command execution, use thread::spawn instead...
[ouch.git] / src / commands / compress.rs
blob180bb2eb86b616a2cc288d773a3551ef6f2d7c2d
1 use std::{
2     io::{self, BufWriter, Cursor, Seek, Write},
3     path::{Path, PathBuf},
4 };
6 use fs_err as fs;
8 use super::warn_user_about_loading_sevenz_in_memory;
9 use crate::{
10     archive,
11     commands::warn_user_about_loading_zip_in_memory,
12     extension::{split_first_compression_format, CompressionFormat::*, Extension},
13     utils::{io::lock_and_flush_output_stdio, user_wants_to_continue, FileVisibilityPolicy},
14     QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
17 /// Compress files into `output_file`.
18 ///
19 /// # Arguments:
20 /// - `files`: is the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
21 /// - `extensions`: is a list of compression formats for compressing, example: [Tar, Gz] (in compression order)
22 /// - `output_file` is the resulting compressed file name, example: "archive.tar.gz"
23 ///
24 /// # Return value
25 /// - Returns `Ok(true)` if compressed all files normally.
26 /// - Returns `Ok(false)` if user opted to abort compression mid-way.
27 #[allow(clippy::too_many_arguments)]
28 pub fn compress_files(
29     files: Vec<PathBuf>,
30     extensions: Vec<Extension>,
31     output_file: fs::File,
32     output_path: &Path,
33     quiet: bool,
34     question_policy: QuestionPolicy,
35     file_visibility_policy: FileVisibilityPolicy,
36     level: Option<i16>,
37 ) -> crate::Result<bool> {
38     // If the input files contain a directory, then the total size will be underestimated
39     let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
41     let mut writer: Box<dyn Send + Write> = Box::new(file_writer);
43     // Grab previous encoder and wrap it inside of a new one
44     let chain_writer_encoder = |format: &_, encoder| -> crate::Result<_> {
45         let encoder: Box<dyn Send + Write> = match format {
46             Gzip => Box::new(
47                 // by default, ParCompress uses a default compression level of 3
48                 // instead of the regular default that flate2 uses
49                 gzp::par::compress::ParCompress::<gzp::deflate::Gzip>::builder()
50                     .compression_level(
51                         level.map_or_else(Default::default, |l| gzp::Compression::new((l as u32).clamp(0, 9))),
52                     )
53                     .from_writer(encoder),
54             ),
55             Bzip => Box::new(bzip2::write::BzEncoder::new(
56                 encoder,
57                 level.map_or_else(Default::default, |l| bzip2::Compression::new((l as u32).clamp(1, 9))),
58             )),
59             Bzip3 => Box::new(
60                 // Use block size of 16 MiB
61                 bzip3::write::Bz3Encoder::new(encoder, 16 * 2_usize.pow(20))?,
62             ),
63             Lz4 => Box::new(lz4_flex::frame::FrameEncoder::new(encoder).auto_finish()),
64             Lzma => Box::new(xz2::write::XzEncoder::new(
65                 encoder,
66                 level.map_or(6, |l| (l as u32).clamp(0, 9)),
67             )),
68             Snappy => Box::new(
69                 gzp::par::compress::ParCompress::<gzp::snap::Snap>::builder()
70                     .compression_level(gzp::par::compress::Compression::new(
71                         level.map_or_else(Default::default, |l| (l as u32).clamp(0, 9)),
72                     ))
73                     .from_writer(encoder),
74             ),
75             Zstd => {
76                 let mut zstd_encoder = zstd::stream::write::Encoder::new(
77                     encoder,
78                     level.map_or(zstd::DEFAULT_COMPRESSION_LEVEL, |l| {
79                         (l as i32).clamp(zstd::zstd_safe::min_c_level(), zstd::zstd_safe::max_c_level())
80                     }),
81                 )?;
82                 // Use all available PHYSICAL cores for compression
83                 zstd_encoder.multithread(num_cpus::get_physical() as u32)?;
84                 Box::new(zstd_encoder.auto_finish())
85             }
86             Tar | Zip | Rar | SevenZip => unreachable!(),
87         };
88         Ok(encoder)
89     };
91     let (first_format, formats) = split_first_compression_format(&extensions);
93     for format in formats.iter().rev() {
94         writer = chain_writer_encoder(format, writer)?;
95     }
97     match first_format {
98         Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
99             writer = chain_writer_encoder(&first_format, writer)?;
100             let mut reader = fs::File::open(&files[0])?;
102             io::copy(&mut reader, &mut writer)?;
103         }
104         Tar => {
105             archive::tar::build_archive_from_paths(&files, output_path, &mut writer, file_visibility_policy, quiet)?;
106             writer.flush()?;
107         }
108         Zip => {
109             if !formats.is_empty() {
110                 // Locking necessary to guarantee that warning and question
111                 // messages stay adjacent
112                 let _locks = lock_and_flush_output_stdio();
114                 warn_user_about_loading_zip_in_memory();
115                 if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
116                     return Ok(false);
117                 }
118             }
120             let mut vec_buffer = Cursor::new(vec![]);
122             archive::zip::build_archive_from_paths(
123                 &files,
124                 output_path,
125                 &mut vec_buffer,
126                 file_visibility_policy,
127                 quiet,
128             )?;
129             vec_buffer.rewind()?;
130             io::copy(&mut vec_buffer, &mut writer)?;
131         }
132         Rar => {
133             #[cfg(feature = "unrar")]
134             return Err(archive::rar::no_compression());
136             #[cfg(not(feature = "unrar"))]
137             return Err(archive::rar_stub::no_support());
138         }
139         SevenZip => {
140             if !formats.is_empty() {
141                 // Locking necessary to guarantee that warning and question
142                 // messages stay adjacent
143                 let _locks = lock_and_flush_output_stdio();
145                 warn_user_about_loading_sevenz_in_memory();
146                 if !user_wants_to_continue(output_path, question_policy, QuestionAction::Compression)? {
147                     return Ok(false);
148                 }
149             }
151             let mut vec_buffer = Cursor::new(vec![]);
152             archive::sevenz::compress_sevenz(&files, output_path, &mut vec_buffer, file_visibility_policy, quiet)?;
153             vec_buffer.rewind()?;
154             io::copy(&mut vec_buffer, &mut writer)?;
155         }
156     }
158     Ok(true)