├── run_bench.sh ├── .gitignore ├── Changelog.md ├── .github └── workflows │ ├── test_linux.yml │ └── test_windows.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── copy_rustlang.rs └── src ├── tests.rs └── lib.rs /run_bench.sh: -------------------------------------------------------------------------------- 1 | cargo install lms 2 | cargo bench 3 | echo "Cleanup" 4 | rm -rf bench_data -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .history 4 | sample 5 | output* 6 | sample* 7 | bench_data 8 | dest -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 0.3.19: 2 | Fix for empty include lists (Thanks @AdamLeyshon) 3 | Add test for empty include lists 4 | 0.3.18: 5 | Update walkdir 6 | 0.3.17: 7 | Fixed bug where multiple include patterns would not work (Thanks @giovannimirarchi420) 8 | -------------------------------------------------------------------------------- /.github/workflows/test_linux.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: Test Linux 4 | 5 | jobs: 6 | check: 7 | name: Test ubuntu-latest 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Run cargo check 12 | uses: actions-rs/cargo@v1 13 | with: 14 | command: check -------------------------------------------------------------------------------- /.github/workflows/test_windows.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: Test Windows 4 | 5 | jobs: 6 | check: 7 | name: Test windows-latest 8 | runs-on: windows-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Run cargo check 12 | uses: actions-rs/cargo@v1 13 | with: 14 | command: check -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dircpy" 3 | version = "0.3.19" 4 | authors = ["Johann Woelper "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "Copy directories recursively with flexible options." 8 | repository = "https://github.com/woelper/dircpy/" 9 | keywords = ["copy", "recursive", "filesystem", "file"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [features] 14 | default = ["jwalk"] 15 | 16 | [dependencies] 17 | walkdir = "2.5" 18 | log = "0.4" 19 | # rayon = "1.4.0" 20 | jwalk = { version = "0.8", optional = true } 21 | 22 | [dev-dependencies] 23 | unzip = "0.1" 24 | reqwest = { version = "0.12", features = ["blocking"] } 25 | env_logger = "0.11" 26 | criterion = "0.5" 27 | 28 | [[bench]] 29 | name = "copy_rustlang" 30 | harness = false 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Johann Woelper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dircpy 2 | [![Crates.io](https://img.shields.io/crates/v/dircpy.svg)](https://crates.io/crates/dircpy) 3 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/woelper/dircpy/blob/master/LICENSE) 4 | [![Docs Status](https://docs.rs/dircpy/badge.svg)](https://docs.rs/dircpy) 5 | 6 | ![Crates.io](https://img.shields.io/crates/d/dircpy?label=crates.io%20downloads) 7 | 8 | [![Test Linux](https://github.com/woelper/dircpy/actions/workflows/test_linux.yml/badge.svg)](https://github.com/woelper/dircpy/actions/workflows/test_linux.yml) 9 | [![Test Windows](https://github.com/woelper/dircpy/actions/workflows/test_windows.yml/badge.svg)](https://github.com/woelper/dircpy/actions/workflows/test_windows.yml) 10 | 11 | A cross-platform library to recursively copy directories, with some convenience added. 12 | 13 | 14 | ```rust 15 | use dircpy::*; 16 | 17 | // Most basic example: 18 | copy_dir("src", "dest"); 19 | 20 | // Simple builder example: 21 | CopyBuilder::new("src", "dest") 22 | .run() 23 | .unwrap(); 24 | 25 | // Copy recursively, only including certain files: 26 | CopyBuilder::new("src", "dest") 27 | .overwrite_if_newer(true) 28 | .overwrite_if_size_differs(true) 29 | .with_include_filter(".txt") 30 | .with_include_filter(".csv") 31 | .run() 32 | .unwrap(); 33 | 34 | ``` 35 | -------------------------------------------------------------------------------- /benches/copy_rustlang.rs: -------------------------------------------------------------------------------- 1 | use criterion::*; 2 | use dircpy::CopyBuilder; 3 | use env_logger; 4 | use log::*; 5 | use std::fs::File; 6 | use unzip::Unzipper; 7 | 8 | //const SAMPLE_DATA: &str = "https://github.com/rust-lang/rust/archive/master.zip"; 9 | const SAMPLE_DATA: &str = "https://github.com/rust-lang/cargo/archive/master.zip"; 10 | const SOURCE: &str = "bench_data/source"; 11 | const DEST: &str = "bench_data/dest"; 12 | 13 | fn random_string() -> String { 14 | format!("{:?}", std::time::Instant::now()) 15 | } 16 | 17 | fn download_and_unpack(url: &str, name: &str) { 18 | let archive = format!("{}.zip", name); 19 | 20 | if !std::path::Path::new(&archive).is_file() { 21 | info!("Downloading {:?}", url); 22 | 23 | let mut resp = reqwest::blocking::get(url).unwrap(); 24 | let mut out = File::create(&archive).expect("failed to create file"); 25 | std::io::copy(&mut resp, &mut out).expect("failed to copy content"); 26 | } else { 27 | info!("Did not download, archive already present"); 28 | } 29 | 30 | info!("Unzipping..."); 31 | 32 | Unzipper::new(File::open(&archive).unwrap(), name) 33 | .unzip() 34 | .unwrap(); 35 | info!("Done. Ready."); 36 | } 37 | 38 | fn setup(_: &mut Criterion) { 39 | std::env::set_var("RUST_LOG", "INFO"); 40 | let _ = env_logger::builder().try_init(); 41 | std::fs::create_dir_all(SOURCE).unwrap(); 42 | download_and_unpack(SAMPLE_DATA, SOURCE); 43 | } 44 | 45 | fn teardown(_: &mut Criterion) { 46 | std::env::set_var("RUST_LOG", "INFO"); 47 | let _ = env_logger::builder().try_init(); 48 | // One-time setup code goes here 49 | info!("CLEANUP"); 50 | // let _ = std::fs::remove_dir_all(source); 51 | let _ = std::fs::remove_dir_all(DEST); 52 | // let _ = std::fs::remove_file(archive); 53 | info!("DONE"); 54 | } 55 | 56 | fn test_cp(c: &mut Criterion) { 57 | std::env::set_var("RUST_LOG", "INFO"); 58 | let _ = env_logger::builder().try_init(); 59 | // One-time setup code goes here 60 | c.bench_function("cp -r", |b| { 61 | // Per-sample (note that a sample can be many iterations) setup goes here 62 | b.iter(|| { 63 | // Measured code goes here 64 | std::process::Command::new("cp") 65 | .arg("-r") 66 | .arg(SOURCE) 67 | .arg(&format!("{}{}", DEST, random_string())) 68 | .output().unwrap(); 69 | }); 70 | }); 71 | } 72 | 73 | 74 | fn test_dircpy_single(c: &mut Criterion) { 75 | // One-time setup code goes here 76 | // download_and_unpack(SAMPLE_DATA, source); 77 | c.bench_function("cpy single threaded", |b| { 78 | // Per-sample (note that a sample can be many iterations) setup goes here 79 | b.iter(|| { 80 | // Measured code goes here 81 | CopyBuilder::new(&SOURCE, &format!("{}{}", DEST, random_string())) 82 | .overwrite(true) 83 | .run() 84 | .unwrap(); 85 | }); 86 | }); 87 | } 88 | 89 | fn test_dircpy_parallel(c: &mut Criterion) { 90 | // One-time setup code goes here 91 | #[cfg(feature = "jwalk")] 92 | c.bench_function("cpy multi-threaded", |b| { 93 | // Per-sample (note that a sample can be many iterations) setup goes here 94 | b.iter(|| { 95 | // Measured code goes here 96 | CopyBuilder::new(&SOURCE, &format!("{}{}", DEST, random_string())) 97 | .overwrite(true) 98 | .run_par() 99 | .unwrap(); 100 | }); 101 | }); 102 | } 103 | 104 | fn test_lms(c: &mut Criterion) { 105 | std::env::set_var("RUST_LOG", "INFO"); 106 | let _ = env_logger::builder().try_init(); 107 | // One-time setup code goes here 108 | // download_and_unpack(SAMPLE_DATA, source); 109 | c.bench_function("lms", |b| { 110 | // Per-sample (note that a sample can be many iterations) setup goes here 111 | b.iter(|| { 112 | // Measured code goes here 113 | std::process::Command::new("lms") 114 | .arg("cp") 115 | .arg(SOURCE) 116 | .arg(&format!("{}{}", DEST, random_string())) 117 | .output().unwrap(); 118 | }); 119 | }); 120 | } 121 | 122 | criterion_group! { 123 | name = benches; 124 | // This can be any expression that returns a `Criterion` object. 125 | config = Criterion::default() 126 | .sample_size(10) 127 | // .sampling_mode() 128 | .warm_up_time(std::time::Duration::from_secs(4)) 129 | .measurement_time(std::time::Duration::from_secs(6)) 130 | ; 131 | targets = setup, test_dircpy_single, test_dircpy_parallel, test_cp, test_lms, teardown 132 | } 133 | criterion_main!(benches); 134 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::fs::create_dir_all; 3 | use std::fs::File; 4 | use std::io::read_to_string; 5 | #[cfg(unix)] 6 | use std::os::unix::fs::{symlink, PermissionsExt}; 7 | 8 | #[test] 9 | fn copy_basic() { 10 | std::env::set_var("RUST_LOG", "DEBUG"); 11 | let _ = env_logger::builder().try_init(); 12 | 13 | let src = "basic_src"; 14 | let dst = "basic_dest"; 15 | 16 | create_dir_all(format!("{src}/level1/level2/level3")).unwrap(); 17 | 18 | File::create(format!("{src}/test")).unwrap(); 19 | File::create(format!("{src}/level1/other_file")).unwrap(); 20 | 21 | #[cfg(unix)] 22 | { 23 | File::create(format!("{src}/exec_file")).unwrap(); 24 | std::fs::set_permissions( 25 | format!("{src}/exec_file"), 26 | std::fs::Permissions::from_mode(0o755), 27 | ) 28 | .unwrap(); 29 | symlink("exec_file", format!("{src}/symlink")).unwrap(); 30 | symlink("does_not_exist", format!("{src}/dangling_symlink")).unwrap(); 31 | } 32 | 33 | CopyBuilder::new(src, dst) 34 | .overwrite(true) 35 | .overwrite_if_newer(true) 36 | .run() 37 | .unwrap(); 38 | 39 | #[cfg(unix)] 40 | { 41 | let f = File::open(format!("{dst}/exec_file")).unwrap(); 42 | let metadata = f.metadata().unwrap(); 43 | let permissions = metadata.permissions(); 44 | println!("permissions: {:o}", permissions.mode()); 45 | assert_eq!(permissions.mode(), 33261); 46 | assert_eq!( 47 | Path::new("exec_file"), 48 | read_link(format!("{dst}/symlink")).unwrap().as_path() 49 | ); 50 | assert_eq!( 51 | Path::new("does_not_exist"), 52 | read_link(format!("{dst}/dangling_symlink")) 53 | .unwrap() 54 | .as_path() 55 | ); 56 | } 57 | 58 | // clean up 59 | std::fs::remove_dir_all(src).unwrap(); 60 | std::fs::remove_dir_all(dst).unwrap(); 61 | } 62 | 63 | #[test] 64 | fn copy_subdir() { 65 | std::env::set_var("RUST_LOG", "debug"); 66 | let _ = env_logger::try_init(); 67 | create_dir_all("source/subdir").unwrap(); 68 | create_dir_all("source/this_should_copy").unwrap(); 69 | File::create("source/this_should_copy/file.doc").unwrap(); 70 | File::create("source/a.jpg").unwrap(); 71 | File::create("source/b.jpg").unwrap(); 72 | File::create("source/d.txt").unwrap(); 73 | 74 | CopyBuilder::new("source", "source/subdir").run().unwrap(); 75 | 76 | std::fs::remove_dir_all("source").unwrap(); 77 | } 78 | 79 | #[test] 80 | fn copy_overwrite() { 81 | use std::fs::File; 82 | use std::io::Write; 83 | 84 | let source_dir = "overwrite_source"; 85 | let dest_dir = "overwrite_dest"; 86 | 87 | std::env::set_var("RUST_LOG", "debug"); 88 | let _ = env_logger::try_init(); 89 | create_dir_all(source_dir).unwrap(); 90 | create_dir_all(dest_dir).unwrap(); 91 | File::create(format!("{source_dir}/a.txt")).unwrap(); 92 | let mut file_b = File::create(format!("{source_dir}/b.txt")).unwrap(); 93 | 94 | let contents = "Contents changed"; 95 | // Copy once, both files are empty 96 | CopyBuilder::new(source_dir, dest_dir).run().unwrap(); 97 | // write something to file b so we can check if we overwrite it 98 | write!(file_b, "{contents}").unwrap(); 99 | // perform a second copy 100 | CopyBuilder::new(source_dir, dest_dir) 101 | .overwrite(true) 102 | .run() 103 | .unwrap(); 104 | // make sure the contents of b are now changed 105 | let s = read_to_string(File::open(format!("{dest_dir}/b.txt")).unwrap()).unwrap(); 106 | assert!(s == contents, "Destination was not overwritten"); 107 | 108 | std::fs::remove_dir_all(source_dir).unwrap(); 109 | std::fs::remove_dir_all(dest_dir).unwrap(); 110 | } 111 | 112 | #[test] 113 | fn copy_exclude() { 114 | std::env::set_var("RUST_LOG", "DEBUG"); 115 | let _ = env_logger::builder().try_init(); 116 | 117 | let src = "ex_src"; 118 | let dst = "ex_dest"; 119 | 120 | create_dir_all(src).unwrap(); 121 | File::create(format!("{src}/foo")).unwrap(); 122 | File::create(format!("{src}/bar")).unwrap(); 123 | 124 | CopyBuilder::new(src, dst) 125 | .overwrite(true) 126 | .overwrite_if_newer(true) 127 | .with_exclude_filter("foo") 128 | .run() 129 | .unwrap(); 130 | 131 | assert!(!Path::new(&format!("{}/foo", dst)).is_file()); 132 | 133 | // clean up 134 | std::fs::remove_dir_all(src).unwrap(); 135 | std::fs::remove_dir_all(dst).unwrap(); 136 | } 137 | 138 | #[test] 139 | fn copy_include() { 140 | std::env::set_var("RUST_LOG", "DEBUG"); 141 | let _ = env_logger::builder().try_init(); 142 | 143 | let src = "in_src"; 144 | let dst = "in_dest"; 145 | 146 | create_dir_all(src).unwrap(); 147 | File::create(format!("{src}/foo")).unwrap(); 148 | File::create(format!("{src}/bar")).unwrap(); 149 | File::create(format!("{src}/baz")).unwrap(); 150 | 151 | CopyBuilder::new(src, dst) 152 | .overwrite(true) 153 | .overwrite_if_newer(true) 154 | .with_include_filter("foo") 155 | .with_include_filter("baz") 156 | .run() 157 | .unwrap(); 158 | 159 | assert!(Path::new(&format!("{dst}/foo")).is_file()); 160 | assert!(!Path::new(&format!("{dst}/bar")).exists()); 161 | assert!(Path::new(&format!("{dst}/baz")).exists()); 162 | 163 | // clean up 164 | std::fs::remove_dir_all(src).unwrap(); 165 | std::fs::remove_dir_all(dst).unwrap(); 166 | } 167 | 168 | #[test] 169 | fn copy_empty_include() { 170 | std::env::set_var("RUST_LOG", "DEBUG"); 171 | let _ = env_logger::builder().try_init(); 172 | 173 | let src = "in_src_inc"; 174 | let dst = "in_dest_inc"; 175 | 176 | create_dir_all(src).unwrap(); 177 | File::create(format!("{src}/foo")).unwrap(); 178 | File::create(format!("{src}/bar")).unwrap(); 179 | File::create(format!("{src}/baz")).unwrap(); 180 | 181 | CopyBuilder::new(src, dst) 182 | .overwrite(true) 183 | .overwrite_if_newer(true) 184 | .run() 185 | .unwrap(); 186 | 187 | assert!(Path::new(&format!("{dst}/foo")).is_file()); 188 | assert!(Path::new(&format!("{dst}/bar")).exists()); 189 | assert!(Path::new(&format!("{dst}/baz")).exists()); 190 | 191 | // clean up 192 | std::fs::remove_dir_all(src).unwrap(); 193 | std::fs::remove_dir_all(dst).unwrap(); 194 | } 195 | 196 | #[test] 197 | fn copy_cargo() { 198 | std::env::set_var("RUST_LOG", "DEBUG"); 199 | let _ = env_logger::builder().try_init(); 200 | let url = "https://github.com/rust-lang/cargo/archive/master.zip"; 201 | let sample_dir = "cargo"; 202 | let output_dir = format!("{sample_dir}_output"); 203 | let archive = format!("{sample_dir}.zip"); 204 | info!("Expanding {archive}"); 205 | 206 | let mut resp = reqwest::blocking::get(url).unwrap(); 207 | let mut out = File::create(&archive).expect("failed to create file"); 208 | std::io::copy(&mut resp, &mut out).expect("failed to copy content"); 209 | 210 | let reader = std::fs::File::open(&archive).unwrap(); 211 | 212 | unzip::Unzipper::new(reader, sample_dir) 213 | .unzip() 214 | .expect("Could not expand cargo sources"); 215 | let num_input_files = WalkDir::new(&sample_dir) 216 | .into_iter() 217 | .filter_map(|e| e.ok()) 218 | .count(); 219 | 220 | CopyBuilder::new( 221 | &Path::new(sample_dir).canonicalize().unwrap(), 222 | &PathBuf::from(&output_dir), 223 | ) 224 | .run() 225 | .unwrap(); 226 | 227 | let num_output_files = WalkDir::new(&output_dir) 228 | .into_iter() 229 | .filter_map(|e| e.ok()) 230 | .count(); 231 | 232 | assert_eq!(num_output_files, num_input_files); 233 | 234 | std::fs::remove_dir_all(sample_dir).unwrap(); 235 | std::fs::remove_dir_all(output_dir).unwrap(); 236 | std::fs::remove_file(archive).unwrap(); 237 | } 238 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Recursively copy a directory from a to b. 2 | //! ``` 3 | //! use dircpy::*; 4 | //! 5 | //! // Most basic example: 6 | //! copy_dir("src", "dest").unwrap(); 7 | //! 8 | //! // Simple builder example: 9 | //!CopyBuilder::new("src", "dest") 10 | //!.run() 11 | //!.unwrap(); 12 | //! 13 | //! // Copy recursively, only including certain files: 14 | //!CopyBuilder::new("src", "dest") 15 | //!.overwrite_if_newer(true) 16 | //!.overwrite_if_size_differs(true) 17 | //!.with_include_filter(".txt") 18 | //!.with_include_filter(".csv") 19 | //!.run() 20 | //!.unwrap(); 21 | //! ``` 22 | 23 | use log::*; 24 | // use rayon::prelude::*; 25 | #[cfg(feature = "jwalk")] 26 | use jwalk::WalkDir as JWalkDir; 27 | use std::fs::{copy, read_link}; 28 | use std::io::{Error, ErrorKind}; 29 | use std::path::{Path, PathBuf}; 30 | use std::time::SystemTime; 31 | use walkdir::WalkDir; 32 | 33 | #[cfg(test)] 34 | mod tests; 35 | 36 | #[derive(Debug, Clone)] 37 | /// Recursively copy a directory from a to b. 38 | /// ``` 39 | /// use dircpy::*; 40 | /// 41 | /// // Most basic example: 42 | /// copy_dir("src", "dest"); 43 | /// 44 | /// // Simple builder example: 45 | ///CopyBuilder::new("src", "dest") 46 | ///.run() 47 | ///.unwrap(); 48 | /// 49 | /// // Copy recursively, only including certain files: 50 | ///CopyBuilder::new("src", "dest") 51 | ///.overwrite_if_newer(true) 52 | ///.overwrite_if_size_differs(true) 53 | ///.with_include_filter(".txt") 54 | ///.with_include_filter(".csv") 55 | ///.run() 56 | ///.unwrap(); 57 | /// ``` 58 | 59 | pub struct CopyBuilder { 60 | /// The source directory 61 | pub source: PathBuf, 62 | /// The destination directory 63 | pub destination: PathBuf, 64 | /// Overwrite all files in target, if already existing 65 | overwrite_all: bool, 66 | /// Overwrite target files if they are newer 67 | overwrite_if_newer: bool, 68 | /// Overwrite target files if they differ in size 69 | overwrite_if_size_differs: bool, 70 | /// A list of include filters 71 | exclude_filters: Vec, 72 | /// A list of exclude filters 73 | include_filters: Vec, 74 | } 75 | 76 | /// Determine if the modification date of file_a is newer than that of file_b 77 | fn is_file_newer(file_a: &Path, file_b: &Path) -> bool { 78 | match (file_a.symlink_metadata(), file_b.symlink_metadata()) { 79 | (Ok(meta_a), Ok(meta_b)) => { 80 | meta_a.modified().unwrap_or_else(|_| SystemTime::now()) 81 | > meta_b.modified().unwrap_or(SystemTime::UNIX_EPOCH) 82 | } 83 | _ => false, 84 | } 85 | } 86 | 87 | /// Determine if file_a and file_b's size differs. 88 | fn is_filesize_different(file_a: &Path, file_b: &Path) -> bool { 89 | match (file_a.symlink_metadata(), file_b.symlink_metadata()) { 90 | (Ok(meta_a), Ok(meta_b)) => meta_a.len() != meta_b.len(), 91 | _ => false, 92 | } 93 | } 94 | 95 | #[cfg(feature = "jwalk")] 96 | fn copy_file(source: &Path, options: CopyBuilder) -> Result<(), std::io::Error> { 97 | let abs_source = options.source.canonicalize()?; 98 | let abs_dest = options.destination.canonicalize()?; 99 | 100 | let rel_dest = source 101 | .strip_prefix(&abs_source) 102 | .map_err(|e| Error::new(ErrorKind::Other, format!("Could not strip prefix: {:?}", e)))?; 103 | let dest_entry = abs_dest.join(rel_dest); 104 | 105 | if source.is_file() { 106 | // the source exists 107 | 108 | // Early out if target is present and overwrite is off 109 | if !options.overwrite_all 110 | && dest_entry.is_file() 111 | && !options.overwrite_if_newer 112 | && !options.overwrite_if_size_differs 113 | { 114 | return Ok(()); 115 | } 116 | 117 | for f in &options.exclude_filters { 118 | if source.to_string_lossy().contains(f) { 119 | return Ok(()); 120 | } 121 | } 122 | 123 | for f in &options.include_filters { 124 | if !source.to_string_lossy().contains(f) { 125 | return Ok(()); 126 | } 127 | } 128 | 129 | // File is not present: copy it 130 | if !dest_entry.is_file() { 131 | debug!( 132 | "Dest not present: CP {} DST {}", 133 | source.display(), 134 | dest_entry.display() 135 | ); 136 | copy(source, dest_entry)?; 137 | return Ok(()); 138 | } 139 | 140 | // File newer? 141 | if options.overwrite_if_newer { 142 | if is_file_newer(source, &dest_entry) { 143 | debug!( 144 | "Source newer: CP {} DST {}", 145 | source.display(), 146 | dest_entry.display() 147 | ); 148 | copy(source, &dest_entry)?; 149 | } 150 | return Ok(()); 151 | } 152 | 153 | // Different size? 154 | if options.overwrite_if_size_differs { 155 | if is_filesize_different(source, &dest_entry) { 156 | debug!( 157 | "Source differs: CP {} DST {}", 158 | source.display(), 159 | dest_entry.display() 160 | ); 161 | copy(source, &dest_entry)?; 162 | } 163 | return Ok(()); 164 | } 165 | 166 | // The regular copy operation 167 | debug!("CP {} DST {}", source.display(), dest_entry.display()); 168 | copy(source, dest_entry)?; 169 | } else if source.is_dir() && !dest_entry.is_dir() { 170 | debug!("MKDIR {}", source.display()); 171 | std::fs::create_dir_all(dest_entry)?; 172 | } 173 | 174 | Ok(()) 175 | } 176 | 177 | impl CopyBuilder { 178 | /// Construct a new CopyBuilder with `source` and `dest`. 179 | pub fn new, Q: AsRef>(source: P, dest: Q) -> CopyBuilder { 180 | CopyBuilder { 181 | source: source.as_ref().to_path_buf(), 182 | destination: dest.as_ref().to_path_buf(), 183 | overwrite_all: false, 184 | overwrite_if_newer: false, 185 | overwrite_if_size_differs: false, 186 | exclude_filters: vec![], 187 | include_filters: vec![], 188 | } 189 | } 190 | 191 | /// Overwrite target files (off by default) 192 | pub fn overwrite(self, overwrite: bool) -> CopyBuilder { 193 | CopyBuilder { 194 | overwrite_all: overwrite, 195 | ..self 196 | } 197 | } 198 | 199 | /// Overwrite if the source is newer (off by default) 200 | pub fn overwrite_if_newer(self, overwrite_only_newer: bool) -> CopyBuilder { 201 | CopyBuilder { 202 | overwrite_if_newer: overwrite_only_newer, 203 | ..self 204 | } 205 | } 206 | 207 | /// Overwrite if size between source and dest differs (off by default) 208 | pub fn overwrite_if_size_differs(self, overwrite_if_size_differs: bool) -> CopyBuilder { 209 | CopyBuilder { 210 | overwrite_if_size_differs, 211 | ..self 212 | } 213 | } 214 | 215 | /// Do not copy files that contain this string 216 | pub fn with_exclude_filter(self, f: &str) -> CopyBuilder { 217 | let mut filters = self.exclude_filters.clone(); 218 | filters.push(f.to_owned()); 219 | CopyBuilder { 220 | exclude_filters: filters, 221 | ..self 222 | } 223 | } 224 | 225 | /// Only copy files that contain this string. 226 | pub fn with_include_filter(self, f: &str) -> CopyBuilder { 227 | let mut filters = self.include_filters.clone(); 228 | filters.push(f.to_owned()); 229 | CopyBuilder { 230 | include_filters: filters, 231 | ..self 232 | } 233 | } 234 | /// Execute the copy operation 235 | pub fn run(&self) -> Result<(), std::io::Error> { 236 | if !self.destination.is_dir() { 237 | debug!("MKDIR {:?}", &self.destination); 238 | std::fs::create_dir_all(&self.destination)?; 239 | } 240 | let abs_source = self.source.canonicalize()?; 241 | let abs_dest = self.destination.canonicalize()?; 242 | debug!( 243 | "Building copy operation: SRC {} DST {}", 244 | abs_source.display(), 245 | abs_dest.display() 246 | ); 247 | 248 | 'files: for entry in WalkDir::new(&abs_source) 249 | .into_iter() 250 | .filter_entry(|e| e.path() != abs_dest) 251 | .filter_map(|e| e.ok()) 252 | { 253 | let rel_dest = entry.path().strip_prefix(&abs_source).map_err(|e| { 254 | Error::new(ErrorKind::Other, format!("Could not strip prefix: {:?}", e)) 255 | })?; 256 | let dest_entry = abs_dest.join(rel_dest); 257 | 258 | if entry.path().symlink_metadata().is_ok() && !entry.file_type().is_dir() { 259 | // the source exists, but isn't a directory 260 | 261 | // Early out if target is present and overwrite is off 262 | if !self.overwrite_all 263 | && dest_entry.symlink_metadata().is_ok() 264 | && !self.overwrite_if_newer 265 | && !self.overwrite_if_size_differs 266 | { 267 | continue; 268 | } 269 | 270 | for f in &self.exclude_filters { 271 | debug!("EXCL {} for {:?}", f, entry); 272 | 273 | if entry.path().to_string_lossy().contains(f) { 274 | continue 'files; 275 | } 276 | } 277 | 278 | if !self.include_filters.is_empty() 279 | && !self 280 | .include_filters 281 | .iter() 282 | .any(|f| entry.path().to_string_lossy().contains(f)) 283 | { 284 | continue 'files; 285 | } 286 | 287 | // File is not present: copy it in any case 288 | let dest_exists = dest_entry.symlink_metadata().is_ok(); 289 | 290 | if !dest_exists { 291 | debug!( 292 | "Dest not present: CP {} DST {}", 293 | entry.path().display(), 294 | dest_entry.display() 295 | ); 296 | } 297 | 298 | // File newer? 299 | if dest_exists && self.overwrite_if_newer { 300 | if is_file_newer(entry.path(), &dest_entry) { 301 | debug!( 302 | "Source newer: CP {} DST {}", 303 | entry.path().display(), 304 | dest_entry.display() 305 | ); 306 | } else { 307 | continue; 308 | } 309 | } 310 | 311 | // Different size? 312 | if dest_exists && self.overwrite_if_size_differs { 313 | if is_filesize_different(entry.path(), &dest_entry) { 314 | debug!( 315 | "Source differs: CP {} DST {}", 316 | entry.path().display(), 317 | dest_entry.display() 318 | ); 319 | } else { 320 | continue; 321 | } 322 | } 323 | 324 | if entry.file_type().is_file() { 325 | // The regular copy operation 326 | debug!("CP {} DST {}", entry.path().display(), dest_entry.display()); 327 | copy(entry.path(), dest_entry)?; 328 | } else if entry.file_type().is_symlink() { 329 | debug!( 330 | "CP LNK {} DST {}", 331 | entry.path().display(), 332 | dest_entry.display() 333 | ); 334 | let target = read_link(entry.path())?; 335 | #[cfg(unix)] 336 | std::os::unix::fs::symlink(target, dest_entry)? 337 | } else { 338 | unimplemented!( 339 | "File {} has unhandled type {:?}", 340 | entry.path().display(), 341 | entry.file_type() 342 | ); 343 | } 344 | } else if entry.path().is_dir() && !dest_entry.is_dir() { 345 | debug!("MKDIR {}", entry.path().display()); 346 | std::fs::create_dir_all(dest_entry)?; 347 | } 348 | } 349 | 350 | Ok(()) 351 | } 352 | 353 | /// Execute the copy operation in parallel. The usage of this function is discouraged 354 | /// until proven to work faster. 355 | #[cfg(feature = "jwalk")] 356 | pub fn run_par(&self) -> Result<(), std::io::Error> { 357 | if !self.destination.is_dir() { 358 | debug!("MKDIR {:?}", &self.destination); 359 | std::fs::create_dir_all(&self.destination)?; 360 | } 361 | let abs_source = self.source.canonicalize()?; 362 | let abs_dest = self.destination.canonicalize()?; 363 | debug!( 364 | "Building copy operation: SRC {} DST {}", 365 | abs_source.display(), 366 | abs_dest.display() 367 | ); 368 | for entry in JWalkDir::new(&abs_source) 369 | .into_iter() 370 | .filter_map(|e| e.ok()) 371 | { 372 | let _ = copy_file(&entry.path(), self.clone()); 373 | } 374 | 375 | Ok(()) 376 | } 377 | } 378 | 379 | /// Copy a directory from `source` to `dest`, creating `dest`, with all options. 380 | pub fn copy_dir_advanced, Q: AsRef>( 381 | source: P, 382 | dest: Q, 383 | overwrite_all: bool, 384 | overwrite_if_newer: bool, 385 | overwrite_if_size_differs: bool, 386 | exclude_filters: Vec, 387 | include_filters: Vec, 388 | ) -> Result<(), std::io::Error> { 389 | CopyBuilder { 390 | source: source.as_ref().to_path_buf(), 391 | destination: dest.as_ref().to_path_buf(), 392 | overwrite_all, 393 | overwrite_if_newer, 394 | overwrite_if_size_differs, 395 | exclude_filters, 396 | include_filters, 397 | } 398 | .run() 399 | } 400 | 401 | /// Copy a directory from `source` to `dest`, creating `dest`, with minimal options. 402 | pub fn copy_dir, Q: AsRef>(source: P, dest: Q) -> Result<(), std::io::Error> { 403 | CopyBuilder { 404 | source: source.as_ref().to_path_buf(), 405 | destination: dest.as_ref().to_path_buf(), 406 | overwrite_all: false, 407 | overwrite_if_newer: false, 408 | overwrite_if_size_differs: false, 409 | exclude_filters: vec![], 410 | include_filters: vec![], 411 | } 412 | .run() 413 | } 414 | --------------------------------------------------------------------------------