use crate::utils::*; use clap::Args; use flate2::write::GzEncoder; use flate2::{read::GzDecoder, Compression}; use std::fs::File; use std::io::{self, Read, Write}; #[derive(Args, Debug)] pub struct GzipArgs { #[clap(flatten)] pub common_args: CommonArgs, #[clap(flatten)] pub level_args: LevelArgs, } pub struct Gzip { pub compression_level: u32, } impl Default for Gzip { fn default() -> Self { Gzip { compression_level: 6, } } } impl Gzip { pub fn new(args: &GzipArgs) -> Gzip { Gzip { compression_level: args.level_args.level.level, } } } impl Compressor for Gzip { /// The standard extension for the gzip format. fn extension(&self) -> &str { "gz" } /// Full name for gzip. fn name(&self) -> &str { "gzip" } /// Generate a default extracted filename /// gzip does not support extracting to a directory, so we return a default filename fn default_extracted_filename(&self, in_path: &std::path::Path) -> String { // If the file has no extension, return a default filename if in_path.extension().is_none() { return "archive".to_string(); } // Otherwise, return the filename without the extension in_path.file_stem().unwrap().to_str().unwrap().to_string() } /// Compress an input file or pipe to a gzip archive fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { if let CmprssOutput::Path(out_path) = &output { if out_path.is_dir() { return cmprss_error("Gzip does not support compressing to a directory. Please specify an output file."); } } if let CmprssInput::Path(input_paths) = &input { for x in input_paths { if x.is_dir() { return cmprss_error( "Gzip does not support compressing a directory. Please specify only files.", ); } } } let mut input_stream = match input { CmprssInput::Path(paths) => { if paths.len() > 1 { return cmprss_error("only 1 file can be compressed at a time"); } Box::new(File::open(paths[0].as_path())?) } CmprssInput::Pipe(pipe) => Box::new(pipe) as Box, }; let output_stream = match output { CmprssOutput::Path(path) => Box::new(File::create(path)?), CmprssOutput::Pipe(pipe) => Box::new(pipe) as Box, }; let mut encoder = GzEncoder::new(output_stream, Compression::new(self.compression_level)); std::io::copy(&mut input_stream, &mut encoder)?; encoder.finish()?; Ok(()) } /// Extract a gzip archive fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { let input_stream = match input { CmprssInput::Path(paths) => { if paths.len() > 1 { return cmprss_error("only 1 file can be extracted at a time"); } Box::new(File::open(paths[0].as_path())?) } CmprssInput::Pipe(pipe) => Box::new(pipe) as Box, }; let mut output_stream = match output { CmprssOutput::Path(path) => Box::new(File::create(path)?), CmprssOutput::Pipe(pipe) => Box::new(pipe) as Box, }; let mut decoder = GzDecoder::new(input_stream); std::io::copy(&mut decoder, &mut output_stream)?; Ok(()) } } #[cfg(test)] mod tests { use super::*; use assert_fs::prelude::*; use predicates::prelude::*; #[test] fn roundtrip() -> Result<(), Box> { let compressor = Gzip::default(); let file = assert_fs::NamedTempFile::new("test.txt")?; file.write_str("garbage data for testing")?; let working_dir = assert_fs::TempDir::new()?; let archive = working_dir.child("archive.".to_owned() + compressor.extension()); archive.assert(predicate::path::missing()); // Roundtrip compress/extract compressor.compress( CmprssInput::Path(vec![file.path().to_path_buf()]), CmprssOutput::Path(archive.path().to_path_buf()), )?; archive.assert(predicate::path::is_file()); compressor.extract( CmprssInput::Path(vec![archive.path().to_path_buf()]), CmprssOutput::Path(working_dir.child("test.txt").path().to_path_buf()), )?; // Assert the files are identical working_dir .child("test.txt") .assert(predicate::path::eq_file(file.path())); Ok(()) } }