diff --git a/README.md b/README.md index fa18f9e..f6a05cc 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,17 @@ A cross-platform library to recursively copy directories, with some convenience ```rust - use dircpy::*; +use dircpy::*; - // Most basic example: - copy_dir("src", "dest"); +// Most basic example: +copy_dir("src", "dest"); - // Simple builder example: +// Simple builder example: CopyBuilder::new("src", "dest") .run() .unwrap(); - // Copy recursively, only including certain files: +// Copy recursively, only including certain files: CopyBuilder::new("src", "dest") .overwrite_if_newer(true) .overwrite_if_size_differs(true) @@ -30,5 +30,12 @@ CopyBuilder::new("src", "dest") .with_include_filter(".csv") .run() .unwrap(); - + +// Copy with progress: +CopyBuilder::new("src", "dest") + .with_progress(|all, done| { + println!("copied {done}/{all}"); + }) + .run() + .unwrap(); ``` diff --git a/benches/copy_rustlang.rs b/benches/copy_rustlang.rs index 949a131..905deaf 100644 --- a/benches/copy_rustlang.rs +++ b/benches/copy_rustlang.rs @@ -65,12 +65,12 @@ fn test_cp(c: &mut Criterion) { .arg("-r") .arg(SOURCE) .arg(&format!("{}{}", DEST, random_string())) - .output().unwrap(); + .output() + .unwrap(); }); }); } - fn test_dircpy_single(c: &mut Criterion) { // One-time setup code goes here // download_and_unpack(SAMPLE_DATA, source); @@ -114,7 +114,8 @@ fn test_lms(c: &mut Criterion) { .arg("cp") .arg(SOURCE) .arg(&format!("{}{}", DEST, random_string())) - .output().unwrap(); + .output() + .unwrap(); }); }); } diff --git a/src/lib.rs b/src/lib.rs index 502d491..36eb22b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,13 +27,17 @@ use jwalk::WalkDir as JWalkDir; use std::fs::{copy, read_link}; use std::io::{Error, ErrorKind}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::SystemTime; use walkdir::WalkDir; #[cfg(test)] mod tests; -#[derive(Debug, Clone)] +/// A function to output progess in the form total files, copied files +type ProgressFn = Arc; + +#[derive(Clone)] /// Recursively copy a directory from a to b. /// ``` /// use dircpy::*; @@ -54,6 +58,15 @@ mod tests; ///.with_include_filter(".csv") ///.run() ///.unwrap(); +/// +/// // Copy with progress: +///CopyBuilder::new("src", "dest") +///.with_progress(|all, done| { +/// println!("copied {done}/{all}"); +///}) +///.run() +///.unwrap(); +/// /// ``` pub struct CopyBuilder { @@ -71,6 +84,23 @@ pub struct CopyBuilder { exclude_filters: Vec, /// A list of exclude filters include_filters: Vec, + /// An optional progress function. Has a performance penalty as the total number of files need to be calculated. + progress_callback: Option, +} + +impl std::fmt::Debug for CopyBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CopyBuilder") + .field("source", &self.source) + .field("destination", &self.destination) + .field("overwrite_all", &self.overwrite_all) + .field("overwrite_if_newer", &self.overwrite_if_newer) + .field("overwrite_if_size_differs", &self.overwrite_if_size_differs) + .field("exclude_filters", &self.exclude_filters) + .field("include_filters", &self.include_filters) + .field("has_progress_callback", &self.progress_callback.is_some()) + .finish() + } } /// Determine if the modification date of file_a is newer than that of file_b @@ -185,6 +215,7 @@ impl CopyBuilder { overwrite_if_size_differs: false, exclude_filters: vec![], include_filters: vec![], + progress_callback: None, } } @@ -212,6 +243,17 @@ impl CopyBuilder { } } + /// Supply a callback function to be executed on each copy operation. It supplies the total number of files and the files already copied. + pub fn with_progress(self, callback: F) -> CopyBuilder + where + F: Fn(usize, usize) + 'static, + { + CopyBuilder { + progress_callback: Some(Arc::new(callback)), + ..self + } + } + /// Do not copy files that contain this string pub fn with_exclude_filter(self, f: &str) -> CopyBuilder { let mut filters = self.exclude_filters.clone(); @@ -245,11 +287,27 @@ impl CopyBuilder { abs_dest.display() ); + let mut num_files_total = 1; + let mut num_files_processed = 0; + + if self.progress_callback.is_some() { + num_files_total = WalkDir::new(&abs_source) + .into_iter() + .filter_entry(|e| e.path() != abs_dest) + .filter_map(|e| e.ok()) + .count(); + } + 'files: for entry in WalkDir::new(&abs_source) .into_iter() .filter_entry(|e| e.path() != abs_dest) .filter_map(|e| e.ok()) { + if let Some(cb) = &self.progress_callback { + num_files_processed += 1; + cb(num_files_total, num_files_processed); + } + let rel_dest = entry.path().strip_prefix(&abs_source).map_err(|e| { Error::new(ErrorKind::Other, format!("Could not strip prefix: {:?}", e)) })?; @@ -394,6 +452,7 @@ pub fn copy_dir_advanced, Q: AsRef>( overwrite_if_size_differs, exclude_filters, include_filters, + progress_callback: None, } .run() } @@ -408,6 +467,7 @@ pub fn copy_dir, Q: AsRef>(source: P, dest: Q) -> Result<() overwrite_if_size_differs: false, exclude_filters: vec![], include_filters: vec![], + progress_callback: None, } .run() } diff --git a/src/tests.rs b/src/tests.rs index f00f9e6..57dff82 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -76,6 +76,40 @@ fn copy_subdir() { std::fs::remove_dir_all("source").unwrap(); } +#[cfg(feature = "jwalk")] +#[test] +fn copy_subdir_jwalk() { + use std::fs::File; + use std::io::Write; + + let source_dir = "overwrite_sourcej"; + let dest_dir = "overwrite_destj"; + + std::env::set_var("RUST_LOG", "debug"); + let _ = env_logger::try_init(); + create_dir_all(source_dir).unwrap(); + create_dir_all(dest_dir).unwrap(); + File::create(format!("{source_dir}/a.txt")).unwrap(); + let mut file_b = File::create(format!("{source_dir}/b.txt")).unwrap(); + + let contents = "Contents changed"; + // Copy once, both files are empty + CopyBuilder::new(source_dir, dest_dir).run().unwrap(); + // write something to file b so we can check if we overwrite it + write!(file_b, "{contents}").unwrap(); + // perform a second copy + CopyBuilder::new(source_dir, dest_dir) + .overwrite(true) + .run() + .unwrap(); + // make sure the contents of b are now changed + let s = read_to_string(File::open(format!("{dest_dir}/b.txt")).unwrap()).unwrap(); + assert!(s == contents, "Destination was not overwritten"); + + std::fs::remove_dir_all(source_dir).unwrap(); + std::fs::remove_dir_all(dest_dir).unwrap(); +} + #[test] fn copy_overwrite() { use std::fs::File; @@ -235,3 +269,50 @@ fn copy_cargo() { std::fs::remove_dir_all(output_dir).unwrap(); std::fs::remove_file(archive).unwrap(); } + +#[test] +fn copy_cargo_progress() { + std::env::set_var("RUST_LOG", "INFO"); + let _ = env_logger::builder().try_init(); + + let url = "https://github.com/rust-lang/cargo/archive/master.zip"; + let sample_dir = "cargo_progress"; + let output_dir = format!("{sample_dir}_output"); + let archive = format!("{sample_dir}.zip"); + info!("Expanding {archive}"); + + let mut resp = reqwest::blocking::get(url).unwrap(); + let mut out = File::create(&archive).expect("failed to create file"); + std::io::copy(&mut resp, &mut out).expect("failed to copy content"); + + let reader = std::fs::File::open(&archive).unwrap(); + + unzip::Unzipper::new(reader, &sample_dir) + .unzip() + .expect("Could not expand cargo sources"); + let num_input_files = WalkDir::new(&sample_dir) + .into_iter() + .filter_map(|e| e.ok()) + .count(); + + CopyBuilder::new( + &Path::new(&sample_dir).canonicalize().unwrap(), + &PathBuf::from(&output_dir), + ) + .with_progress(|all, done| { + info!("copied {done}/{all}"); + }) + .run() + .unwrap(); + + let num_output_files = WalkDir::new(&output_dir) + .into_iter() + .filter_map(|e| e.ok()) + .count(); + + assert_eq!(num_output_files, num_input_files); + + std::fs::remove_dir_all(sample_dir).unwrap(); + std::fs::remove_dir_all(output_dir).unwrap(); + std::fs::remove_file(archive).unwrap(); +}