this repo has no description
0
fork

Configure Feed

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

at d7eac9681af468ef2dd62f00e8a09e36cf744585 266 lines 8.7 kB view raw
1use crate::progress::{ProgressArgs, copy_with_progress}; 2use crate::utils::*; 3use anyhow::bail; 4use clap::Args; 5use flate2::write::GzEncoder; 6use flate2::{Compression, read::GzDecoder}; 7use std::fs::File; 8use std::io::{self, BufReader, BufWriter, Read, Write}; 9 10#[derive(Args, Debug)] 11pub struct GzipArgs { 12 #[clap(flatten)] 13 pub common_args: CommonArgs, 14 15 #[clap(flatten)] 16 pub level_args: LevelArgs, 17 18 #[clap(flatten)] 19 pub progress_args: ProgressArgs, 20} 21 22pub struct Gzip { 23 pub compression_level: i32, 24 pub progress_args: ProgressArgs, 25} 26 27impl Default for Gzip { 28 fn default() -> Self { 29 let validator = DefaultCompressionValidator; 30 Gzip { 31 compression_level: validator.default_level(), 32 progress_args: ProgressArgs::default(), 33 } 34 } 35} 36 37impl Gzip { 38 pub fn new(args: &GzipArgs) -> Gzip { 39 let validator = DefaultCompressionValidator; 40 let level = args.level_args.level.level; 41 42 // Validate and clamp the level to gzip's valid range 43 let level = validator.validate_and_clamp_level(level); 44 45 Gzip { 46 compression_level: level, 47 progress_args: args.progress_args, 48 } 49 } 50} 51 52impl Compressor for Gzip { 53 /// The standard extension for the gzip format. 54 fn extension(&self) -> &str { 55 "gz" 56 } 57 58 /// Full name for gzip. 59 fn name(&self) -> &str { 60 "gzip" 61 } 62 63 /// Gzip extracts to a file by default 64 fn default_extracted_target(&self) -> ExtractedTarget { 65 ExtractedTarget::FILE 66 } 67 68 /// Compress an input file or pipe to a gzip archive 69 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 70 if let CmprssOutput::Path(out_path) = &output 71 && out_path.is_dir() 72 { 73 bail!( 74 "Gzip does not support compressing to a directory. Please specify an output file." 75 ); 76 } 77 if let CmprssInput::Path(input_paths) = &input { 78 for x in input_paths { 79 if x.is_dir() { 80 bail!( 81 "Gzip does not support compressing a directory. Please specify only files." 82 ); 83 } 84 } 85 } 86 let mut file_size = None; 87 let mut input_stream: Box<dyn Read + Send> = match input { 88 CmprssInput::Path(paths) => { 89 if paths.len() > 1 { 90 bail!("Multiple input files not supported for gzip"); 91 } 92 let path = &paths[0]; 93 file_size = Some(std::fs::metadata(path)?.len()); 94 Box::new(BufReader::new(File::open(path)?)) 95 } 96 CmprssInput::Pipe(stdin) => Box::new(BufReader::new(stdin)), 97 CmprssInput::Reader(reader) => reader.0, 98 }; 99 100 if let CmprssOutput::Writer(writer) = output { 101 let mut encoder = 102 GzEncoder::new(writer, Compression::new(self.compression_level as u32)); 103 io::copy(&mut input_stream, &mut encoder)?; 104 encoder.finish()?; 105 } else { 106 let output_stream: Box<dyn Write + Send> = match &output { 107 CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 108 CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 109 CmprssOutput::Writer(_) => unreachable!(), 110 }; 111 let mut encoder = GzEncoder::new( 112 output_stream, 113 Compression::new(self.compression_level as u32), 114 ); 115 copy_with_progress( 116 &mut input_stream, 117 &mut encoder, 118 self.progress_args.chunk_size.size_in_bytes, 119 file_size, 120 self.progress_args.progress, 121 &output, 122 )?; 123 encoder.finish()?; 124 } 125 Ok(()) 126 } 127 128 /// Extract a gzip archive 129 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 130 let mut file_size = None; 131 let input_stream: Box<dyn Read + Send> = match input { 132 CmprssInput::Path(paths) => { 133 if paths.len() > 1 { 134 bail!("Multiple input files not supported for gzip extraction"); 135 } 136 let path = &paths[0]; 137 file_size = Some(std::fs::metadata(path)?.len()); 138 Box::new(BufReader::new(File::open(path)?)) 139 } 140 CmprssInput::Pipe(stdin) => Box::new(BufReader::new(stdin)), 141 CmprssInput::Reader(reader) => reader.0, 142 }; 143 144 let mut decoder = GzDecoder::new(input_stream); 145 146 if let CmprssOutput::Writer(mut writer) = output { 147 io::copy(&mut decoder, &mut writer)?; 148 } else { 149 let mut output_stream: Box<dyn Write + Send> = match &output { 150 CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)), 151 CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)), 152 CmprssOutput::Writer(_) => unreachable!(), 153 }; 154 copy_with_progress( 155 &mut decoder, 156 &mut output_stream, 157 self.progress_args.chunk_size.size_in_bytes, 158 file_size, 159 self.progress_args.progress, 160 &output, 161 )?; 162 } 163 164 Ok(()) 165 } 166} 167 168#[cfg(test)] 169mod tests { 170 use super::*; 171 use crate::test_utils::*; 172 use std::fs; 173 use std::io::{Read, Write}; 174 use tempfile::tempdir; 175 176 /// Test the basic interface of the Gzip compressor 177 #[test] 178 fn test_gzip_interface() { 179 let compressor = Gzip::default(); 180 test_compressor_interface(&compressor, "gzip", Some("gz")); 181 } 182 183 /// Test the default compression level 184 #[test] 185 fn test_gzip_default_compression() -> Result { 186 let compressor = Gzip::default(); 187 test_compression(&compressor) 188 } 189 190 /// Test fast compression level 191 #[test] 192 fn test_gzip_fast_compression() -> Result { 193 let fast_compressor = Gzip { 194 compression_level: 1, 195 progress_args: ProgressArgs::default(), 196 }; 197 test_compression(&fast_compressor) 198 } 199 200 /// Test best compression level 201 #[test] 202 fn test_gzip_best_compression() -> Result { 203 let best_compressor = Gzip { 204 compression_level: 9, 205 progress_args: ProgressArgs::default(), 206 }; 207 test_compression(&best_compressor) 208 } 209 210 /// Test for gzip-specific behavior: handling of concatenated gzip archives 211 #[test] 212 fn test_concatenated_gzip() -> Result { 213 let compressor = Gzip::default(); 214 let temp_dir = tempdir().expect("Failed to create temp dir"); 215 216 // Create two test files 217 let input_path1 = temp_dir.path().join("input1.txt"); 218 let input_path2 = temp_dir.path().join("input2.txt"); 219 let test_data1 = "This is the first file"; 220 let test_data2 = "This is the second file"; 221 fs::write(&input_path1, test_data1)?; 222 fs::write(&input_path2, test_data2)?; 223 224 // Compress each file separately 225 let archive_path1 = temp_dir.path().join("archive1.gz"); 226 let archive_path2 = temp_dir.path().join("archive2.gz"); 227 228 compressor.compress( 229 CmprssInput::Path(vec![input_path1.clone()]), 230 CmprssOutput::Path(archive_path1.clone()), 231 )?; 232 233 compressor.compress( 234 CmprssInput::Path(vec![input_path2.clone()]), 235 CmprssOutput::Path(archive_path2.clone()), 236 )?; 237 238 // Create a concatenated archive 239 let concat_archive = temp_dir.path().join("concat.gz"); 240 let mut concat_file = fs::File::create(&concat_archive)?; 241 242 // Concat the two gzip files 243 let mut archive1_data = Vec::new(); 244 let mut archive2_data = Vec::new(); 245 fs::File::open(&archive_path1)?.read_to_end(&mut archive1_data)?; 246 fs::File::open(&archive_path2)?.read_to_end(&mut archive2_data)?; 247 248 concat_file.write_all(&archive1_data)?; 249 concat_file.write_all(&archive2_data)?; 250 concat_file.flush()?; 251 252 // Extract the concatenated archive - this should yield the first file's contents 253 let output_path = temp_dir.path().join("output.txt"); 254 255 compressor.extract( 256 CmprssInput::Path(vec![concat_archive]), 257 CmprssOutput::Path(output_path.clone()), 258 )?; 259 260 // Verify the result is the first file's content 261 let output_data = fs::read_to_string(output_path)?; 262 assert_eq!(output_data, test_data1); 263 264 Ok(()) 265 } 266}