···44use std::sync::mpsc::{channel, Receiver, Sender};
55use std::thread;
6677-/// A compressor that chains multiple compressors together
88-/// This allows for multi-level compression formats like tar.gz
99-pub struct MultiLevelCompressor {
77+/// A pipeline of one or more compressors applied in sequence (e.g., tar.gz)
88+pub struct Pipeline {
109 // The chain of compressors to apply in order (innermost to outermost)
1110 compressors: Vec<Box<dyn Compressor>>,
1211}
13121414-impl MultiLevelCompressor {
1515- /// Create a new MultiLevelCompressor with a chain of compressors
1313+impl Pipeline {
1414+ /// Create a new Pipeline with the given compressors
1615 pub fn new(compressors: Vec<Box<dyn Compressor>>) -> Self {
1717- MultiLevelCompressor { compressors }
1616+ Pipeline { compressors }
1817 }
19182020- /// Create a new MultiLevelCompressor from compressor type names
1919+ /// Create a new Pipeline from compressor type names
2120 pub fn from_names(compressor_names: &[String]) -> io::Result<Self> {
2221 let compressors = compressor_names
2322 .iter()
···149148 }
150149}
151150152152-impl Compressor for MultiLevelCompressor {
151151+impl Compressor for Pipeline {
153152 fn name(&self) -> &str {
154153 if let Some(comp) = self.compressors.last() {
155154 comp.name()
···217216 if self.compressors.is_empty() {
218217 return Err(io::Error::new(
219218 io::ErrorKind::Other,
220220- "No compressors in multi-level chain",
219219+ "No compressors in pipeline",
221220 ));
222221 }
223222···272271 if self.compressors.is_empty() {
273272 return Err(io::Error::new(
274273 io::ErrorKind::Other,
275275- "No compressors in multi-level chain for extraction",
274274+ "No compressors in pipeline for extraction",
276275 ));
277276 }
278277···349348 use tempfile::tempdir;
350349351350 #[test]
352352- fn test_multi_level_compression() -> Result<(), io::Error> {
353353- // Create a temporary directory for our test
351351+ fn test_pipeline_compression() -> Result<(), io::Error> {
354352 let temp_dir = tempdir()?;
355353356356- // Create a test file
357357- let test_content = "This is a test file for multi-level compression";
354354+ let test_content = "This is a test file for pipeline compression";
358355 let test_file_path = temp_dir.path().join("test.txt");
359356 fs::write(&test_file_path, test_content)?;
360357361361- // Create a tar.gz compressor (tar first, then gzip)
362362- let compressors: Vec<Box<dyn Compressor>> = vec![
358358+ let pipeline = Pipeline::new(vec![
363359 Box::new(crate::backends::Tar::default()),
364360 Box::new(crate::backends::Gzip::default()),
365365- ];
366366- let multi_compressor = MultiLevelCompressor::new(compressors);
361361+ ]);
367362368368- // Compress the test file
369363 let archive_path = temp_dir.path().join("test.tar.gz");
370370- multi_compressor.compress(
364364+ pipeline.compress(
371365 CmprssInput::Path(vec![test_file_path.clone()]),
372366 CmprssOutput::Path(archive_path.clone()),
373367 )?;
374368375375- // Verify the archive was created
376369 assert!(archive_path.exists());
377370378378- // Extract the archive
379371 let output_dir = temp_dir.path().join("extracted");
380372 fs::create_dir(&output_dir)?;
381381- multi_compressor.extract(
373373+ pipeline.extract(
382374 CmprssInput::Path(vec![archive_path.clone()]),
383375 CmprssOutput::Path(output_dir.clone()),
384376 )?;
385377386386- // Verify the file was extracted correctly
387378 let extracted_file = output_dir.join("test.txt");
388379 assert!(extracted_file.exists());
389380390390- // Verify the content is the same
391381 let extracted_content = fs::read_to_string(extracted_file)?;
392382 assert_eq!(extracted_content, test_content);
393383
+3-3
src/main.rs
···8181 action: Action,
8282}
83838484-/// Get a compressor from a filename, detecting multi-level formats like tar.gz
8484+/// Get a compressor pipeline from a filename by scanning extensions right-to-left
8585fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> {
8686 let file_name = filename.file_name()?.to_str()?;
8787 let parts: Vec<&str> = file_name.split('.').collect();
···108108109109 // Reverse to innermost-to-outermost order
110110 compressor_names.reverse();
111111- MultiLevelCompressor::from_names(&compressor_names)
111111+ Pipeline::from_names(&compressor_names)
112112 .ok()
113113 .map(|m| Box::new(m) as Box<dyn Compressor>)
114114}
···507507 }
508508 // Handle all Writer output cases
509509 (_, CmprssOutput::Writer(_)) => {
510510- // Writer outputs are only supported in multi-level compression
510510+ // Writer outputs are only used internally by Pipeline
511511 // In main.rs we'll assume compression
512512 action = Action::Compress;
513513 }
+4-10
tests/multi_level.rs
tests/pipeline.rs
···88use predicates::prelude::*;
99use std::process::Command;
10101111-// Test the most common multi-level compression case: tar.gz
1111+// Test manual step-by-step tar.gz roundtrip
1212#[test]
1313fn test_tar_gz_manual_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
1414 let temp_dir = TempDir::new()?;
···6262 Ok(())
6363}
64646565-//
6666-// Test the multi-level compression using tar.gz format
6767-//
6565+// Test pipeline compression using tar.gz format
6866#[test]
6967fn test_tar_gz_compress() -> Result<(), Box<dyn std::error::Error>> {
7068 let temp_dir = TempDir::new()?;
···9189 Ok(())
9290}
93919494-//
9595-// Test the multi-level extraction using tar.gz format
9696-//
9292+// Test pipeline extraction using tar.gz format
9793#[test]
9894fn test_tar_gz_extract() -> Result<(), Box<dyn std::error::Error>> {
9995 let temp_dir = TempDir::new()?;
···179175 Ok(())
180176}
181177182182-//
183183-// Test the multi-level extraction using tar.gz format with explicit commands
184184-//
178178+// Test pipeline extraction using tar.gz with explicit compress then auto-detect extract
185179#[test]
186180fn test_tar_gz_explicit_then_extract() -> Result<(), Box<dyn std::error::Error>> {
187181 let temp_dir = TempDir::new()?;