this repo has no description
0
fork

Configure Feed

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

at 52ec9ed3a28388caee2e1d877b9b3eaf727655b6 223 lines 7.9 kB view raw
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 /// Zip extracts to a directory by default 68 fn default_extracted_target(&self) -> ExtractedTarget { 69 ExtractedTarget::DIRECTORY 70 } 71 72 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 73 match output { 74 CmprssOutput::Path(ref path) => { 75 let file = File::create(path)?; 76 self.compress_to_file(input, file) 77 } 78 CmprssOutput::Pipe(mut pipe) => { 79 // Create a temporary file to write the zip to 80 let mut temp_file = tempfile()?; 81 self.compress_to_file(input, &mut temp_file)?; 82 83 // Reset the file position to the beginning 84 temp_file.seek(SeekFrom::Start(0))?; 85 86 // Copy the temporary file to the pipe 87 io::copy(&mut temp_file, &mut pipe)?; 88 Ok(()) 89 } 90 } 91 } 92 93 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 94 match output { 95 CmprssOutput::Path(ref out_dir) => { 96 // Create the output directory if it doesn't exist 97 if !out_dir.exists() { 98 std::fs::create_dir_all(out_dir)?; 99 } else if !out_dir.is_dir() { 100 return cmprss_error("zip extraction output must be a directory"); 101 } 102 103 match input { 104 CmprssInput::Path(paths) => { 105 if paths.len() != 1 { 106 return cmprss_error("zip extraction expects a single archive file"); 107 } 108 let file = File::open(&paths[0])?; 109 let mut archive = ZipArchive::new(file)?; 110 archive 111 .extract(out_dir) 112 .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) 113 } 114 CmprssInput::Pipe(mut pipe) => { 115 // Create a temporary file to store the zip content 116 let mut temp_file = tempfile()?; 117 118 // Copy from pipe to temporary file 119 io::copy(&mut pipe, &mut temp_file)?; 120 121 // Reset the file position to the beginning 122 temp_file.seek(SeekFrom::Start(0))?; 123 124 // Extract from the temporary file 125 let mut archive = ZipArchive::new(temp_file)?; 126 archive 127 .extract(out_dir) 128 .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) 129 } 130 } 131 } 132 CmprssOutput::Pipe(_) => cmprss_error("zip extraction to stdout is not supported"), 133 } 134 } 135} 136 137fn add_directory<W: Write + Seek>( 138 zip: &mut ZipWriter<W>, 139 base: &Path, 140 path: &Path, 141) -> Result<(), io::Error> { 142 for entry in std::fs::read_dir(path)? { 143 let entry = entry?; 144 let entry_path = entry.path(); 145 // Get relative path for archive entry 146 let name = entry_path 147 .strip_prefix(base) 148 .unwrap() 149 .to_string_lossy() 150 .replace('\\', "/"); 151 if entry_path.is_file() { 152 let options = FileOptions::default().compression_method(CompressionMethod::Deflated); 153 zip.start_file(name, options)?; 154 let mut f = File::open(&entry_path)?; 155 io::copy(&mut f, zip)?; 156 } else if entry_path.is_dir() { 157 // Ensure directory entry ends with '/' 158 let dir_name = name.clone() + "/"; 159 zip.add_directory( 160 dir_name, 161 FileOptions::default().compression_method(CompressionMethod::Deflated), 162 )?; 163 add_directory(zip, base, &entry_path)?; 164 } 165 } 166 Ok(()) 167} 168 169#[cfg(test)] 170mod tests { 171 use super::*; 172 use crate::test_utils::*; 173 use assert_fs::prelude::*; 174 use predicates::prelude::*; 175 use std::path::PathBuf; 176 177 /// Test the basic interface of the Zip compressor 178 #[test] 179 fn test_zip_interface() { 180 let compressor = Zip::default(); 181 test_compressor_interface(&compressor, "zip", Some("zip")); 182 } 183 184 /// Test the default compression level 185 #[test] 186 fn test_zip_default_compression() -> Result<(), io::Error> { 187 let compressor = Zip::default(); 188 test_compression(&compressor) 189 } 190 191 /// Test zip-specific functionality: directory handling 192 #[test] 193 fn test_directory_handling() -> Result<(), Box<dyn std::error::Error>> { 194 let compressor = Zip::default(); 195 let dir = assert_fs::TempDir::new()?; 196 let file_path = dir.child("file.txt"); 197 file_path.write_str("directory test data")?; 198 let working_dir = assert_fs::TempDir::new()?; 199 let archive = working_dir.child("dir_archive.zip"); 200 archive.assert(predicate::path::missing()); 201 202 compressor.compress( 203 CmprssInput::Path(vec![dir.path().to_path_buf()]), 204 CmprssOutput::Path(archive.path().to_path_buf()), 205 )?; 206 archive.assert(predicate::path::is_file()); 207 208 let extract_dir = working_dir.child("extracted"); 209 std::fs::create_dir_all(extract_dir.path())?; 210 compressor.extract( 211 CmprssInput::Path(vec![archive.path().to_path_buf()]), 212 CmprssOutput::Path(extract_dir.path().to_path_buf()), 213 )?; 214 // When extracting a directory from a zip, the directory name is included in the path 215 // Since the archive stores the entire directory, the extracted file is contained in the directory 216 let dir_name: PathBuf = dir.path().file_name().unwrap().into(); 217 extract_dir 218 .child(dir_name) 219 .child("file.txt") 220 .assert(predicate::path::eq_file(file_path.path())); 221 Ok(()) 222 } 223}