this repo has no description
1use super::containers::total_input_bytes;
2use crate::progress::{OutputTarget, ProgressArgs, ProgressReader, create_progress_bar};
3use crate::utils::{
4 CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor,
5 DefaultCompressionValidator, ExtractedTarget, LevelArgs, Result,
6};
7use anyhow::bail;
8use clap::Args;
9use indicatif::ProgressBar;
10use std::fs::File;
11use std::io::{self, Seek, SeekFrom, Write};
12use std::path::Path;
13use tempfile::tempfile;
14use zip::read::ZipArchive;
15use zip::write::FileOptions;
16use zip::{CompressionMethod, ZipWriter};
17
18#[derive(Args, Debug)]
19pub struct ZipArgs {
20 #[clap(flatten)]
21 pub common_args: CommonArgs,
22
23 #[clap(flatten)]
24 pub level_args: LevelArgs,
25
26 #[clap(flatten)]
27 pub progress_args: ProgressArgs,
28}
29
30#[derive(Clone)]
31pub struct Zip {
32 pub compression_level: i32,
33 pub progress_args: ProgressArgs,
34}
35
36impl Default for Zip {
37 fn default() -> Self {
38 Zip {
39 compression_level: DefaultCompressionValidator.default_level(),
40 progress_args: ProgressArgs::default(),
41 }
42 }
43}
44
45impl Zip {
46 pub fn new(args: &ZipArgs) -> Zip {
47 Zip {
48 compression_level: args.level_args.resolve(&DefaultCompressionValidator),
49 progress_args: args.progress_args,
50 }
51 }
52
53 fn file_options(&self) -> FileOptions<'static, ()> {
54 FileOptions::<()>::default()
55 .compression_method(CompressionMethod::Deflated)
56 .compression_level(Some(self.compression_level as i64))
57 .large_file(true)
58 }
59
60 fn extract_seekable<R: std::io::Read + Seek>(
61 &self,
62 reader: R,
63 size: u64,
64 out_dir: &Path,
65 ) -> Result {
66 let bar = create_progress_bar(Some(size), self.progress_args.progress, OutputTarget::File);
67 let reader = ProgressReader::new(reader, bar.clone());
68 let mut archive = ZipArchive::new(reader)?;
69 archive.extract(out_dir)?;
70 if let Some(b) = bar {
71 b.finish();
72 }
73 Ok(())
74 }
75
76 fn compress_to_file<W: Write + Seek>(
77 &self,
78 input: CmprssInput,
79 writer: W,
80 bar: Option<&ProgressBar>,
81 ) -> Result {
82 let mut zip_writer = ZipWriter::new(writer);
83 let options = self.file_options();
84
85 match input {
86 CmprssInput::Path(paths) => {
87 for path in paths {
88 if path.is_file() {
89 let name = path.file_name().unwrap().to_string_lossy();
90 zip_writer.start_file(name, options)?;
91 let f = File::open(&path)?;
92 let mut reader = ProgressReader::new(f, bar.cloned());
93 io::copy(&mut reader, &mut zip_writer)?;
94 } else if path.is_dir() {
95 // Use the directory as the base and add its contents
96 let base = path.parent().unwrap_or(&path);
97 add_directory(&mut zip_writer, base, &path, options, bar)?;
98 } else {
99 bail!("zip does not support this file type");
100 }
101 }
102 }
103 CmprssInput::Pipe(mut pipe) => {
104 // For pipe input, we'll create a single file named "archive"
105 zip_writer.start_file("archive", options)?;
106 io::copy(&mut pipe, &mut zip_writer)?;
107 }
108 CmprssInput::Reader(_) => {
109 bail!("zip does not accept an in-memory reader input");
110 }
111 }
112
113 zip_writer.finish()?;
114 Ok(())
115 }
116}
117
118impl Compressor for Zip {
119 fn name(&self) -> &str {
120 "zip"
121 }
122
123 fn clone_boxed(&self) -> Box<dyn Compressor> {
124 Box::new(self.clone())
125 }
126
127 /// Zip extracts to a directory by default
128 fn default_extracted_target(&self) -> ExtractedTarget {
129 ExtractedTarget::Directory
130 }
131
132 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
133 match output {
134 CmprssOutput::Path(ref path) => {
135 let total = match &input {
136 CmprssInput::Path(paths) => Some(total_input_bytes(paths)),
137 _ => None,
138 };
139 let bar =
140 create_progress_bar(total, self.progress_args.progress, OutputTarget::File);
141 let file = File::create(path)?;
142 self.compress_to_file(input, file, bar.as_ref())?;
143 if let Some(b) = bar {
144 b.finish();
145 }
146 Ok(())
147 }
148 CmprssOutput::Pipe(mut pipe) => {
149 // Create a temporary file to write the zip to
150 let mut temp_file = tempfile()?;
151 self.compress_to_file(input, &mut temp_file, None)?;
152
153 // Reset the file position to the beginning
154 temp_file.seek(SeekFrom::Start(0))?;
155
156 // Copy the temporary file to the pipe
157 io::copy(&mut temp_file, &mut pipe)?;
158 Ok(())
159 }
160 CmprssOutput::Writer(mut writer) => {
161 let mut temp_file = tempfile()?;
162 self.compress_to_file(input, &mut temp_file, None)?;
163 temp_file.seek(SeekFrom::Start(0))?;
164 io::copy(&mut temp_file, &mut writer)?;
165 Ok(())
166 }
167 }
168 }
169
170 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
171 match output {
172 CmprssOutput::Path(ref out_dir) => {
173 // Create the output directory if it doesn't exist
174 if !out_dir.exists() {
175 std::fs::create_dir_all(out_dir)?;
176 } else if !out_dir.is_dir() {
177 bail!("zip extraction output must be a directory");
178 }
179
180 match input {
181 CmprssInput::Path(paths) => {
182 if paths.len() != 1 {
183 bail!("zip extraction expects exactly one archive file");
184 }
185 let file = File::open(&paths[0])?;
186 let size = file.metadata()?.len();
187 self.extract_seekable(file, size, out_dir)
188 }
189 CmprssInput::Pipe(mut pipe) => {
190 // Create a temporary file to store the zip content
191 let mut temp_file = tempfile()?;
192
193 // Copy from pipe to temporary file
194 io::copy(&mut pipe, &mut temp_file)?;
195
196 // Reset the file position to the beginning
197 temp_file.seek(SeekFrom::Start(0))?;
198 let size = temp_file.metadata()?.len();
199 self.extract_seekable(temp_file, size, out_dir)
200 }
201 CmprssInput::Reader(_) => {
202 bail!(
203 "zip extraction does not accept an in-memory reader input (requires seekable input)"
204 )
205 }
206 }
207 }
208 CmprssOutput::Pipe(_) => bail!("zip extraction to stdout is not supported"),
209 CmprssOutput::Writer(mut writer) => match input {
210 CmprssInput::Path(paths) => {
211 if paths.len() != 1 {
212 bail!("zip extraction expects exactly one archive file");
213 }
214 let mut file = File::open(&paths[0])?;
215 io::copy(&mut file, &mut writer)?;
216 Ok(())
217 }
218 CmprssInput::Pipe(mut pipe) => {
219 io::copy(&mut pipe, &mut writer)?;
220 Ok(())
221 }
222 CmprssInput::Reader(mut reader) => {
223 io::copy(&mut reader, &mut writer)?;
224 Ok(())
225 }
226 },
227 }
228 }
229
230 fn list(&self, input: CmprssInput) -> Result {
231 // ZipArchive requires a seekable reader. For non-path inputs we must
232 // buffer into a tempfile first.
233 let stdout = io::stdout();
234 let mut out = stdout.lock();
235 match input {
236 CmprssInput::Path(paths) => {
237 if paths.len() != 1 {
238 bail!("zip listing expects exactly one archive file");
239 }
240 let archive = ZipArchive::new(File::open(&paths[0])?)?;
241 for name in archive.file_names() {
242 writeln!(out, "{}", name)?;
243 }
244 }
245 CmprssInput::Pipe(mut pipe) => {
246 let mut temp = tempfile()?;
247 io::copy(&mut pipe, &mut temp)?;
248 temp.seek(SeekFrom::Start(0))?;
249 let archive = ZipArchive::new(temp)?;
250 for name in archive.file_names() {
251 writeln!(out, "{}", name)?;
252 }
253 }
254 CmprssInput::Reader(mut reader) => {
255 let mut temp = tempfile()?;
256 io::copy(&mut reader, &mut temp)?;
257 temp.seek(SeekFrom::Start(0))?;
258 let archive = ZipArchive::new(temp)?;
259 for name in archive.file_names() {
260 writeln!(out, "{}", name)?;
261 }
262 }
263 }
264 Ok(())
265 }
266}
267
268fn add_directory<W: Write + Seek>(
269 zip: &mut ZipWriter<W>,
270 base: &Path,
271 path: &Path,
272 options: FileOptions<'static, ()>,
273 bar: Option<&ProgressBar>,
274) -> Result {
275 for entry in std::fs::read_dir(path)? {
276 let entry = entry?;
277 let entry_path = entry.path();
278 // Get relative path for archive entry
279 let name = entry_path
280 .strip_prefix(base)
281 .unwrap()
282 .to_string_lossy()
283 .replace('\\', "/");
284 if entry_path.is_file() {
285 zip.start_file(name, options)?;
286 let f = File::open(&entry_path)?;
287 let mut reader = ProgressReader::new(f, bar.cloned());
288 io::copy(&mut reader, zip)?;
289 } else if entry_path.is_dir() {
290 // Ensure directory entry ends with '/'
291 let dir_name = name.clone() + "/";
292 zip.add_directory(dir_name, options)?;
293 add_directory(zip, base, &entry_path, options, bar)?;
294 }
295 }
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::test_utils::*;
303 use assert_fs::prelude::*;
304 use predicates::prelude::*;
305 use std::path::PathBuf;
306
307 /// Test the basic interface of the Zip compressor
308 #[test]
309 fn test_zip_interface() {
310 let compressor = Zip::default();
311 test_compressor_interface(&compressor, "zip", Some("zip"));
312 }
313
314 /// Test the default compression level
315 #[test]
316 fn test_zip_default_compression() -> Result {
317 let compressor = Zip::default();
318 test_compression(&compressor)
319 }
320
321 /// Test fast compression level
322 #[test]
323 fn test_zip_fast_compression() -> Result {
324 let fast_compressor = Zip {
325 compression_level: 1,
326 progress_args: ProgressArgs::default(),
327 };
328 test_compression(&fast_compressor)
329 }
330
331 /// Test best compression level
332 #[test]
333 fn test_zip_best_compression() -> Result {
334 let best_compressor = Zip {
335 compression_level: 9,
336 progress_args: ProgressArgs::default(),
337 };
338 test_compression(&best_compressor)
339 }
340
341 /// Test zip-specific functionality: directory handling
342 #[test]
343 fn test_directory_handling() -> Result {
344 let compressor = Zip::default();
345 let dir = assert_fs::TempDir::new()?;
346 let file_path = dir.child("file.txt");
347 file_path.write_str("directory test data")?;
348 let working_dir = assert_fs::TempDir::new()?;
349 let archive = working_dir.child("dir_archive.zip");
350 archive.assert(predicate::path::missing());
351
352 compressor.compress(
353 CmprssInput::Path(vec![dir.path().to_path_buf()]),
354 CmprssOutput::Path(archive.path().to_path_buf()),
355 )?;
356 archive.assert(predicate::path::is_file());
357
358 let extract_dir = working_dir.child("extracted");
359 std::fs::create_dir_all(extract_dir.path())?;
360 compressor.extract(
361 CmprssInput::Path(vec![archive.path().to_path_buf()]),
362 CmprssOutput::Path(extract_dir.path().to_path_buf()),
363 )?;
364 // When extracting a directory from a zip, the directory name is included in the path
365 // Since the archive stores the entire directory, the extracted file is contained in the directory
366 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
367 extract_dir
368 .child(dir_name)
369 .child("file.txt")
370 .assert(predicate::path::eq_file(file_path.path()));
371 Ok(())
372 }
373}