this repo has no description
0
fork

Configure Feed

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

feat(tar): add progress bars with shared pre-walk helper

+168 -20
+61
src/backends/containers.rs
··· 1 + //! Shared helpers for container formats (tar, zip, 7z). 2 + //! 3 + //! Single-stream codecs know the total input size up front from file 4 + //! metadata. Container formats take N paths and recurse into directories, so 5 + //! they have to pre-walk the input to get a meaningful progress total. 6 + 7 + use std::path::Path; 8 + 9 + /// Sum sizes of all regular files reachable from the given paths, recursing 10 + /// into directories. Best-effort: anything we can't stat (permission denied, 11 + /// racy deletion) is counted as zero; the bar may finish short rather than 12 + /// fail the run. 13 + pub fn total_input_bytes<P: AsRef<Path>>(paths: &[P]) -> u64 { 14 + paths.iter().map(|p| sum_path(p.as_ref())).sum() 15 + } 16 + 17 + fn sum_path(path: &Path) -> u64 { 18 + let Ok(meta) = std::fs::symlink_metadata(path) else { 19 + return 0; 20 + }; 21 + if meta.is_file() { 22 + return meta.len(); 23 + } 24 + if meta.is_dir() { 25 + let Ok(entries) = std::fs::read_dir(path) else { 26 + return 0; 27 + }; 28 + return entries.flatten().map(|e| sum_path(&e.path())).sum(); 29 + } 30 + 0 31 + } 32 + 33 + #[cfg(test)] 34 + mod tests { 35 + use super::*; 36 + use assert_fs::prelude::*; 37 + 38 + #[test] 39 + fn sums_single_file() { 40 + let dir = assert_fs::TempDir::new().unwrap(); 41 + let f = dir.child("a.txt"); 42 + f.write_str("hello").unwrap(); 43 + assert_eq!(total_input_bytes(&[f.path().to_path_buf()]), 5); 44 + } 45 + 46 + #[test] 47 + fn sums_directory_recursively() { 48 + let dir = assert_fs::TempDir::new().unwrap(); 49 + dir.child("a.txt").write_str("abc").unwrap(); 50 + dir.child("sub/b.txt").write_str("defgh").unwrap(); 51 + assert_eq!(total_input_bytes(&[dir.path().to_path_buf()]), 8); 52 + } 53 + 54 + #[test] 55 + fn missing_path_counts_zero() { 56 + assert_eq!( 57 + total_input_bytes(&[std::path::PathBuf::from("/nope/xx")]), 58 + 0 59 + ); 60 + } 61 + }
+1
src/backends/mod.rs
··· 1 1 mod brotli; 2 2 mod bzip2; 3 + mod containers; 3 4 mod gzip; 4 5 mod lz4; 5 6 mod lzma;
+106 -20
src/backends/tar.rs
··· 2 2 3 3 use anyhow::bail; 4 4 use clap::Args; 5 + use indicatif::ProgressBar; 5 6 use std::fs::File; 6 7 use std::io::{self, Read, Seek, SeekFrom, Write}; 7 - use tar::{Archive, Builder}; 8 + use std::path::Path; 9 + use tar::{Archive, Builder, EntryType, Header}; 8 10 use tempfile::tempfile; 9 11 12 + use super::containers::total_input_bytes; 13 + use crate::progress::{OutputTarget, ProgressArgs, ProgressReader, create_progress_bar}; 10 14 use crate::utils::{CmprssInput, CmprssOutput, CommonArgs, Compressor, ExtractedTarget, Result}; 11 15 12 16 #[derive(Args, Debug)] 13 17 pub struct TarArgs { 14 18 #[clap(flatten)] 15 19 pub common_args: CommonArgs, 20 + 21 + #[clap(flatten)] 22 + pub progress_args: ProgressArgs, 16 23 } 17 24 18 25 #[derive(Default, Clone)] 19 - pub struct Tar {} 26 + pub struct Tar { 27 + pub progress_args: ProgressArgs, 28 + } 20 29 21 30 impl Tar { 22 - pub fn new(_args: &TarArgs) -> Tar { 23 - Tar {} 31 + pub fn new(args: &TarArgs) -> Tar { 32 + Tar { 33 + progress_args: args.progress_args, 34 + } 24 35 } 25 36 } 26 37 ··· 42 53 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 43 54 match output { 44 55 CmprssOutput::Path(path) => { 56 + let total = match &input { 57 + CmprssInput::Path(paths) => Some(total_input_bytes(paths)), 58 + _ => None, 59 + }; 60 + let bar = 61 + create_progress_bar(total, self.progress_args.progress, OutputTarget::File); 45 62 let file = File::create(path)?; 46 - self.compress_internal(input, Builder::new(file)) 63 + self.compress_internal(input, Builder::new(file), bar.as_ref())?; 64 + if let Some(b) = bar { 65 + b.finish(); 66 + } 67 + Ok(()) 47 68 } 48 69 CmprssOutput::Pipe(mut pipe) => { 49 70 // Create a temporary file to write the tar to 50 71 let mut temp_file = tempfile()?; 51 - self.compress_internal(input, Builder::new(&mut temp_file))?; 72 + self.compress_internal(input, Builder::new(&mut temp_file), None)?; 52 73 53 74 // Reset the file position to the beginning 54 75 temp_file.seek(SeekFrom::Start(0))?; ··· 59 80 } 60 81 CmprssOutput::Writer(mut writer) => { 61 82 let mut temp_file = tempfile()?; 62 - self.compress_internal(input, Builder::new(&mut temp_file))?; 83 + self.compress_internal(input, Builder::new(&mut temp_file), None)?; 63 84 temp_file.seek(SeekFrom::Start(0))?; 64 85 io::copy(&mut temp_file, &mut writer)?; 65 86 Ok(()) ··· 83 104 bail!("tar extraction expects exactly one archive file"); 84 105 } 85 106 let file = File::open(&paths[0])?; 86 - let mut archive = Archive::new(file); 87 - Ok(archive.unpack(out_dir)?) 107 + let size = file.metadata()?.len(); 108 + self.unpack_with_progress(file, Some(size), out_dir) 88 109 } 89 110 CmprssInput::Pipe(mut pipe) => { 90 111 // Create a temporary file to store the tar content ··· 95 116 96 117 // Reset the file position to the beginning 97 118 temp_file.seek(SeekFrom::Start(0))?; 98 - 99 - // Extract from the temporary file 100 - let mut archive = Archive::new(temp_file); 101 - Ok(archive.unpack(out_dir)?) 119 + let size = temp_file.metadata()?.len(); 120 + self.unpack_with_progress(temp_file, Some(size), out_dir) 102 121 } 103 122 CmprssInput::Reader(reader) => { 104 123 let mut archive = Archive::new(reader.0); ··· 153 172 } 154 173 155 174 impl Tar { 156 - /// Internal compress helper 157 - fn compress_internal<W: Write>(&self, input: CmprssInput, mut archive: Builder<W>) -> Result { 175 + /// Internal compress helper. When `bar` is `Some`, recursively walks 176 + /// path inputs ourselves (rather than using `Builder::append_dir_all`) 177 + /// so every file read runs through `ProgressReader`, sharing a single 178 + /// bar across all entries. 179 + fn compress_internal<W: Write>( 180 + &self, 181 + input: CmprssInput, 182 + mut archive: Builder<W>, 183 + bar: Option<&ProgressBar>, 184 + ) -> Result { 158 185 match input { 159 186 CmprssInput::Path(paths) => { 160 187 for path in paths { 188 + let name = path.file_name().unwrap(); 161 189 if path.is_file() { 162 - archive.append_file( 163 - path.file_name().unwrap(), 164 - &mut File::open(path.as_path())?, 165 - )?; 190 + append_file_entry(&mut archive, Path::new(name), &path, bar)?; 166 191 } else if path.is_dir() { 167 - archive.append_dir_all(path.file_name().unwrap(), path.as_path())?; 192 + append_dir_entry(&mut archive, Path::new(name), &path, bar)?; 168 193 } else { 169 194 bail!("tar does not support this file type"); 170 195 } ··· 183 208 } 184 209 Ok(archive.finish()?) 185 210 } 211 + 212 + fn unpack_with_progress<R: Read>( 213 + &self, 214 + reader: R, 215 + size: Option<u64>, 216 + out_dir: &Path, 217 + ) -> Result { 218 + let bar = create_progress_bar(size, self.progress_args.progress, OutputTarget::File); 219 + let reader = ProgressReader::new(reader, bar.clone()); 220 + let mut archive = Archive::new(reader); 221 + archive.unpack(out_dir)?; 222 + if let Some(b) = bar { 223 + b.finish(); 224 + } 225 + Ok(()) 226 + } 227 + } 228 + 229 + /// Append one regular file to the tar archive, wrapping reads in a 230 + /// `ProgressReader` that ticks the shared bar. 231 + fn append_file_entry<W: Write>( 232 + archive: &mut Builder<W>, 233 + archive_name: &Path, 234 + disk_path: &Path, 235 + bar: Option<&ProgressBar>, 236 + ) -> Result { 237 + let mut file = File::open(disk_path)?; 238 + let meta = file.metadata()?; 239 + let mut header = Header::new_gnu(); 240 + header.set_metadata(&meta); 241 + header.set_size(meta.len()); 242 + let reader = ProgressReader::new(&mut file, bar.cloned()); 243 + archive.append_data(&mut header, archive_name, reader)?; 244 + Ok(()) 245 + } 246 + 247 + /// Write the directory header, then recurse into its children. 248 + fn append_dir_entry<W: Write>( 249 + archive: &mut Builder<W>, 250 + archive_name: &Path, 251 + disk_path: &Path, 252 + bar: Option<&ProgressBar>, 253 + ) -> Result { 254 + let meta = std::fs::metadata(disk_path)?; 255 + let mut header = Header::new_gnu(); 256 + header.set_metadata(&meta); 257 + header.set_entry_type(EntryType::Directory); 258 + header.set_size(0); 259 + archive.append_data(&mut header, archive_name, io::empty())?; 260 + for entry in std::fs::read_dir(disk_path)? { 261 + let entry = entry?; 262 + let child_archive = archive_name.join(entry.file_name()); 263 + let child_disk = entry.path(); 264 + if child_disk.is_file() { 265 + append_file_entry(archive, &child_archive, &child_disk, bar)?; 266 + } else if child_disk.is_dir() { 267 + append_dir_entry(archive, &child_archive, &child_disk, bar)?; 268 + } 269 + // Skip symlinks/other types; they weren't handled before either. 270 + } 271 + Ok(()) 186 272 } 187 273 188 274 #[cfg(test)]