this repo has no description
0
fork

Configure Feed

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

at tangled-ci 352 lines 12 kB view raw
1use super::containers::total_input_bytes; 2use crate::progress::{OutputTarget, ProgressArgs, ProgressReader, create_progress_bar}; 3use crate::utils::{ 4 CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor, 5 DefaultCompressionValidator, ExtractedTarget, LevelArgs, Result, 6}; 7use anyhow::{anyhow, bail}; 8use clap::Args; 9use indicatif::ProgressBar; 10use sevenz_rust2::{ 11 ArchiveEntry, ArchiveReader, ArchiveWriter, Password, decompress, encoder_options::Lzma2Options, 12}; 13use std::fs::File; 14use std::io::{self, Empty, Seek, SeekFrom, Write}; 15use std::path::Path; 16use tempfile::tempfile; 17 18#[derive(Args, Debug)] 19pub struct SevenZArgs { 20 #[clap(flatten)] 21 pub common_args: CommonArgs, 22 23 #[clap(flatten)] 24 pub level_args: LevelArgs, 25 26 #[clap(flatten)] 27 pub progress_args: ProgressArgs, 28} 29 30#[derive(Clone)] 31pub struct SevenZ { 32 pub compression_level: i32, 33 pub progress_args: ProgressArgs, 34} 35 36impl Default for SevenZ { 37 fn default() -> Self { 38 SevenZ { 39 compression_level: DefaultCompressionValidator.default_level(), 40 progress_args: ProgressArgs::default(), 41 } 42 } 43} 44 45impl SevenZ { 46 pub fn new(args: &SevenZArgs) -> SevenZ { 47 SevenZ { 48 compression_level: args.level_args.resolve(&DefaultCompressionValidator), 49 progress_args: args.progress_args, 50 } 51 } 52 53 /// Extract a seekable 7z input with a byte-level progress bar keyed to 54 /// the compressed archive size. 55 fn decompress_seekable<R: io::Read + Seek>( 56 &self, 57 reader: R, 58 size: u64, 59 out_dir: &Path, 60 ) -> Result { 61 let bar = create_progress_bar(Some(size), self.progress_args.progress, OutputTarget::File); 62 let reader = ProgressReader::new(reader, bar.clone()); 63 decompress(reader, out_dir)?; 64 if let Some(b) = bar { 65 b.finish(); 66 } 67 Ok(()) 68 } 69 70 /// Compress to the given seekable writer, walking path inputs ourselves 71 /// so each file's read goes through `ProgressReader` sharing `bar`. 72 fn compress_to_file<W: Write + Seek>( 73 &self, 74 input: CmprssInput, 75 writer: W, 76 bar: Option<&ProgressBar>, 77 ) -> Result { 78 let mut aw = ArchiveWriter::new(writer)?; 79 let lzma = Lzma2Options::from_level(self.compression_level as u32); 80 aw.set_content_methods(vec![lzma.into()]); 81 82 match input { 83 CmprssInput::Path(paths) => { 84 for path in paths { 85 let name = path 86 .file_name() 87 .ok_or_else(|| anyhow!("input path has no file name: {:?}", path))? 88 .to_string_lossy() 89 .to_string(); 90 if path.is_file() { 91 push_file_entry(&mut aw, &name, &path, bar)?; 92 } else if path.is_dir() { 93 push_dir_entries(&mut aw, &name, &path, bar)?; 94 } else { 95 bail!("7z does not support this file type"); 96 } 97 } 98 } 99 CmprssInput::Pipe(pipe) => { 100 let entry = ArchiveEntry::new_file("archive"); 101 aw.push_archive_entry(entry, Some(pipe))?; 102 } 103 CmprssInput::Reader(_) => { 104 bail!("7z does not accept an in-memory reader input"); 105 } 106 } 107 108 aw.finish()?; 109 Ok(()) 110 } 111} 112 113impl Compressor for SevenZ { 114 fn name(&self) -> &str { 115 "7z" 116 } 117 118 fn default_extracted_target(&self) -> ExtractedTarget { 119 ExtractedTarget::Directory 120 } 121 122 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 123 match output { 124 CmprssOutput::Path(ref path) => { 125 let total = match &input { 126 CmprssInput::Path(paths) => Some(total_input_bytes(paths)), 127 _ => None, 128 }; 129 let bar = 130 create_progress_bar(total, self.progress_args.progress, OutputTarget::File); 131 let file = File::create(path)?; 132 self.compress_to_file(input, file, bar.as_ref())?; 133 if let Some(b) = bar { 134 b.finish(); 135 } 136 Ok(()) 137 } 138 CmprssOutput::Pipe(mut pipe) => { 139 let mut temp_file = tempfile()?; 140 self.compress_to_file(input, &mut temp_file, None)?; 141 temp_file.seek(SeekFrom::Start(0))?; 142 io::copy(&mut temp_file, &mut pipe)?; 143 Ok(()) 144 } 145 CmprssOutput::Writer(mut writer) => { 146 let mut temp_file = tempfile()?; 147 self.compress_to_file(input, &mut temp_file, None)?; 148 temp_file.seek(SeekFrom::Start(0))?; 149 io::copy(&mut temp_file, &mut writer)?; 150 Ok(()) 151 } 152 } 153 } 154 155 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 156 match output { 157 CmprssOutput::Path(ref out_dir) => { 158 if !out_dir.exists() { 159 std::fs::create_dir_all(out_dir)?; 160 } else if !out_dir.is_dir() { 161 bail!("7z extraction output must be a directory"); 162 } 163 164 match input { 165 CmprssInput::Path(paths) => { 166 if paths.len() != 1 { 167 bail!("7z extraction expects exactly one archive file"); 168 } 169 let file = File::open(&paths[0])?; 170 let size = file.metadata()?.len(); 171 self.decompress_seekable(file, size, out_dir) 172 } 173 CmprssInput::Pipe(mut pipe) => { 174 let mut temp_file = tempfile()?; 175 io::copy(&mut pipe, &mut temp_file)?; 176 temp_file.seek(SeekFrom::Start(0))?; 177 let size = temp_file.metadata()?.len(); 178 self.decompress_seekable(temp_file, size, out_dir) 179 } 180 CmprssInput::Reader(_) => { 181 bail!( 182 "7z extraction does not accept an in-memory reader input (requires seekable input)" 183 ) 184 } 185 } 186 } 187 CmprssOutput::Pipe(_) => bail!("7z extraction to stdout is not supported"), 188 CmprssOutput::Writer(mut writer) => match input { 189 CmprssInput::Path(paths) => { 190 if paths.len() != 1 { 191 bail!("7z extraction expects exactly one archive file"); 192 } 193 let mut file = File::open(&paths[0])?; 194 io::copy(&mut file, &mut writer)?; 195 Ok(()) 196 } 197 CmprssInput::Pipe(mut pipe) => { 198 io::copy(&mut pipe, &mut writer)?; 199 Ok(()) 200 } 201 CmprssInput::Reader(mut reader) => { 202 io::copy(&mut reader, &mut writer)?; 203 Ok(()) 204 } 205 }, 206 } 207 } 208 209 fn list(&self, input: CmprssInput) -> Result { 210 let stdout = io::stdout(); 211 let mut out = stdout.lock(); 212 match input { 213 CmprssInput::Path(paths) => { 214 if paths.len() != 1 { 215 bail!("7z listing expects exactly one archive file"); 216 } 217 let reader = ArchiveReader::open(&paths[0], Password::empty())?; 218 for entry in &reader.archive().files { 219 writeln!(out, "{}", entry.name())?; 220 } 221 } 222 CmprssInput::Pipe(mut pipe) => { 223 let mut temp = tempfile()?; 224 io::copy(&mut pipe, &mut temp)?; 225 temp.seek(SeekFrom::Start(0))?; 226 let reader = ArchiveReader::new(temp, Password::empty())?; 227 for entry in &reader.archive().files { 228 writeln!(out, "{}", entry.name())?; 229 } 230 } 231 CmprssInput::Reader(mut reader) => { 232 let mut temp = tempfile()?; 233 io::copy(&mut reader, &mut temp)?; 234 temp.seek(SeekFrom::Start(0))?; 235 let ar = ArchiveReader::new(temp, Password::empty())?; 236 for entry in &ar.archive().files { 237 writeln!(out, "{}", entry.name())?; 238 } 239 } 240 } 241 Ok(()) 242 } 243} 244 245/// Push a single regular file as an archive entry, with reads flowing 246/// through `ProgressReader` so they tick the shared bar. 247fn push_file_entry<W: Write + Seek>( 248 aw: &mut ArchiveWriter<W>, 249 archive_name: &str, 250 disk_path: &Path, 251 bar: Option<&ProgressBar>, 252) -> Result { 253 let entry = ArchiveEntry::from_path(disk_path, archive_name.to_string()); 254 let file = File::open(disk_path)?; 255 let reader = ProgressReader::new(file, bar.cloned()); 256 aw.push_archive_entry(entry, Some(reader))?; 257 Ok(()) 258} 259 260/// Push a directory entry, then recurse into its children. Mirrors the 261/// layout that `push_source_path` would produce (entries named 262/// `<dir>/<child>`), but gives us a read hook for each file. 263fn push_dir_entries<W: Write + Seek>( 264 aw: &mut ArchiveWriter<W>, 265 archive_name: &str, 266 disk_path: &Path, 267 bar: Option<&ProgressBar>, 268) -> Result { 269 let entry = ArchiveEntry::from_path(disk_path, archive_name.to_string()); 270 aw.push_archive_entry::<Empty>(entry, None)?; 271 for child in std::fs::read_dir(disk_path)? { 272 let child = child?; 273 let child_path = child.path(); 274 let child_name = format!("{}/{}", archive_name, child.file_name().to_string_lossy()); 275 if child_path.is_file() { 276 push_file_entry(aw, &child_name, &child_path, bar)?; 277 } else if child_path.is_dir() { 278 push_dir_entries(aw, &child_name, &child_path, bar)?; 279 } 280 // Skip symlinks/other types — parity with prior behavior. 281 } 282 Ok(()) 283} 284 285#[cfg(test)] 286mod tests { 287 use super::*; 288 use crate::test_utils::*; 289 use assert_fs::prelude::*; 290 use predicates::prelude::*; 291 use std::path::PathBuf; 292 293 #[test] 294 fn test_sevenz_interface() { 295 let compressor = SevenZ::default(); 296 test_compressor_interface(&compressor, "7z", Some("7z")); 297 } 298 299 #[test] 300 fn test_sevenz_default_compression() -> Result { 301 let compressor = SevenZ::default(); 302 test_compression(&compressor) 303 } 304 305 #[test] 306 fn test_sevenz_fast_compression() -> Result { 307 let fast_compressor = SevenZ { 308 compression_level: 1, 309 progress_args: ProgressArgs::default(), 310 }; 311 test_compression(&fast_compressor) 312 } 313 314 #[test] 315 fn test_sevenz_best_compression() -> Result { 316 let best_compressor = SevenZ { 317 compression_level: 9, 318 progress_args: ProgressArgs::default(), 319 }; 320 test_compression(&best_compressor) 321 } 322 323 #[test] 324 fn test_directory_handling() -> Result { 325 let compressor = SevenZ::default(); 326 let dir = assert_fs::TempDir::new()?; 327 let file_path = dir.child("file.txt"); 328 file_path.write_str("directory test data")?; 329 let working_dir = assert_fs::TempDir::new()?; 330 let archive = working_dir.child("dir_archive.7z"); 331 archive.assert(predicate::path::missing()); 332 333 compressor.compress( 334 CmprssInput::Path(vec![dir.path().to_path_buf()]), 335 CmprssOutput::Path(archive.path().to_path_buf()), 336 )?; 337 archive.assert(predicate::path::is_file()); 338 339 let extract_dir = working_dir.child("extracted"); 340 std::fs::create_dir_all(extract_dir.path())?; 341 compressor.extract( 342 CmprssInput::Path(vec![archive.path().to_path_buf()]), 343 CmprssOutput::Path(extract_dir.path().to_path_buf()), 344 )?; 345 let dir_name: PathBuf = dir.path().file_name().unwrap().into(); 346 extract_dir 347 .child(dir_name) 348 .child("file.txt") 349 .assert(predicate::path::eq_file(file_path.path())); 350 Ok(()) 351 } 352}