cp_r-0.5.1/.cargo_vcs_info.json0000644000000001360000000000100117620ustar { "git": { "sha1": "48556babc9fa744bd5a6777fb9520b4ffcfb8fd8" }, "path_in_vcs": "" }cp_r-0.5.1/.github/workflows/cargo-audit.yml000064400000000000000000000007010072674642500171170ustar 00000000000000name: cargo-audit on: schedule: - cron: '17 0 * * *' push: paths: - '**/Cargo.toml' - '**/Cargo.lock' - .github/workflows/cargo-audit.yml jobs: cargo-audit: runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} cp_r-0.5.1/.github/workflows/tests.yml000064400000000000000000000012210072674642500160600ustar 00000000000000name: Tests on: push: pull_request: # see https://matklad.github.io/2021/09/04/fast-rust-builds.html env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CI: 1 RUST_BACKTRACE: short RUSTFLAGS: "-W rust-2021-compatibility" RUSTUP_MAX_RETRIES: 10 jobs: build: strategy: matrix: os: [macOS-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Show Cargo and rustc version run: | cargo --version rustc --version - name: Build run: cargo build --all-targets - name: Test run: cargo test cp_r-0.5.1/.gitignore000064400000000000000000000000400072674642500125640ustar 00000000000000/target Cargo.lock mutants.out* cp_r-0.5.1/Cargo.toml0000644000000017020000000000100077600ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "cp_r" version = "0.5.1" authors = ["Martin Pool"] description = "Copy files and directories recursively, preserving mtime and permissions" keywords = [ "copy", "directory", "filesystem", "permissions", "recursive", ] categories = ["filesystem"] license = "MIT" repository = "https://github.com/sourcefrog/cp_r" [dependencies.filetime] version = "0.2" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.tempfile] version = "3.2" cp_r-0.5.1/Cargo.toml.orig000064400000000000000000000007120072674642500134710ustar 00000000000000[package] name = "cp_r" version = "0.5.1" edition = "2018" authors = ["Martin Pool"] description = "Copy files and directories recursively, preserving mtime and permissions" license = "MIT" repository = "https://github.com/sourcefrog/cp_r" categories = ["filesystem"] keywords = ["copy", "directory", "filesystem", "permissions", "recursive"] [dependencies] filetime = "0.2" [dev-dependencies] anyhow = "1.0" # to test attaching context tempfile = "3.2" cp_r-0.5.1/LICENSE000064400000000000000000000020540072674642500116100ustar 00000000000000MIT License Copyright (c) 2021 Martin Pool Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cp_r-0.5.1/NEWS.md000064400000000000000000000001030072674642500116720ustar 00000000000000See src/lib.rs rustdoc or . cp_r-0.5.1/README.md000064400000000000000000000024200072674642500120570ustar 00000000000000# Rust `cp_r` [![Docs](https://img.shields.io/docsrs/cp_r.svg)](https://docs.rs/cp_r) [![Tests](https://github.com/sourcefrog/cp_r/workflows/Tests/badge.svg?branch=main)](https://github.com/sourcefrog/cp_r/actions?query=workflow%3ATests) [![cargo-audit](https://github.com/sourcefrog/cp_r/actions/workflows/cargo-audit.yml/badge.svg)](https://github.com/sourcefrog/cp_r/actions/workflows/cargo-audit.yml) [![crates.io](https://img.shields.io/crates/v/cp_r.svg)](https://crates.io/crates/cp_r) ![Maturity: Beta](https://img.shields.io/badge/maturity-beta-yellow.svg) A small Rust library to copy a directory tree preserving mtimes and permissions, with minimal dependencies, and with clean error reporting. ## Features * Minimal dependencies: currently just `filetime` to support copying mtimes. * Returns a struct describing how much data and how many files were copied. * Tested on Linux, macOS and Windows. * Copies mtimes and permissions. * Takes an optional callback to decide which entries are copied or skipped, `CopyOptions::filter`. * Takes an optional callback to show progress or record which files are copied, `CopyOptions::after_entry_copied`. See the [docs](https://docs.rs/cp_r) for more information. Patches welcome! License: MIT. cp_r-0.5.1/src/lib.rs000064400000000000000000000404600072674642500125110ustar 00000000000000// Copyright 2021, 2022 Martin Pool //! Copy a directory tree, including mtimes and permissions. //! //! To copy a tree, first configure parameters on [CopyOptions] and then call //! [CopyOptions::copy_tree]. //! //! # Features //! //! * Minimal dependencies: currently just `filetime` to support copying mtimes. //! * Returns [CopyStats] describing how much data and how many files were //! copied. //! * Tested on Linux, macOS and Windows. //! * Copies mtimes and permissions. //! * Takes an optional callback to decide which entries are copied or skipped, //! [CopyOptions::filter]. //! * Takes an optional callback to show progress or record which files are copied, //! [CopyOptions::after_entry_copied]. //! //! # Missing features that could be added //! //! * Options to _not_ copy mtimes or permissions. //! * A callback that can decide whether to continue after an error. //! * Overwrite existing directories or files. //! * Copy single files: don't assume the source path is a directory. //! * A dry-run mode. //! //! # Example //! //! ``` //! use std::path::Path; //! use cp_r::{CopyOptions, CopyStats}; //! use tempfile; //! //! // Copy this crate's `src` directory. //! let dest = tempfile::tempdir().unwrap(); //! let stats = CopyOptions::new().copy_tree(Path::new("src"), dest.path()).unwrap(); //! assert_eq!(stats.files, 1, "only one file in src"); //! assert_eq!(stats.dirs, 0, "no children"); //! assert_eq!(stats.symlinks, 0, "no symlinks"); //! ``` //! //! # Release history //! //! ## 0.5.1 //! //! Released 2022-03-24. //! //! * Change: Ignore errors when trying to set the mtime on copied files. This can //! happen on Windows if the file is read-only. //! //! ## 0.5.0 //! //! Released 2022-02-15 //! //! ### API changes //! //! * The callback passed to [CopyOptions::after_entry_copied] now returns `Result<()>` //! (previously `()`), so it can return an Err to abort copying. //! //! ## 0.4.0 //! //! Released 2021-11-30 //! //! ### API changes //! //! * Remove `copy_buffer_size`, `file_buffers_copied`: these are too niche to have in the public //! API, and anyhow become meaningless when we use [std::fs::copy]. //! //! * New [ErrorKind::DestinationDoesNotExist]. //! //! * [Error::io_error] returns `Option` (previously just an `io::Error`): //! errors from this crate may not have a direct `io::Error` source. //! //! * [CopyOptions::copy_tree] arguments are relaxed to `AsRef` so that they will accept //! `&str`, `PathBuf`, `tempfile::TempDir`, etc. //! //! ### Improvements //! //! * Use [std::fs::copy], which is more efficient, and makes this crate simpler. //! //! ## 0.3.1 //! //! Released 2021-11-07 //! //! ### API changes //! //! * [CopyOptions::copy_tree] consumes `self` (rather than taking `&mut self`), //! which reduces lifetime issues in accessing values owned by callbacks. //! //! ### New features //! //! * [CopyOptions::after_entry_copied] callback added, which can be used for //! example to draw a progress bar. //! //! ## 0.3.0 //! //! Released 2021-11-06 //! //! ### API changes //! //! * [CopyOptions] builder functions now return `self` rather than `&mut self`. //! * The actual copy operation is run by calling [CopyOptions::copy_tree], //! rather than passing the options as a parameter to `copy_tree`. //! * Rename `with_copy_buffer_size` to `copy_buffer_size`. //! //! ### New features //! * A new option to provide a filter on which entries should be copied, //! through [CopyOptions::filter]. //! //! ## 0.2.0 //! * `copy_tree` will create the immediate destination directory by default, //! but this can be controlled by [CopyOptions::create_destination]. The //! destination, if created, is counted in [CopyStats::dirs] and inherits its //! permissions from the source. //! //! ## 0.1.1 //! * [Error] implements [std::error::Error] and [std::fmt::Display]. //! //! * [Error] is tested to be compatible with [Anyhow](https://docs.rs/anyhow). //! (There is only a dev-dependency on Anyhow; users of this library won't //! pull it in.) //! //! ## 0.1.0 //! * Initial release. #![warn(missing_docs)] use std::collections::VecDeque; use std::fmt; use std::fs::{self, DirEntry}; use std::io; use std::path::{Path, PathBuf}; /// Options for copying file trees. /// /// Default options may be OK for many callers: /// * Preserve mtime and permissions. /// * Create the destination if it does not exist. pub struct CopyOptions<'f> { // TODO: Continue or stop on error? // TODO: Option controlling whether to copy mtimes? // TODO: Copy permissions? create_destination: bool, // I agree with Clippy that the callbacks are complex types, but stable Rust // seems to have no other way to spell it, because you can't make a type or // trait alias for a Fn. #[allow(clippy::type_complexity)] filter: Option Result + 'f>>, #[allow(clippy::type_complexity)] after_entry_copied: Option Result<()> + 'f>>, } impl<'f> Default for CopyOptions<'f> { fn default() -> CopyOptions<'f> { CopyOptions { create_destination: true, filter: None, after_entry_copied: None, } } } impl<'f> CopyOptions<'f> { /// Construct reasonable default options. pub fn new() -> CopyOptions<'f> { CopyOptions::default() } /// Set whether to create the destination if it does not exist (the default), or return an error. /// /// Only the immediate destination is created, not all its parents. #[must_use] pub fn create_destination(self, create_destination: bool) -> CopyOptions<'f> { CopyOptions { create_destination, ..self } } /// Set a filter callback that can determine which files should be copied. /// /// The filter can return /// * `Ok(true)` to copy an entry (and recursively continue into directories) /// * `Ok(false)` to skip an entry (and anything inside the directory) /// * `Err(_)` to stop copying and return this error /// /// The path is relative to the top of the tree. The [std::fs::DirEntry] gives access to the file type and other metadata of the source file. /// /// ``` /// use std::fs; /// use std::path::Path; /// use cp_r::CopyOptions; /// /// let src = tempfile::tempdir().unwrap(); /// fs::write(src.path().join("transient.tmp"), b"hello?").unwrap(); /// fs::write(src.path().join("permanent.txt"), b"hello?").unwrap(); /// let dest = tempfile::tempdir().unwrap(); /// /// let stats = CopyOptions::new() /// .filter( /// |path, _| Ok(path.extension().and_then(|s| s.to_str()) != Some("tmp"))) /// .copy_tree(&src.path(), &dest.path()) /// .unwrap(); /// /// assert!(dest.path().join("permanent.txt").exists()); /// assert!(!dest.path().join("transient.tmp").exists()); /// assert_eq!(stats.filtered_out, 1); /// assert_eq!(stats.files, 1); /// ``` /// /// *Note:* Due to limitations in the current Rust compiler's type inference /// for closures, filter closures may give errors about lifetimes if they are /// assigned to to a variable rather than declared inline in the parameter. #[must_use] pub fn filter(self, filter: F) -> CopyOptions<'f> where F: FnMut(&Path, &DirEntry) -> Result + 'f, { CopyOptions { filter: Some(Box::new(filter)), ..self } } /// Set a progress callback that's called after each entry is successfully copied. /// /// The callback is passed: /// * The path, relative to the top of the tree, that was just copied. /// * The [std::fs::FileType] of the entry that was copied. /// * The [stats](CopyStats) so far, including the number of files copied. /// /// If the callback returns an error, it will abort the copy and the same /// error will be returned from [CopyOptions::copy_tree]. #[must_use] pub fn after_entry_copied(self, after_entry_copied: F) -> CopyOptions<'f> where F: FnMut(&Path, &fs::FileType, &CopyStats) -> Result<()> + 'f, { CopyOptions { after_entry_copied: Some(Box::new(after_entry_copied)), ..self } } /// Copy the tree according to the options. /// /// Returns [CopyStats] describing how many files were copied, etc. pub fn copy_tree(mut self, src: P, dest: Q) -> Result where P: AsRef, Q: AsRef, { let src = src.as_ref(); let dest = dest.as_ref(); let mut stats = CopyStats::default(); // TODO: Handle the src not being a dir: copy that single entry. if self.create_destination { if !dest.is_dir() { copy_dir(src, dest, &mut stats)?; } } else if !dest.is_dir() { return Err(Error::new(ErrorKind::DestinationDoesNotExist, dest)); } let mut subdir_queue: VecDeque = VecDeque::new(); subdir_queue.push_back(PathBuf::from("")); while let Some(subdir) = subdir_queue.pop_front() { let subdir_full_path = src.join(&subdir); for entry in fs::read_dir(&subdir_full_path) .map_err(|io| Error::from_io_error(io, ErrorKind::ReadDir, &subdir_full_path))? { let dir_entry = entry.map_err(|io| { Error::from_io_error(io, ErrorKind::ReadDir, &subdir_full_path) })?; let entry_subpath = subdir.join(dir_entry.file_name()); if let Some(filter) = &mut self.filter { if !filter(&entry_subpath, &dir_entry)? { stats.filtered_out += 1; continue; } } let src_fullpath = src.join(&entry_subpath); let dest_fullpath = dest.join(&entry_subpath); let file_type = dir_entry .file_type() .map_err(|io| Error::from_io_error(io, ErrorKind::ReadDir, &src_fullpath))?; if file_type.is_file() { copy_file(&src_fullpath, &dest_fullpath, &mut stats)? } else if file_type.is_dir() { copy_dir(&src_fullpath, &dest_fullpath, &mut stats)?; subdir_queue.push_back(entry_subpath.clone()); } else if file_type.is_symlink() { copy_symlink(&src_fullpath, &dest_fullpath, &mut stats)? } else { // TODO: Include the file type. return Err(Error::new(ErrorKind::UnsupportedFileType, src_fullpath)); } if let Some(ref mut f) = self.after_entry_copied { f(&entry_subpath, &file_type, &stats)?; } } } Ok(stats) } } /// Counters of how many things were copied. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct CopyStats { /// The number of plain files copied. pub files: usize, /// The number of directories copied. pub dirs: usize, /// The number of symlinks copied. pub symlinks: usize, /// The number of bytes of file content copied, across all files. pub file_bytes: u64, /// The number of entries filtered out by the [CopyOptions::filter] callback. pub filtered_out: usize, } /// An error from copying a tree. /// /// At present this library does not support continuing after an error, so only the first error is /// returned by [CopyOptions::copy_tree]. #[derive(Debug)] pub struct Error { path: PathBuf, /// The original IO error, if any. io: Option, kind: ErrorKind, } /// A [std::result::Result] possibly containing a `cp_r` [Error]. pub type Result = std::result::Result; impl Error { /// Construct a new error with no source. pub fn new

(kind: ErrorKind, path: P) -> Error where P: Into, { Error { path: path.into(), kind, io: None, } } /// Construct a new error from a [std::io::Error]. pub fn from_io_error

(io: io::Error, kind: ErrorKind, path: P) -> Error where P: Into, { Error { path: path.into(), kind, io: Some(io), } } /// The path where this error occurred. pub fn path(&self) -> &Path { // TODO: Be consistent about whether this is relative to the root etc. &self.path } /// The IO error that caused this error, if any. pub fn io_error(&self) -> Option<&io::Error> { self.io.as_ref() } /// The kind of error that occurred. pub fn kind(&self) -> ErrorKind { self.kind } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { // It seems like you should be able to spell this like `self.io.as_ref().into()` but that // doesn't work and I'm not sure why... if let Some(io) = &self.io { Some(io) } else { None } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ErrorKind::*; let kind_msg = match self.kind { ReadDir => "reading source directory", ReadFile => "reading source file", WriteFile => "writing file", CreateDir => "creating directory", ReadSymlink => "reading symlink", CreateSymlink => "creating symlink", UnsupportedFileType => "unsupported file type", CopyFile => "copying file", DestinationDoesNotExist => "destination directory does not exist", Interrupted => "interrupted", }; if let Some(io) = &self.io { write!(f, "{}: {}: {}", kind_msg, self.path.display(), io) } else { write!(f, "{}: {}", kind_msg, self.path.display()) } } } /// Various kinds of errors that can occur while copying a tree. #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum ErrorKind { /// Error listing a source directory. ReadDir, /// Error opening or reading a source file. ReadFile, /// Error creating or writing a destination file. WriteFile, /// Error in copying a file: might be a read or write error. CopyFile, /// Error creating a destination directory. CreateDir, /// Error reading a symlink. ReadSymlink, /// Error creating a symlink in the destination. CreateSymlink, /// The source tree contains a type of file that this library can't copy, such as a Unix /// FIFO. UnsupportedFileType, /// The destination directory does not exist. DestinationDoesNotExist, /// The copy was interrupted by the user. /// /// This is not currently generated internally by `cp_r` but can be returned /// by a callback. Interrupted, } fn copy_file(src: &Path, dest: &Path, stats: &mut CopyStats) -> Result<()> { // TODO: Optionally first check and error if the destination exists. let bytes_copied = fs::copy(src, dest).map_err(|io| Error::from_io_error(io, ErrorKind::CopyFile, src))?; stats.file_bytes += bytes_copied; let src_metadata = src .metadata() .map_err(|io| Error::from_io_error(io, ErrorKind::ReadFile, src))?; let src_mtime = filetime::FileTime::from_last_modification_time(&src_metadata); // It's OK if we can't set the mtime. let _ = filetime::set_file_mtime(&dest, src_mtime); // Permissions should have already been set by fs::copy. stats.files += 1; Ok(()) } fn copy_dir(_src: &Path, dest: &Path, stats: &mut CopyStats) -> Result<()> { fs::create_dir(dest) .map_err(|io| Error::from_io_error(io, ErrorKind::CreateDir, dest)) .map(|()| stats.dirs += 1) } #[cfg(unix)] fn copy_symlink(src: &Path, dest: &Path, stats: &mut CopyStats) -> Result<()> { let target = fs::read_link(src).map_err(|io| Error::from_io_error(io, ErrorKind::ReadSymlink, src))?; std::os::unix::fs::symlink(target, dest) .map_err(|io| Error::from_io_error(io, ErrorKind::CreateSymlink, dest))?; stats.symlinks += 1; Ok(()) } #[cfg(windows)] fn copy_symlink(_src: &Path, _dest: &Path, _stats: &mut CopyStats) -> Result<()> { unimplemented!("symlinks are not yet supported on Windows"); } cp_r-0.5.1/tests/anyhow.rs000064400000000000000000000016670072674642500136310ustar 00000000000000// Copyright 2021 Martin Pool //! Test compatibility with Anyhow. use std::path::Path; use anyhow::Context; use cp_r::CopyOptions; #[test] fn attach_anyhow_context_to_success() { // This is mostly an assertion that the error type is compatible with that expected by Anyhow. let dest = tempfile::tempdir().unwrap(); let stats = CopyOptions::new() .copy_tree(&Path::new("src"), &dest.path()) .context("copy src dir for test") .unwrap(); dbg!(&stats); } #[test] fn attach_anyhow_context_to_failure() { // This is mostly an assertion that the error type is compatible with that expected by Anyhow. let dest = tempfile::tempdir().unwrap(); let err = CopyOptions::new() .create_destination(false) .copy_tree(&Path::new("src"), &dest.path().join("nonexistent")) .context("copy src dir for test") .unwrap_err(); dbg!(&err); println!("Display error: {}", err); } cp_r-0.5.1/tests/tests.rs000064400000000000000000000242540072674642500134630ustar 00000000000000// Copyright 2021, 2022 Martin Pool //! Public API tests for `cp_r`. use std::fs; use std::io; use std::path::{Path, PathBuf}; use tempfile::TempDir; use cp_r::*; #[test] fn basic_copy() { let src = tempfile::tempdir().unwrap(); let dest = tempfile::tempdir().unwrap(); let file_content = b"hello world\n"; let file_name = "a file"; let src_file_path = src.path().join(file_name); fs::write(&src_file_path, file_content).unwrap(); let stats = CopyOptions::default() .copy_tree(src.path(), dest.path()) .unwrap(); let dest_file_path = &dest.path().join(file_name); assert_eq!(fs::read(&dest_file_path).unwrap(), file_content); assert_eq!(stats.files, 1); assert_eq!(stats.file_bytes, file_content.len() as u64); assert_eq!( fs::metadata(&dest_file_path).unwrap().modified().unwrap(), fs::metadata(&src_file_path).unwrap().modified().unwrap() ); } #[test] fn subdirs() { let src = TempDir::new().unwrap(); let dest = TempDir::new().unwrap(); fs::create_dir(&src.path().join("a")).unwrap(); fs::create_dir(&src.path().join("b")).unwrap(); fs::create_dir(&src.path().join("b/bb")).unwrap(); fs::create_dir(&src.path().join("a").join("aa")).unwrap(); let file_content = b"some file content\n"; fs::write(&src.path().join("a/aa/aaafile"), &file_content).unwrap(); // Note here that we can just path a reference to the TempDirs without calling // `.path()`, because they `AsRef` to a `Path`. let stats = CopyOptions::default().copy_tree(&src, &dest).unwrap(); assert_eq!( fs::read(&dest.path().join("a/aa/aaafile")).unwrap(), file_content ); assert!(fs::metadata(&dest.path().join("b/bb")) .unwrap() .file_type() .is_dir()); assert_eq!(stats.files, 1); assert_eq!(stats.file_bytes, file_content.len() as u64); assert_eq!(stats.dirs, 4); } #[test] fn clean_error_on_nonexistent_source() { let dest = tempfile::tempdir().unwrap(); let err = CopyOptions::new().copy_tree("nothing", &dest).unwrap_err(); println!("err = {:#?}", err); assert!(err.path().starts_with("nothing")); assert_eq!(err.kind(), ErrorKind::ReadDir); assert_eq!(err.io_error().unwrap().kind(), io::ErrorKind::NotFound); } #[test] fn create_destination_by_default() { let empty_src = tempfile::tempdir().unwrap(); let dest_parent = tempfile::tempdir().unwrap(); let dest = dest_parent.path().join("nonexistent_child"); let stats = CopyOptions::new() .copy_tree(empty_src.path(), &dest) .unwrap(); assert!(dest.is_dir()); assert_eq!(stats.dirs, 1); assert_eq!(stats.files, 0); } #[test] fn create_destination_when_requested() { let empty_src = tempfile::tempdir().unwrap(); let dest_parent = tempfile::tempdir().unwrap(); let dest = dest_parent.path().join("nonexistent_child"); let stats = CopyOptions::new() .create_destination(true) .copy_tree(&empty_src, &dest) .unwrap(); assert!(dest.is_dir()); assert_eq!(stats.dirs, 1); assert_eq!(stats.files, 0); } #[test] fn optionally_destination_must_exist() { // TODO: At least add an option to create the destination if it does not exist. // But, for now, it must. let dest_parent = tempfile::tempdir().unwrap(); let dest = dest_parent.path().join("nonexistent_child"); let err = CopyOptions::new() .create_destination(false) .copy_tree("src", &dest) .unwrap_err(); println!("err = {:#?}", err); assert_eq!(err.kind(), ErrorKind::DestinationDoesNotExist); assert!( err.path().starts_with(&dest), "path in the error relates to the destination" ); assert!(err.io_error().is_none(), "no underlying io::Error"); } #[cfg(unix)] #[test] fn clean_error_failing_to_copy_devices() { let dest = tempfile::tempdir().unwrap(); let err = CopyOptions::new() .copy_tree("/dev", &dest.path()) .unwrap_err(); println!("{:#?}", err); let kind = err.kind(); assert!( kind == ErrorKind::UnsupportedFileType || kind == ErrorKind::CopyFile, "unexpected ErrorKind {:?}", kind ); // Depending on OS peculiarities, we might detect this at different points, and therefore // return different error kinds, and there may or may not be an ioerror. assert!(err.path().strip_prefix("/dev/").is_ok()); let formatted = format!("{}", err); assert!( formatted.starts_with("unsupported file type: /dev/") || formatted.contains( "the source path is neither a regular file nor a symlink to a regular file" ), "unexpected string format: {:?}", formatted ); } #[cfg(unix)] #[test] fn copy_dangling_symlink() { let src = tempfile::tempdir().unwrap(); let dest = tempfile::tempdir().unwrap(); std::os::unix::fs::symlink("dangling target", src.path().join("a_link")).unwrap(); let stats = CopyOptions::new() .copy_tree(src.path(), dest.path()) .unwrap(); println!("{:#?}", stats); assert_eq!( stats, CopyStats { files: 0, dirs: 0, symlinks: 1, file_bytes: 0, filtered_out: 0, } ); } #[test] fn filter_by_path() { let src = tempfile::tempdir().unwrap(); let dest = tempfile::tempdir().unwrap(); fs::create_dir(&src.path().join("a")).unwrap(); fs::create_dir(&src.path().join("b")).unwrap(); fs::create_dir(&src.path().join("b/bb")).unwrap(); fs::create_dir(&src.path().join("a").join("aa")).unwrap(); let file_content = b"some file content\n"; fs::write(&src.path().join("a/aa/aaafile"), &file_content).unwrap(); fn not_b(path: &Path, _: &fs::DirEntry) -> cp_r::Result { Ok(path != Path::new("b")) } let stats = CopyOptions::new() .filter(not_b) .copy_tree(src.path(), dest.path()) .unwrap(); assert_eq!( fs::read(&dest.path().join("a/aa/aaafile")).unwrap(), file_content ); assert!(!dest.path().join("b").exists()); assert_eq!( stats, CopyStats { files: 1, file_bytes: file_content.len() as u64, dirs: 2, symlinks: 0, filtered_out: 1, } ); } const AAA_CONTENT: &[u8] = b"some file content\n"; fn setup_a_b_src() -> tempfile::TempDir { let src = tempfile::tempdir().unwrap(); fs::create_dir(&src.path().join("a")).unwrap(); fs::create_dir(&src.path().join("b")).unwrap(); fs::create_dir(&src.path().join("b/bb")).unwrap(); fs::create_dir(&src.path().join("a").join("aa")).unwrap(); let file_content = AAA_CONTENT; fs::write(&src.path().join("a/aa/aaafile"), &file_content).unwrap(); src } #[test] fn filter_by_mut_closure() { let src = setup_a_b_src(); let dest = tempfile::tempdir().unwrap(); // Filter paths and also collect all the paths we've seen, as an example of a filter // that's more than a simple function pointer. let mut filter_seen_paths: Vec = Vec::new(); let stats = CopyOptions::default() .filter(|path: &Path, _de| { filter_seen_paths.push(path.to_str().unwrap().replace('\\', "/")); Ok(path != Path::new("b")) }) .copy_tree(src.path(), dest.path()) .unwrap(); assert_eq!( fs::read(&dest.path().join("a/aa/aaafile")).unwrap(), AAA_CONTENT, ); assert!(!dest.path().join("b").exists()); assert_eq!( stats, CopyStats { files: 1, file_bytes: AAA_CONTENT.len() as u64, dirs: 2, symlinks: 0, filtered_out: 1, } ); // The order in which entries are seen is not guaranteed, and in practice // will be partly determined by the unpredictable order that the filesystem // returns directory entries. // // "b" is seen (because the filter records it before filtering it out), but // b's children are not visited. filter_seen_paths.sort_unstable(); assert_eq!(filter_seen_paths, ["a", "a/aa", "a/aa/aaafile", "b"]); } #[test] fn after_entry_copied_callback() { let src = setup_a_b_src(); let dest = tempfile::tempdir().unwrap(); let mut progress_seen: Vec<(PathBuf, fs::FileType)> = Vec::new(); let mut last_stats = CopyStats::default(); // We can't count on the entries being seen in any particular order, but there are other // properties we can check... let final_stats = CopyOptions::new() .after_entry_copied(|p, ft, stats| { assert!( !progress_seen.iter().any(|(pp, _)| pp == p), "filename has not been seen before" ); progress_seen.push((p.to_owned(), *ft)); if ft.is_file() { assert_eq!(stats.files, last_stats.files + 1); } else if ft.is_dir() { assert_eq!(stats.dirs, last_stats.dirs + 1); } else { panic!("unexpected file type {:?}", ft); } last_stats = stats.clone(); Ok(()) }) .copy_tree(src.path(), dest.path()) .unwrap(); assert_eq!( last_stats, final_stats, "progress after the final copy include stats equal to the overall final stats" ); } #[test] fn after_entry_callback_error_terminates_copy() { let src = setup_a_b_src(); let dest = tempfile::tempdir().unwrap(); // Stop after copying one file. The order in which files are copied is not defined, but we should see // exactly one in the result. let options = CopyOptions::new().after_entry_copied(|p, ft, _stats| { if ft.is_file() { Err(Error::new(ErrorKind::Interrupted, p)) } else { Ok(()) } }); let result = options.copy_tree(src.path(), dest.path()); let err = result.unwrap_err(); let err_str = err.to_string(); assert!( err_str.starts_with("interrupted"), "unexpected err_str: {:?}", err_str ); let err_debug = format!("{:?}", err); assert!( err_debug.starts_with("Error") && err_debug.contains("kind: Interrupted"), "unexpected err_debug: {:?}", err_debug ); }