this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 6656606eeb2bf5e5ad6220aeff42ad0ca5d4e112 373 lines 13 kB view raw
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}