dircpy_stable/
lib.rs

1use log::*;
2use std::fs::copy;
3use std::io::{Error, ErrorKind};
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6use walkdir::WalkDir;
7
8#[derive(Debug, Clone)]
9/// Recursively copy a directory from a to b.
10/// ```
11/// use dircpy::*;
12///
13/// // Most basic example:
14/// copy_dir("src", "dest");
15///
16/// // Simple builder example:
17///CopyBuilder::new("src", "dest")
18///.run()
19///.unwrap();
20///
21/// // Copy recursively, only including certain files:
22///CopyBuilder::new("src", "dest")
23///.overwrite_if_newer(true)
24///.overwrite_if_size_differs(true)
25///.with_include_filter(".txt")
26///.with_include_filter(".csv")
27///.run()
28///.unwrap();
29/// ```
30
31pub struct CopyBuilder {
32    /// The source directory
33    pub source: PathBuf,
34    /// the destination directory
35    pub destination: PathBuf,
36    overwrite_all: bool,
37    overwrite_if_newer: bool,
38    overwrite_if_size_differs: bool,
39    exclude_filters: Vec<String>,
40    include_filters: Vec<String>,
41}
42
43/// Determine if the modification date of file_a is newer than that of file_b
44fn is_file_newer(file_a: &Path, file_b: &Path) -> bool {
45    match (file_a.metadata(), file_b.metadata()) {
46        (Ok(meta_a), Ok(meta_b)) => {
47            meta_a.modified().unwrap_or_else(|_| SystemTime::now())
48                > meta_b.modified().unwrap_or(SystemTime::UNIX_EPOCH)
49        }
50        _ => false,
51    }
52}
53
54/// Determine if file_a and file_b's size differs.
55fn is_filesize_different(file_a: &Path, file_b: &Path) -> bool {
56    match (file_a.metadata(), file_b.metadata()) {
57        (Ok(meta_a), Ok(meta_b)) => meta_a.len() != meta_b.len(),
58        _ => false,
59    }
60}
61
62impl CopyBuilder {
63
64    /// Construct a new CopyBuilder with `source` and `dest`.
65    pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(source: P, dest: Q) -> CopyBuilder {
66        CopyBuilder {
67            source: source.as_ref().to_path_buf(),
68            destination: dest.as_ref().to_path_buf(),
69            overwrite_all: false,
70            overwrite_if_newer: false,
71            overwrite_if_size_differs: false,
72            exclude_filters: vec![],
73            include_filters: vec![],
74        }
75    }
76
77    /// Overwrite target files (off by default)
78    pub fn overwrite(self, overwrite: bool) -> CopyBuilder {
79        CopyBuilder {
80            overwrite_all: overwrite,
81            ..self
82        }
83    }
84
85    /// Overwrite if the source is newer (off by default)
86    pub fn overwrite_if_newer(self, overwrite_only_newer: bool) -> CopyBuilder {
87        CopyBuilder {
88            overwrite_if_newer: overwrite_only_newer,
89            ..self
90        }
91    }
92
93    /// Overwrite if size between source and dest differs (off by default)
94    pub fn overwrite_if_size_differs(self, overwrite_if_size_differs: bool) -> CopyBuilder {
95        CopyBuilder {
96            overwrite_if_size_differs,
97            ..self
98        }
99    }
100
101    /// Do not copy files that contain this string
102    pub fn with_exclude_filter(self, f: &str) -> CopyBuilder {
103        let mut filters = self.exclude_filters.clone();
104        filters.push(f.to_owned());
105        CopyBuilder {
106            exclude_filters: filters,
107            ..self
108        }
109    }
110
111    /// Only copy files that contain this string.
112    pub fn with_include_filter(self, f: &str) -> CopyBuilder {
113        let mut filters = self.exclude_filters.clone();
114        filters.push(f.to_owned());
115        CopyBuilder {
116            include_filters: filters,
117            ..self
118        }
119    }
120
121    /// Execute the copy operation
122    pub fn run(&self) -> Result<(), std::io::Error> {
123        if !self.destination.is_dir() {
124            debug!("MKDIR {:?}", &self.destination);
125            std::fs::create_dir_all(&self.destination)?;
126        }
127        let abs_source = self.source.canonicalize()?;
128        let abs_dest = self.destination.canonicalize()?;
129        debug!(
130            "Building copy operation: SRC {} DST {}",
131            abs_source.display(),
132            abs_dest.display()
133        );
134
135        for entry in WalkDir::new(&abs_source).into_iter().filter_map(|e| e.ok()) {
136            let rel_dest = entry.path().strip_prefix(&abs_source).map_err(|e| {
137                Error::new(ErrorKind::Other, format!("Could not strip prefix: {:?}", e))
138            })?;
139            let dest_entry = abs_dest.join(rel_dest);
140
141            if entry.path().is_file() {
142                // the source exists
143
144                // Early out if target is present and overwrite is off
145                if !self.overwrite_all
146                    && dest_entry.is_file()
147                    && !self.overwrite_if_newer
148                    && !self.overwrite_if_size_differs
149                {
150                    continue;
151                }
152
153                for f in &self.exclude_filters {
154                    if entry.path().to_string_lossy().contains(f) {
155                        continue;
156                    }
157                }
158
159                for f in &self.include_filters {
160                    if !entry.path().to_string_lossy().contains(f) {
161                        continue;
162                    }
163                }
164
165                // File is not present: copy it
166                if !dest_entry.is_file() {
167                    debug!(
168                        "Dest not present: CP {} DST {}",
169                        entry.path().display(),
170                        dest_entry.display()
171                    );
172                    copy(entry.path(), dest_entry)?;
173                    continue;
174                }
175
176                // File newer?
177                if self.overwrite_if_newer {
178                    if is_file_newer(entry.path(), &dest_entry) {
179                        debug!(
180                            "Source newer: CP {} DST {}",
181                            entry.path().display(),
182                            dest_entry.display()
183                        );
184                        copy(entry.path(), &dest_entry)?;
185                    }
186                    continue;
187                }
188
189                // Different size?
190                if self.overwrite_if_size_differs {
191                    if is_filesize_different(entry.path(), &dest_entry) {
192                        debug!(
193                            "Source differs: CP {} DST {}",
194                            entry.path().display(),
195                            dest_entry.display()
196                        );
197                        copy(entry.path(), &dest_entry)?;
198                    }
199                    continue;
200                }
201
202                // The regular copy operation
203                debug!("CP {} DST {}", entry.path().display(), dest_entry.display());
204                copy(entry.path(), dest_entry)?;
205            } else if entry.path().is_dir() && !dest_entry.is_dir() {
206                debug!("MKDIR {}", entry.path().display());
207                std::fs::create_dir_all(dest_entry)?;
208            }
209        }
210
211        Ok(())
212    }
213}
214
215/// Copy a directory from `source` to `dest`, creating `dest`, with all options.
216pub fn copy_dir_advanced<P: AsRef<Path>, Q: AsRef<Path>>(source: P, dest: Q, overwrite_all: bool, overwrite_if_newer: bool, overwrite_if_size_differs: bool, exclude_filters: Vec<String>, include_filters: Vec<String>) -> Result<(), std::io::Error>{
217    CopyBuilder {
218        source: source.as_ref().to_path_buf(),
219        destination: dest.as_ref().to_path_buf(),
220        overwrite_all,
221        overwrite_if_newer,
222        overwrite_if_size_differs,
223        exclude_filters,
224        include_filters,
225    }.run()
226}
227
228/// Copy a directory from `source` to `dest`, creating `dest`, with minimal options.
229pub fn copy_dir<P: AsRef<Path>, Q: AsRef<Path>>(source: P, dest: Q) -> Result<(), std::io::Error>{
230    CopyBuilder {
231        source: source.as_ref().to_path_buf(),
232        destination: dest.as_ref().to_path_buf(),
233        overwrite_all: false,
234        overwrite_if_newer: false,
235        overwrite_if_size_differs: false,
236        exclude_filters: vec![],
237        include_filters: vec![],
238    }.run()
239}