copy_dir/
lib.rs

1//! The essential objective of this crate is to provide an API for copying
2//! directories and their contents in a straightforward and predictable way.
3//! See the documentation of the `copy_dir` function for more info.
4
5extern crate walkdir;
6
7use std::fs;
8use std::io::{Error, ErrorKind, Result};
9use std::path::Path;
10
11macro_rules! push_error {
12    ($expr:expr, $vec:ident) => {
13        match $expr {
14            Err(e) => $vec.push(e),
15            Ok(_) => (),
16        }
17    };
18}
19
20macro_rules! make_err {
21    ($text:expr, $kind:expr) => {
22        Error::new($kind, $text)
23    };
24
25    ($text:expr) => {
26        make_err!($text, ErrorKind::Other)
27    };
28}
29
30/// Copy a directory and its contents
31///
32/// Unlike e.g. the `cp -r` command, the behavior of this function is simple
33/// and easy to understand. The file or directory at the source path is copied
34/// to the destination path. If the source path points to a directory, it will
35/// be copied recursively with its contents.
36///
37/// # Errors
38///
39/// * It's possible for many errors to occur during the recursive copy
40///   operation. These errors are all returned in a `Vec`. They may or may
41///   not be helpful or useful.
42/// * If the source path does not exist.
43/// * If the destination path exists.
44/// * If something goes wrong with copying a regular file, as with
45///   `std::fs::copy()`.
46/// * If something goes wrong creating the new root directory when copying
47///   a directory, as with `std::fs::create_dir`.
48/// * If you try to copy a directory to a path prefixed by itself i.e.
49///   `copy_dir(".", "./foo")`. See below for more details.
50///
51/// # Caveats/Limitations
52///
53/// I would like to add some flexibility around how "edge cases" in the copying
54/// operation are handled, but for now there is no flexibility and the following
55/// caveats and limitations apply (not by any means an exhaustive list):
56///
57/// * You cannot currently copy a directory into itself i.e.
58///   `copy_dir(".", "./foo")`. This is because we are recursively walking
59///   the directory to be copied *while* we're copying it, so in this edge
60///   case you get an infinite recursion. Fixing this is the top of my list
61///   of things to do with this crate.
62/// * Hard links are not accounted for, i.e. if more than one hard link
63///   pointing to the same inode are to be copied, the data will be copied
64///   twice.
65/// * Filesystem boundaries may be crossed.
66/// * Symbolic links will be copied, not followed.
67pub fn copy_dir<Q: AsRef<Path>, P: AsRef<Path>>(from: P, to: Q) -> Result<Vec<Error>> {
68    if !from.as_ref().exists() {
69        return Err(make_err!("source path does not exist", ErrorKind::NotFound));
70    } else if to.as_ref().exists() {
71        return Err(make_err!("target path exists", ErrorKind::AlreadyExists));
72    }
73
74    let mut errors = Vec::new();
75
76    // copying a regular file is EZ
77    if from.as_ref().is_file() {
78        return fs::copy(&from, &to).map(|_| Vec::new());
79    }
80
81    fs::create_dir(&to)?;
82
83    // The approach taken by this code (i.e. walkdir) will not gracefully
84    // handle copying a directory into itself, so we're going to simply
85    // disallow it by checking the paths. This is a thornier problem than I
86    // wish it was, and I'd like to find a better solution, but for now I
87    // would prefer to return an error rather than having the copy blow up
88    // in users' faces. Ultimately I think a solution to this will involve
89    // not using walkdir at all, and might come along with better handling
90    // of hard links.
91    let target_is_under_source = from
92        .as_ref()
93        .canonicalize()
94        .and_then(|fc| to.as_ref().canonicalize().map(|tc| (fc, tc)))
95        .map(|(fc, tc)| tc.starts_with(fc))?;
96
97    if target_is_under_source {
98        fs::remove_dir(&to)?;
99
100        return Err(make_err!(
101            "cannot copy to a path prefixed by the source path"
102        ));
103    }
104
105    for entry in walkdir::WalkDir::new(&from)
106        .min_depth(1)
107        .into_iter()
108        .filter_map(|e| e.ok())
109    {
110        let relative_path = match entry.path().strip_prefix(&from) {
111            Ok(rp) => rp,
112            Err(_) => panic!("strip_prefix failed; this is a probably a bug in copy_dir"),
113        };
114
115        let target_path = {
116            let mut target_path = to.as_ref().to_path_buf();
117            target_path.push(relative_path);
118            target_path
119        };
120
121        let source_metadata = match entry.metadata() {
122            Err(_) => {
123                errors.push(make_err!(format!(
124                    "walkdir metadata error for {:?}",
125                    entry.path()
126                )));
127
128                continue;
129            }
130
131            Ok(md) => md,
132        };
133
134        if source_metadata.is_dir() {
135            push_error!(fs::create_dir(&target_path), errors);
136            push_error!(
137                fs::set_permissions(&target_path, source_metadata.permissions()),
138                errors
139            );
140        } else {
141            push_error!(fs::copy(entry.path(), &target_path), errors);
142        }
143    }
144
145    Ok(errors)
146}
147
148#[cfg(test)]
149mod tests {
150    #![allow(unused_variables)]
151
152    extern crate std;
153    use std::fs;
154    use std::path::Path;
155    use std::process::Command;
156
157    extern crate tempdir;
158    extern crate walkdir;
159
160    #[test]
161    fn single_file() {
162        let file = File("foo.file");
163        assert_we_match_the_real_thing(&file, true, None);
164    }
165
166    #[test]
167    fn directory_with_file() {
168        let dir = Dir(
169            "foo",
170            vec![File("bar"), Dir("baz", vec![File("quux"), File("fobe")])],
171        );
172        assert_we_match_the_real_thing(&dir, true, None);
173    }
174
175    #[test]
176    fn source_does_not_exist() {
177        let base_dir = tempdir::TempDir::new("copy_dir_test").unwrap();
178        let source_path = base_dir.as_ref().join("noexist.file");
179        match super::copy_dir(&source_path, "dest.file") {
180            Ok(_) => panic!("expected Err"),
181            Err(err) => match err.kind() {
182                std::io::ErrorKind::NotFound => (),
183                _ => panic!("expected kind NotFound"),
184            },
185        }
186    }
187
188    #[test]
189    fn target_exists() {
190        let base_dir = tempdir::TempDir::new("copy_dir_test").unwrap();
191        let source_path = base_dir.as_ref().join("exist.file");
192        let target_path = base_dir.as_ref().join("exist2.file");
193
194        {
195            fs::File::create(&source_path).unwrap();
196            fs::File::create(&target_path).unwrap();
197        }
198
199        match super::copy_dir(&source_path, &target_path) {
200            Ok(_) => panic!("expected Err"),
201            Err(err) => match err.kind() {
202                std::io::ErrorKind::AlreadyExists => (),
203                _ => panic!("expected kind AlreadyExists"),
204            },
205        }
206    }
207
208    #[test]
209    fn attempt_copy_under_self() {
210        let base_dir = tempdir::TempDir::new("copy_dir_test").unwrap();
211        let dir = Dir(
212            "foo",
213            vec![File("bar"), Dir("baz", vec![File("quux"), File("fobe")])],
214        );
215        dir.create(&base_dir).unwrap();
216
217        let from = base_dir.as_ref().join("foo");
218        let to = from.as_path().join("beez");
219
220        let copy_result = super::copy_dir(&from, &to);
221        assert!(copy_result.is_err());
222
223        let copy_err = copy_result.unwrap_err();
224        assert_eq!(copy_err.kind(), std::io::ErrorKind::Other);
225    }
226
227    // utility stuff below here
228
229    enum DirMaker<'a> {
230        Dir(&'a str, Vec<DirMaker<'a>>),
231        File(&'a str),
232    }
233
234    use self::DirMaker::*;
235
236    impl<'a> DirMaker<'a> {
237        fn create<P: AsRef<Path>>(&self, base: P) -> std::io::Result<()> {
238            match *self {
239                Dir(ref name, ref contents) => {
240                    let path = base.as_ref().join(name);
241                    fs::create_dir(&path)?;
242
243                    for thing in contents {
244                        thing.create(&path)?;
245                    }
246                }
247
248                File(ref name) => {
249                    let path = base.as_ref().join(name);
250                    fs::File::create(path)?;
251                }
252            }
253
254            Ok(())
255        }
256
257        fn name(&self) -> &str {
258            match *self {
259                Dir(name, _) => name,
260                File(name) => name,
261            }
262        }
263    }
264
265    fn assert_dirs_same<P: AsRef<Path>>(a: P, b: P) {
266        let mut wa = walkdir::WalkDir::new(a.as_ref()).into_iter();
267        let mut wb = walkdir::WalkDir::new(b.as_ref()).into_iter();
268
269        loop {
270            let o_na = wa.next();
271            let o_nb = wb.next();
272
273            if o_na.is_some() && o_nb.is_some() {
274                let r_na = o_na.unwrap();
275                let r_nb = o_nb.unwrap();
276
277                if r_na.is_ok() && r_nb.is_ok() {
278                    let na = r_na.unwrap();
279                    let nb = r_nb.unwrap();
280
281                    assert_eq!(
282                        na.path().strip_prefix(a.as_ref()),
283                        nb.path().strip_prefix(b.as_ref())
284                    );
285
286                    assert_eq!(na.file_type(), nb.file_type());
287
288                    // TODO test permissions
289                }
290            } else if o_na.is_none() && o_nb.is_none() {
291                return;
292            } else {
293                assert!(false);
294            }
295        }
296    }
297
298    fn assert_we_match_the_real_thing(
299        dir: &DirMaker,
300        explicit_name: bool,
301        o_pre_state: Option<&DirMaker>,
302    ) {
303        let base_dir = tempdir::TempDir::new("copy_dir_test").unwrap();
304
305        let source_dir = base_dir.as_ref().join("source");
306        let our_dir = base_dir.as_ref().join("ours");
307        let their_dir = base_dir.as_ref().join("theirs");
308
309        fs::create_dir(&source_dir).unwrap();
310        fs::create_dir(&our_dir).unwrap();
311        fs::create_dir(&their_dir).unwrap();
312
313        dir.create(&source_dir).unwrap();
314        let source_path = source_dir.as_path().join(dir.name());
315
316        let (our_target, their_target) = if explicit_name {
317            (
318                our_dir.as_path().join(dir.name()),
319                their_dir.as_path().join(dir.name()),
320            )
321        } else {
322            (our_dir.clone(), their_dir.clone())
323        };
324
325        if let Some(pre_state) = o_pre_state {
326            pre_state.create(&our_dir).unwrap();
327            pre_state.create(&their_dir).unwrap();
328        }
329
330        let we_good = super::copy_dir(&source_path, &our_target).is_ok();
331
332        let their_status = Command::new("cp")
333            .arg("-r")
334            .arg(source_path.as_os_str())
335            .arg(their_target.as_os_str())
336            .status()
337            .unwrap();
338
339        // TODO any way to ask cp whether it worked or not?
340        // portability?
341        // assert_eq!(we_good, their_status.success());
342        assert_dirs_same(&their_dir, &our_dir);
343    }
344
345    #[test]
346    fn dir_maker_and_assert_dirs_same_baseline() {
347        let dir = Dir("foobar", vec![File("bar"), Dir("baz", Vec::new())]);
348
349        let base_dir = tempdir::TempDir::new("copy_dir_test").unwrap();
350
351        let a_path = base_dir.as_ref().join("a");
352        let b_path = base_dir.as_ref().join("b");
353
354        fs::create_dir(&a_path).unwrap();
355        fs::create_dir(&b_path).unwrap();
356
357        dir.create(&a_path).unwrap();
358        dir.create(&b_path).unwrap();
359
360        assert_dirs_same(&a_path, &b_path);
361    }
362
363    #[test]
364    #[should_panic]
365    fn assert_dirs_same_properly_fails() {
366        let dir = Dir("foobar", vec![File("bar"), Dir("baz", Vec::new())]);
367
368        let dir2 = Dir("foobar", vec![File("fobe"), File("beez")]);
369
370        let base_dir = tempdir::TempDir::new("copy_dir_test").unwrap();
371
372        let a_path = base_dir.as_ref().join("a");
373        let b_path = base_dir.as_ref().join("b");
374
375        fs::create_dir(&a_path).unwrap();
376        fs::create_dir(&b_path).unwrap();
377
378        dir.create(&a_path).unwrap();
379        dir2.create(&b_path).unwrap();
380
381        assert_dirs_same(&a_path, &b_path);
382    }
383}