this repo has no description
0
fork

Configure Feed

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

at f7bbb7638e8ed85d5c92d23cfdcf38ed665b3b21 283 lines 10 kB view raw
1use crate::utils::*; 2use anyhow::bail; 3use clap::Args; 4use std::fs::File; 5use std::io::{self, Seek, SeekFrom, Write}; 6use std::path::Path; 7use tempfile::tempfile; 8use zip::read::ZipArchive; 9use zip::write::FileOptions; 10use zip::{CompressionMethod, ZipWriter}; 11 12#[derive(Args, Debug)] 13pub struct ZipArgs { 14 #[clap(flatten)] 15 pub common_args: CommonArgs, 16} 17 18#[derive(Default)] 19pub struct Zip {} 20 21impl Zip { 22 pub fn new(_args: &ZipArgs) -> Zip { 23 Zip {} 24 } 25 26 fn compress_to_file<W: Write + Seek>(&self, input: CmprssInput, writer: W) -> Result { 27 let mut zip_writer = ZipWriter::new(writer); 28 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated); 29 30 match input { 31 CmprssInput::Path(paths) => { 32 for path in paths { 33 if path.is_file() { 34 let name = path.file_name().unwrap().to_string_lossy(); 35 zip_writer.start_file(name, options)?; 36 let mut f = File::open(&path)?; 37 io::copy(&mut f, &mut zip_writer)?; 38 } else if path.is_dir() { 39 // Use the directory as the base and add its contents 40 let base = path.parent().unwrap_or(&path); 41 add_directory(&mut zip_writer, base, &path)?; 42 } else { 43 bail!("unsupported file type for zip compression"); 44 } 45 } 46 } 47 CmprssInput::Pipe(mut pipe) => { 48 // For pipe input, we'll create a single file named "archive" 49 zip_writer.start_file("archive", options)?; 50 io::copy(&mut pipe, &mut zip_writer)?; 51 } 52 CmprssInput::Reader(_) => { 53 bail!("Cannot zip a reader input"); 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 { 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 CmprssOutput::Writer(mut writer) => { 91 let mut temp_file = tempfile()?; 92 self.compress_to_file(input, &mut temp_file)?; 93 temp_file.seek(SeekFrom::Start(0))?; 94 io::copy(&mut temp_file, &mut writer)?; 95 Ok(()) 96 } 97 } 98 } 99 100 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 101 match output { 102 CmprssOutput::Path(ref out_dir) => { 103 // Create the output directory if it doesn't exist 104 if !out_dir.exists() { 105 std::fs::create_dir_all(out_dir)?; 106 } else if !out_dir.is_dir() { 107 bail!("zip extraction output must be a directory"); 108 } 109 110 match input { 111 CmprssInput::Path(paths) => { 112 if paths.len() != 1 { 113 bail!("zip extraction expects a single archive file"); 114 } 115 let file = File::open(&paths[0])?; 116 let mut archive = ZipArchive::new(file)?; 117 Ok(archive.extract(out_dir)?) 118 } 119 CmprssInput::Pipe(mut pipe) => { 120 // Create a temporary file to store the zip content 121 let mut temp_file = tempfile()?; 122 123 // Copy from pipe to temporary file 124 io::copy(&mut pipe, &mut temp_file)?; 125 126 // Reset the file position to the beginning 127 temp_file.seek(SeekFrom::Start(0))?; 128 129 // Extract from the temporary file 130 let mut archive = ZipArchive::new(temp_file)?; 131 Ok(archive.extract(out_dir)?) 132 } 133 CmprssInput::Reader(_) => { 134 bail!( 135 "Cannot extract from a reader input for zip (requires seekable input)" 136 ) 137 } 138 } 139 } 140 CmprssOutput::Pipe(_) => bail!("zip extraction to stdout is not supported"), 141 CmprssOutput::Writer(mut writer) => match input { 142 CmprssInput::Path(paths) => { 143 if paths.len() != 1 { 144 bail!("zip extraction expects a single archive file"); 145 } 146 let mut file = File::open(&paths[0])?; 147 io::copy(&mut file, &mut writer)?; 148 Ok(()) 149 } 150 CmprssInput::Pipe(mut pipe) => { 151 io::copy(&mut pipe, &mut writer)?; 152 Ok(()) 153 } 154 CmprssInput::Reader(mut reader) => { 155 io::copy(&mut reader, &mut writer)?; 156 Ok(()) 157 } 158 }, 159 } 160 } 161 162 fn list(&self, input: CmprssInput) -> Result { 163 // ZipArchive requires a seekable reader. For non-path inputs we must 164 // buffer into a tempfile first. 165 let stdout = io::stdout(); 166 let mut out = stdout.lock(); 167 match input { 168 CmprssInput::Path(paths) => { 169 if paths.len() != 1 { 170 bail!("zip listing expects a single archive file"); 171 } 172 let archive = ZipArchive::new(File::open(&paths[0])?)?; 173 for name in archive.file_names() { 174 writeln!(out, "{}", name)?; 175 } 176 } 177 CmprssInput::Pipe(mut pipe) => { 178 let mut temp = tempfile()?; 179 io::copy(&mut pipe, &mut temp)?; 180 temp.seek(SeekFrom::Start(0))?; 181 let archive = ZipArchive::new(temp)?; 182 for name in archive.file_names() { 183 writeln!(out, "{}", name)?; 184 } 185 } 186 CmprssInput::Reader(mut reader) => { 187 let mut temp = tempfile()?; 188 io::copy(&mut reader, &mut temp)?; 189 temp.seek(SeekFrom::Start(0))?; 190 let archive = ZipArchive::new(temp)?; 191 for name in archive.file_names() { 192 writeln!(out, "{}", name)?; 193 } 194 } 195 } 196 Ok(()) 197 } 198} 199 200fn add_directory<W: Write + Seek>(zip: &mut ZipWriter<W>, base: &Path, path: &Path) -> Result { 201 for entry in std::fs::read_dir(path)? { 202 let entry = entry?; 203 let entry_path = entry.path(); 204 // Get relative path for archive entry 205 let name = entry_path 206 .strip_prefix(base) 207 .unwrap() 208 .to_string_lossy() 209 .replace('\\', "/"); 210 if entry_path.is_file() { 211 let options = 212 FileOptions::<()>::default().compression_method(CompressionMethod::Deflated); 213 zip.start_file(name, options)?; 214 let mut f = File::open(&entry_path)?; 215 io::copy(&mut f, zip)?; 216 } else if entry_path.is_dir() { 217 // Ensure directory entry ends with '/' 218 let dir_name = name.clone() + "/"; 219 zip.add_directory( 220 dir_name, 221 FileOptions::<()>::default().compression_method(CompressionMethod::Deflated), 222 )?; 223 add_directory(zip, base, &entry_path)?; 224 } 225 } 226 Ok(()) 227} 228 229#[cfg(test)] 230mod tests { 231 use super::*; 232 use crate::test_utils::*; 233 use assert_fs::prelude::*; 234 use predicates::prelude::*; 235 use std::path::PathBuf; 236 237 /// Test the basic interface of the Zip compressor 238 #[test] 239 fn test_zip_interface() { 240 let compressor = Zip::default(); 241 test_compressor_interface(&compressor, "zip", Some("zip")); 242 } 243 244 /// Test the default compression level 245 #[test] 246 fn test_zip_default_compression() -> Result { 247 let compressor = Zip::default(); 248 test_compression(&compressor) 249 } 250 251 /// Test zip-specific functionality: directory handling 252 #[test] 253 fn test_directory_handling() -> Result { 254 let compressor = Zip::default(); 255 let dir = assert_fs::TempDir::new()?; 256 let file_path = dir.child("file.txt"); 257 file_path.write_str("directory test data")?; 258 let working_dir = assert_fs::TempDir::new()?; 259 let archive = working_dir.child("dir_archive.zip"); 260 archive.assert(predicate::path::missing()); 261 262 compressor.compress( 263 CmprssInput::Path(vec![dir.path().to_path_buf()]), 264 CmprssOutput::Path(archive.path().to_path_buf()), 265 )?; 266 archive.assert(predicate::path::is_file()); 267 268 let extract_dir = working_dir.child("extracted"); 269 std::fs::create_dir_all(extract_dir.path())?; 270 compressor.extract( 271 CmprssInput::Path(vec![archive.path().to_path_buf()]), 272 CmprssOutput::Path(extract_dir.path().to_path_buf()), 273 )?; 274 // When extracting a directory from a zip, the directory name is included in the path 275 // Since the archive stores the entire directory, the extracted file is contained in the directory 276 let dir_name: PathBuf = dir.path().file_name().unwrap().into(); 277 extract_dir 278 .child(dir_name) 279 .child("file.txt") 280 .assert(predicate::path::eq_file(file_path.path())); 281 Ok(()) 282 } 283}