Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,30 @@ 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)
.with_include_filter(".txt")
.with_include_filter(".csv")
.run()
.unwrap();


// Copy with progress:
CopyBuilder::new("src", "dest")
.with_progress(|all, done| {
println!("copied {done}/{all}");
})
.run()
.unwrap();
```
7 changes: 4 additions & 3 deletions benches/copy_rustlang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -114,7 +114,8 @@ fn test_lms(c: &mut Criterion) {
.arg("cp")
.arg(SOURCE)
.arg(&format!("{}{}", DEST, random_string()))
.output().unwrap();
.output()
.unwrap();
});
});
}
Expand Down
62 changes: 61 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@
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<dyn Fn(usize, usize)>;

#[derive(Clone)]
/// Recursively copy a directory from a to b.
/// ```
/// use dircpy::*;
Expand All @@ -54,6 +58,15 @@
///.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 {
Expand All @@ -71,6 +84,23 @@
exclude_filters: Vec<String>,
/// A list of exclude filters
include_filters: Vec<String>,
/// An optional progress function. Has a performance penalty as the total number of files need to be calculated.
progress_callback: Option<ProgressFn>,
}

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
Expand Down Expand Up @@ -185,6 +215,7 @@
overwrite_if_size_differs: false,
exclude_filters: vec![],
include_filters: vec![],
progress_callback: None,
}
}

Expand Down Expand Up @@ -212,6 +243,17 @@
}
}

/// 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<F>(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();
Expand Down Expand Up @@ -245,11 +287,27 @@
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))
})?;
Expand Down Expand Up @@ -331,7 +389,7 @@
entry.path().display(),
dest_entry.display()
);
let target = read_link(entry.path())?;

Check warning on line 392 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Run Windows tests

unused variable: `target`

Check warning on line 392 in src/lib.rs

View workflow job for this annotation

GitHub Actions / Check windows-latest

unused variable: `target`
#[cfg(unix)]
std::os::unix::fs::symlink(target, dest_entry)?
} else {
Expand Down Expand Up @@ -394,6 +452,7 @@
overwrite_if_size_differs,
exclude_filters,
include_filters,
progress_callback: None,
}
.run()
}
Expand All @@ -408,6 +467,7 @@
overwrite_if_size_differs: false,
exclude_filters: vec![],
include_filters: vec![],
progress_callback: None,
}
.run()
}
81 changes: 81 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Loading