this repo has no description
1use crate::utils::*;
2use clap::Args;
3use std::fs::File;
4use std::io::{self, Seek, SeekFrom, Write};
5use std::path::Path;
6use tempfile::tempfile;
7use zip::read::ZipArchive;
8use zip::write::FileOptions;
9use zip::{CompressionMethod, ZipWriter};
10
11#[derive(Args, Debug)]
12pub struct ZipArgs {
13 #[clap(flatten)]
14 pub common_args: CommonArgs,
15}
16
17#[derive(Default)]
18pub struct Zip {}
19
20impl Zip {
21 pub fn new(_args: &ZipArgs) -> Zip {
22 Zip {}
23 }
24
25 fn compress_to_file<W: Write + Seek>(
26 &self,
27 input: CmprssInput,
28 writer: W,
29 ) -> Result<(), io::Error> {
30 let mut zip_writer = ZipWriter::new(writer);
31 let options = FileOptions::default().compression_method(CompressionMethod::Deflated);
32
33 match input {
34 CmprssInput::Path(paths) => {
35 for path in paths {
36 if path.is_file() {
37 let name = path.file_name().unwrap().to_string_lossy();
38 zip_writer.start_file(name, options)?;
39 let mut f = File::open(&path)?;
40 io::copy(&mut f, &mut zip_writer)?;
41 } else if path.is_dir() {
42 // Use the directory as the base and add its contents
43 let base = path.parent().unwrap_or(&path);
44 add_directory(&mut zip_writer, base, &path)?;
45 } else {
46 return cmprss_error("unsupported file type for zip compression");
47 }
48 }
49 }
50 CmprssInput::Pipe(mut pipe) => {
51 // For pipe input, we'll create a single file named "archive"
52 zip_writer.start_file("archive", options)?;
53 io::copy(&mut pipe, &mut zip_writer)?;
54 }
55 }
56
57 zip_writer.finish()?;
58 Ok(())
59 }
60}
61
62impl Compressor for Zip {
63 fn name(&self) -> &str {
64 "zip"
65 }
66
67 fn default_extracted_filename(&self, in_path: &Path) -> String {
68 if let Some(stem) = in_path.file_stem() {
69 stem.to_string_lossy().into_owned()
70 } else {
71 ".".to_string()
72 }
73 }
74
75 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> {
76 match output {
77 CmprssOutput::Path(ref path) => {
78 let file = File::create(path)?;
79 self.compress_to_file(input, file)
80 }
81 CmprssOutput::Pipe(mut pipe) => {
82 // Create a temporary file to write the zip to
83 let mut temp_file = tempfile()?;
84 self.compress_to_file(input, &mut temp_file)?;
85
86 // Reset the file position to the beginning
87 temp_file.seek(SeekFrom::Start(0))?;
88
89 // Copy the temporary file to the pipe
90 io::copy(&mut temp_file, &mut pipe)?;
91 Ok(())
92 }
93 }
94 }
95
96 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> {
97 match output {
98 CmprssOutput::Path(ref out_dir) => {
99 // Create the output directory if it doesn't exist
100 if !out_dir.exists() {
101 std::fs::create_dir_all(out_dir)?;
102 } else if !out_dir.is_dir() {
103 return cmprss_error("zip extraction output must be a directory");
104 }
105
106 match input {
107 CmprssInput::Path(paths) => {
108 if paths.len() != 1 {
109 return cmprss_error("zip extraction expects a single archive file");
110 }
111 let file = File::open(&paths[0])?;
112 let mut archive = ZipArchive::new(file)?;
113 archive
114 .extract(out_dir)
115 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
116 }
117 CmprssInput::Pipe(mut pipe) => {
118 // Create a temporary file to store the zip content
119 let mut temp_file = tempfile()?;
120
121 // Copy from pipe to temporary file
122 io::copy(&mut pipe, &mut temp_file)?;
123
124 // Reset the file position to the beginning
125 temp_file.seek(SeekFrom::Start(0))?;
126
127 // Extract from the temporary file
128 let mut archive = ZipArchive::new(temp_file)?;
129 archive
130 .extract(out_dir)
131 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
132 }
133 }
134 }
135 CmprssOutput::Pipe(_) => cmprss_error("zip extraction to stdout is not supported"),
136 }
137 }
138}
139
140fn add_directory<W: Write + Seek>(
141 zip: &mut ZipWriter<W>,
142 base: &Path,
143 path: &Path,
144) -> Result<(), io::Error> {
145 for entry in std::fs::read_dir(path)? {
146 let entry = entry?;
147 let entry_path = entry.path();
148 // Get relative path for archive entry
149 let name = entry_path
150 .strip_prefix(base)
151 .unwrap()
152 .to_string_lossy()
153 .replace('\\', "/");
154 if entry_path.is_file() {
155 let options = FileOptions::default().compression_method(CompressionMethod::Deflated);
156 zip.start_file(name, options)?;
157 let mut f = File::open(&entry_path)?;
158 io::copy(&mut f, zip)?;
159 } else if entry_path.is_dir() {
160 // Ensure directory entry ends with '/'
161 let dir_name = name.clone() + "/";
162 zip.add_directory(
163 dir_name,
164 FileOptions::default().compression_method(CompressionMethod::Deflated),
165 )?;
166 add_directory(zip, base, &entry_path)?;
167 }
168 }
169 Ok(())
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use assert_fs::prelude::*;
176 use predicates::prelude::*;
177 use std::path::PathBuf;
178
179 #[test]
180 fn roundtrip_file() -> Result<(), Box<dyn std::error::Error>> {
181 let compressor = Zip::default();
182 let file = assert_fs::NamedTempFile::new("test.txt")?;
183 file.write_str("test data for zip")?;
184 let working_dir = assert_fs::TempDir::new()?;
185 let archive = working_dir.child("archive.zip");
186 archive.assert(predicate::path::missing());
187
188 compressor.compress(
189 CmprssInput::Path(vec![file.path().to_path_buf()]),
190 CmprssOutput::Path(archive.path().to_path_buf()),
191 )?;
192 archive.assert(predicate::path::is_file());
193
194 let extract_dir = working_dir.child("out");
195 std::fs::create_dir_all(extract_dir.path())?;
196 compressor.extract(
197 CmprssInput::Path(vec![archive.path().to_path_buf()]),
198 CmprssOutput::Path(extract_dir.path().to_path_buf()),
199 )?;
200 extract_dir
201 .child("test.txt")
202 .assert(predicate::path::eq_file(file.path()));
203 Ok(())
204 }
205
206 #[test]
207 fn roundtrip_directory() -> Result<(), Box<dyn std::error::Error>> {
208 let compressor = Zip::default();
209 let dir = assert_fs::TempDir::new()?;
210 let file_path = dir.child("file.txt");
211 file_path.write_str("directory test data")?;
212 let working_dir = assert_fs::TempDir::new()?;
213 let archive = working_dir.child("dir_archive.zip");
214 archive.assert(predicate::path::missing());
215
216 compressor.compress(
217 CmprssInput::Path(vec![dir.path().to_path_buf()]),
218 CmprssOutput::Path(archive.path().to_path_buf()),
219 )?;
220 archive.assert(predicate::path::is_file());
221
222 let extract_dir = working_dir.child("extracted");
223 std::fs::create_dir_all(extract_dir.path())?;
224 compressor.extract(
225 CmprssInput::Path(vec![archive.path().to_path_buf()]),
226 CmprssOutput::Path(extract_dir.path().to_path_buf()),
227 )?;
228 // When extracting a directory from a zip, the directory name is included in the path
229 // Since the archive stores the entire directory, the extracted file is contained in the directory
230 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
231 extract_dir
232 .child(dir_name)
233 .child("file.txt")
234 .assert(predicate::path::eq_file(file_path.path()));
235 Ok(())
236 }
237}