this repo has no description
0
fork

Configure Feed

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

at eeb60a130d5c62af7e5265c53679a7a52ce4f786 237 lines 8.5 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 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}