#![warn(missing_docs)]
use std::collections::VecDeque;
use std::fmt;
use std::fs::{self, DirEntry};
use std::io;
use std::path::{Path, PathBuf};
pub struct CopyOptions<'f> {
create_destination: bool,
#[allow(clippy::type_complexity)]
filter: Option<Box<dyn FnMut(&Path, &DirEntry) -> Result<bool> + 'f>>,
#[allow(clippy::type_complexity)]
after_entry_copied: Option<Box<dyn FnMut(&Path, &fs::FileType, &CopyStats) -> 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> {
pub fn new() -> CopyOptions<'f> {
CopyOptions::default()
}
#[must_use]
pub fn create_destination(self, create_destination: bool) -> CopyOptions<'f> {
CopyOptions {
create_destination,
..self
}
}
#[must_use]
pub fn filter<F>(self, filter: F) -> CopyOptions<'f>
where
F: FnMut(&Path, &DirEntry) -> Result<bool> + 'f,
{
CopyOptions {
filter: Some(Box::new(filter)),
..self
}
}
#[must_use]
pub fn after_entry_copied<F>(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
}
}
pub fn copy_tree<P, Q>(mut self, src: P, dest: Q) -> Result<CopyStats>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let src = src.as_ref();
let dest = dest.as_ref();
let mut stats = CopyStats::default();
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<PathBuf> = 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 {
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)
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct CopyStats {
pub files: usize,
pub dirs: usize,
pub symlinks: usize,
pub file_bytes: u64,
pub filtered_out: usize,
}
#[derive(Debug)]
pub struct Error {
path: PathBuf,
io: Option<io::Error>,
kind: ErrorKind,
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn new<P>(kind: ErrorKind, path: P) -> Error
where
P: Into<PathBuf>,
{
Error {
path: path.into(),
kind,
io: None,
}
}
pub fn from_io_error<P>(io: io::Error, kind: ErrorKind, path: P) -> Error
where
P: Into<PathBuf>,
{
Error {
path: path.into(),
kind,
io: Some(io),
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn io_error(&self) -> Option<&io::Error> {
self.io.as_ref()
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
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())
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum ErrorKind {
ReadDir,
ReadFile,
WriteFile,
CopyFile,
CreateDir,
ReadSymlink,
CreateSymlink,
UnsupportedFileType,
DestinationDoesNotExist,
Interrupted,
}
fn copy_file(src: &Path, dest: &Path, stats: &mut CopyStats) -> Result<()> {
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);
let _ = filetime::set_file_mtime(&dest, src_mtime);
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");
}