From c597ec8a40d4e0d06439b23c6af9e96600fbcfbd Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 14 Mar 2026 13:58:42 +0100 Subject: [PATCH 1/8] Cleanup and docs --- benches/copy_rustlang.rs | 7 ++--- src/lib.rs | 58 +++++++++++++++++++++++++++++++++++++++- src/tests.rs | 46 +++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) 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..b8e76ec 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,19 @@ 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("has_progress_callback", &self.progress_callback.is_some()) + // ... other fields ... + .finish() + } } /// Determine if the modification date of file_a is newer than that of file_b @@ -185,6 +211,7 @@ impl CopyBuilder { overwrite_if_size_differs: false, exclude_filters: vec![], include_filters: vec![], + progress_callback: None, } } @@ -212,6 +239,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) + Send + Sync + '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 +283,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 +448,7 @@ pub fn copy_dir_advanced, Q: AsRef>( overwrite_if_size_differs, exclude_filters, include_filters, + progress_callback: None, } .run() } @@ -408,6 +463,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..1402ae8 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -235,3 +235,49 @@ 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"; + 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(); +} From ab9874becf6be8fe9f681ebf422cb7f8db4ba1dc Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Fri, 20 Mar 2026 23:56:07 +0100 Subject: [PATCH 2/8] fix parallel tests --- src/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests.rs b/src/tests.rs index 1402ae8..dc8781f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -241,7 +241,7 @@ 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"; + let sample_dir = "cargo_progress"; let output_dir = format!("{sample_dir}_output"); let archive = format!("{sample_dir}.zip"); info!("Expanding {archive}"); From efc05089f3ae9f7553217d4308e0bd8e653d04e2 Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Fri, 20 Mar 2026 23:59:15 +0100 Subject: [PATCH 3/8] remove send and sync --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b8e76ec..8e0b3b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,7 @@ use walkdir::WalkDir; mod tests; /// A function to output progess in the form total files, copied files -type ProgressFn = Arc; +type ProgressFn = Arc; #[derive(Clone)] /// Recursively copy a directory from a to b. @@ -242,7 +242,7 @@ 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) + Send + Sync + 'static, + F: Fn(usize, usize) + 'static, { CopyBuilder { progress_callback: Some(Arc::new(callback)), From b83358669fb3bd05bb3a39d52d4952c4e4056ea3 Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 21 Mar 2026 00:02:49 +0100 Subject: [PATCH 4/8] Add progress instructions --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fa18f9e..9832a11 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,13 @@ 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(); ``` From bbf44d6e0fa28decc5a1a53d44a7e8e10a8a6134 Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 21 Mar 2026 07:13:21 +0100 Subject: [PATCH 5/8] Programatically get test name --- src/tests.rs | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/tests.rs b/src/tests.rs index dc8781f..ee94bf4 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; @@ -240,8 +274,10 @@ fn copy_cargo() { fn copy_cargo_progress() { std::env::set_var("RUST_LOG", "INFO"); let _ = env_logger::builder().try_init(); + let test_name = std::thread::current().name().unwrap().to_string(); + let url = "https://github.com/rust-lang/cargo/archive/master.zip"; - let sample_dir = "cargo_progress"; + let sample_dir = test_name; let output_dir = format!("{sample_dir}_output"); let archive = format!("{sample_dir}.zip"); info!("Expanding {archive}"); @@ -252,7 +288,7 @@ fn copy_cargo_progress() { let reader = std::fs::File::open(&archive).unwrap(); - unzip::Unzipper::new(reader, sample_dir) + unzip::Unzipper::new(reader, &sample_dir) .unzip() .expect("Could not expand cargo sources"); let num_input_files = WalkDir::new(&sample_dir) @@ -261,7 +297,7 @@ fn copy_cargo_progress() { .count(); CopyBuilder::new( - &Path::new(sample_dir).canonicalize().unwrap(), + &Path::new(&sample_dir).canonicalize().unwrap(), &PathBuf::from(&output_dir), ) .with_progress(|all, done| { From 6ba6c0a3b034fdf20f343ca0d6b4204df8d8d5bf Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 21 Mar 2026 07:22:59 +0100 Subject: [PATCH 6/8] revert name as the test fails on windows --- src/tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests.rs b/src/tests.rs index ee94bf4..57dff82 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -274,10 +274,9 @@ fn copy_cargo() { fn copy_cargo_progress() { std::env::set_var("RUST_LOG", "INFO"); let _ = env_logger::builder().try_init(); - let test_name = std::thread::current().name().unwrap().to_string(); let url = "https://github.com/rust-lang/cargo/archive/master.zip"; - let sample_dir = test_name; + let sample_dir = "cargo_progress"; let output_dir = format!("{sample_dir}_output"); let archive = format!("{sample_dir}.zip"); info!("Expanding {archive}"); From 3fc15cdfaad1fea5f7b5b3d039e79be705923310 Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Sat, 21 Mar 2026 07:23:48 +0100 Subject: [PATCH 7/8] Add missing fields for Debug --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8e0b3b8..36eb22b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,8 +93,12 @@ impl std::fmt::Debug for CopyBuilder { 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()) - // ... other fields ... .finish() } } From d6129fafef7e3308731bb32befb2b146ac0222de Mon Sep 17 00:00:00 2001 From: Johann Woelper Date: Tue, 24 Mar 2026 19:52:30 +0100 Subject: [PATCH 8/8] cleanup --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9832a11..f6a05cc 100644 --- a/README.md +++ b/README.md @@ -38,5 +38,4 @@ CopyBuilder::new("src", "dest") }) .run() .unwrap(); - ```