use std::path::{Path, PathBuf};
use_enabled_fs_module!();
use super::{
common::DestinationDirectoryRule,
prepared::{try_exists_without_follow, DirectoryCopyPrepared, QueuedOperation},
};
use crate::{
error::{CopyDirectoryError, CopyDirectoryExecutionError},
file::{
copy_file,
copy_file_with_progress,
CollidingFileBehaviour,
FileCopyOptions,
FileCopyWithProgressOptions,
FileProgress,
},
DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
DEFAULT_READ_BUFFER_SIZE,
DEFAULT_WRITE_BUFFER_SIZE,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DirectoryCopyDepthLimit {
Unlimited,
Limited {
maximum_depth: usize,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SymlinkBehaviour {
Keep,
Follow,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BrokenSymlinkBehaviour {
Keep,
Abort,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DirectoryCopyOptions {
pub destination_directory_rule: DestinationDirectoryRule,
pub copy_depth_limit: DirectoryCopyDepthLimit,
pub symlink_behaviour: SymlinkBehaviour,
pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
}
impl Default for DirectoryCopyOptions {
fn default() -> Self {
Self {
destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
symlink_behaviour: SymlinkBehaviour::Keep,
broken_symlink_behaviour: BrokenSymlinkBehaviour::Keep,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DirectoryCopyFinished {
pub total_bytes_copied: u64,
pub files_copied: usize,
pub symlinks_created: usize,
pub directories_created: usize,
}
pub(crate) fn copy_directory_unchecked(
prepared_directory_copy: DirectoryCopyPrepared,
options: DirectoryCopyOptions,
) -> Result<DirectoryCopyFinished, CopyDirectoryExecutionError> {
let can_overwrite_files = options
.destination_directory_rule
.allows_overwriting_existing_destination_files();
let can_ignore_existing_sub_directories = options
.destination_directory_rule
.allows_existing_destination_subdirectories();
let mut total_bytes_copied = 0;
let mut num_files_copied = 0;
let mut num_symlinks_recreated = 0;
let mut num_directories_created = 0;
for operation in prepared_directory_copy.operation_queue {
match operation {
QueuedOperation::CopyFile {
source_file_path,
source_size_bytes,
destination_file_path,
} => {
let destination_file_exists = try_exists_without_follow(&destination_file_path)
.map_err(|error| CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_file_path.clone(),
error,
})?;
if destination_file_exists {
let destination_file_metadata = fs::symlink_metadata(&destination_file_path)
.map_err(|error| {
CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_file_path.clone(),
error,
}
})?;
if !destination_file_metadata.is_file() {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_file_path.clone(),
});
}
if !can_overwrite_files {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_file_path.clone(),
});
}
}
copy_file(
source_file_path,
&destination_file_path,
FileCopyOptions {
colliding_file_behaviour: match can_overwrite_files {
true => CollidingFileBehaviour::Overwrite,
false => CollidingFileBehaviour::Abort,
},
},
)
.map_err(|file_error| {
CopyDirectoryExecutionError::FileCopyError {
file_path: destination_file_path,
error: file_error,
}
})?;
num_files_copied += 1;
total_bytes_copied += source_size_bytes;
}
QueuedOperation::CreateDirectory {
source_size_bytes,
destination_directory_path,
create_parent_directories,
} => {
let destination_directory_exists =
try_exists_without_follow(&destination_directory_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_directory_path.clone(),
error,
}
})?;
if destination_directory_exists {
if !destination_directory_path.is_dir() {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_directory_path.clone(),
});
}
if !can_ignore_existing_sub_directories {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_directory_path.clone(),
});
}
continue;
}
if create_parent_directories {
fs::create_dir_all(&destination_directory_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToCreateDirectory {
directory_path: destination_directory_path,
error,
}
})?;
} else {
fs::create_dir(&destination_directory_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToCreateDirectory {
directory_path: destination_directory_path,
error,
}
})?;
}
num_directories_created += 1;
total_bytes_copied += source_size_bytes;
}
#[cfg(windows)]
QueuedOperation::CreateSymlink {
symlink_path,
symlink_destination_type: symlink_type,
source_symlink_size_bytes,
symlink_destination_path,
} => {
use crate::directory::prepared::SymlinkType;
match symlink_type {
SymlinkType::File => {
std::os::windows::fs::symlink_file(
&symlink_destination_path,
&symlink_path,
)
.map_err(|error| {
CopyDirectoryExecutionError::SymlinkCreationError {
symlink_path: symlink_path.clone(),
error,
}
})?;
}
SymlinkType::Directory => {
std::os::windows::fs::symlink_dir(&symlink_destination_path, &symlink_path)
.map_err(|error| CopyDirectoryExecutionError::SymlinkCreationError {
symlink_path: symlink_path.clone(),
error,
})?;
}
}
num_symlinks_recreated += 1;
total_bytes_copied += source_symlink_size_bytes;
}
#[cfg(unix)]
QueuedOperation::CreateSymlink {
symlink_path,
source_symlink_size_bytes,
symlink_destination_path,
} => {
std::os::unix::fs::symlink(&symlink_destination_path, &symlink_path).map_err(
|error| CopyDirectoryExecutionError::SymlinkCreationError {
symlink_path: symlink_path.clone(),
error,
},
)?;
num_symlinks_recreated += 1;
total_bytes_copied += source_symlink_size_bytes;
}
};
}
Ok(DirectoryCopyFinished {
total_bytes_copied,
files_copied: num_files_copied,
symlinks_created: num_symlinks_recreated,
directories_created: num_directories_created,
})
}
pub fn copy_directory<S, T>(
source_directory_path: S,
destination_directory_path: T,
options: DirectoryCopyOptions,
) -> Result<DirectoryCopyFinished, CopyDirectoryError>
where
S: AsRef<Path>,
T: AsRef<Path>,
{
let prepared_copy = DirectoryCopyPrepared::prepare(
source_directory_path.as_ref(),
destination_directory_path.as_ref(),
options.destination_directory_rule,
options.copy_depth_limit,
options.symlink_behaviour,
options.broken_symlink_behaviour,
)?;
let finished_copy = copy_directory_unchecked(prepared_copy, options)?;
Ok(finished_copy)
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum DirectoryCopyOperation {
CreatingDirectory {
destination_directory_path: PathBuf,
},
CopyingFile {
destination_file_path: PathBuf,
progress: FileProgress,
},
CreatingSymbolicLink {
destination_symbolic_link_file_path: PathBuf,
},
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DirectoryCopyProgressRef<'o> {
pub bytes_total: u64,
pub bytes_finished: u64,
pub files_copied: usize,
pub symlinks_created: usize,
pub directories_created: usize,
pub current_operation: &'o DirectoryCopyOperation,
pub current_operation_index: usize,
pub total_operations: usize,
}
impl DirectoryCopyProgressRef<'_> {
pub fn to_owned_progress(&self) -> DirectoryCopyProgress {
DirectoryCopyProgress {
bytes_total: self.bytes_total,
bytes_finished: self.bytes_finished,
files_copied: self.files_copied,
symlinks_created: self.symlinks_created,
directories_created: self.directories_created,
current_operation: self.current_operation.to_owned(),
current_operation_index: self.current_operation_index,
total_operations: self.total_operations,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DirectoryCopyProgress {
pub bytes_total: u64,
pub bytes_finished: u64,
pub files_copied: usize,
pub symlinks_created: usize,
pub directories_created: usize,
pub current_operation: DirectoryCopyOperation,
pub current_operation_index: usize,
pub total_operations: usize,
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct DirectoryCopyInternalProgress {
bytes_total: u64,
bytes_finished: u64,
files_copied: usize,
symlinks_created: usize,
directories_created: usize,
current_operation: Option<DirectoryCopyOperation>,
current_operation_index: Option<usize>,
total_operations: usize,
}
impl DirectoryCopyInternalProgress {
fn update_operation_and_emit_progress<M, F>(
&mut self,
mut self_modifier_closure: M,
progress_handler: &mut F,
) where
M: FnMut(&mut Self),
F: FnMut(&DirectoryCopyProgressRef),
{
self_modifier_closure(self);
progress_handler(&self.to_user_facing_progress());
}
fn set_next_operation_and_emit_progress<F>(
&mut self,
operation: DirectoryCopyOperation,
progress_handler: &mut F,
) where
F: FnMut(&DirectoryCopyProgressRef),
{
if let Some(existing_operation_index) = self.current_operation_index.as_mut() {
*existing_operation_index += 1;
} else {
self.current_operation_index = Some(0);
}
self.current_operation = Some(operation);
progress_handler(&self.to_user_facing_progress())
}
fn to_user_facing_progress(&self) -> DirectoryCopyProgressRef<'_> {
let current_operation_reference = self
.current_operation
.as_ref()
.expect("current_operation field to be Some");
let current_operation_index = self
.current_operation_index
.expect("current_operation_index to be Some");
DirectoryCopyProgressRef {
bytes_total: self.bytes_total,
bytes_finished: self.bytes_finished,
files_copied: self.files_copied,
symlinks_created: self.symlinks_created,
directories_created: self.directories_created,
current_operation: current_operation_reference,
current_operation_index,
total_operations: self.total_operations,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DirectoryCopyWithProgressOptions {
pub destination_directory_rule: DestinationDirectoryRule,
pub copy_depth_limit: DirectoryCopyDepthLimit,
pub symlink_behaviour: SymlinkBehaviour,
pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
pub read_buffer_size: usize,
pub write_buffer_size: usize,
pub progress_update_byte_interval: u64,
}
impl Default for DirectoryCopyWithProgressOptions {
fn default() -> Self {
Self {
destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
symlink_behaviour: SymlinkBehaviour::Keep,
broken_symlink_behaviour: BrokenSymlinkBehaviour::Abort,
read_buffer_size: DEFAULT_READ_BUFFER_SIZE,
write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
progress_update_byte_interval: DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
}
}
}
fn execute_copy_file_operation_with_progress<F>(
source_file_path: PathBuf,
source_size_bytes: u64,
destination_path: PathBuf,
options: &DirectoryCopyWithProgressOptions,
progress: &mut DirectoryCopyInternalProgress,
progress_handler: &mut F,
) -> Result<(), CopyDirectoryExecutionError>
where
F: FnMut(&DirectoryCopyProgressRef),
{
let can_overwrite_destination_file = options
.destination_directory_rule
.allows_overwriting_existing_destination_files();
let destination_path_exists =
try_exists_without_follow(&destination_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_path.clone(),
error,
}
})?;
if destination_path_exists {
let destination_path_metadata =
fs::symlink_metadata(&destination_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_path.clone(),
error,
}
})?;
if !destination_path_metadata.is_file() {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_path.clone(),
});
}
if !can_overwrite_destination_file {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_path.clone(),
});
}
}
progress.set_next_operation_and_emit_progress(
DirectoryCopyOperation::CopyingFile {
destination_file_path: destination_path.clone(),
progress: FileProgress {
bytes_finished: 0,
bytes_total: source_size_bytes,
},
},
progress_handler,
);
let mut updated_bytes_total_with_fresh_value = false;
let bytes_copied_before = progress.bytes_finished;
copy_file_with_progress(
source_file_path,
&destination_path,
FileCopyWithProgressOptions {
colliding_file_behaviour: match options.destination_directory_rule {
DestinationDirectoryRule::DisallowExisting => CollidingFileBehaviour::Abort,
DestinationDirectoryRule::AllowEmpty => CollidingFileBehaviour::Abort,
DestinationDirectoryRule::AllowNonEmpty { colliding_file_behaviour, .. } => colliding_file_behaviour,
},
read_buffer_size: options.read_buffer_size,
write_buffer_size: options.write_buffer_size,
progress_update_byte_interval: options.progress_update_byte_interval,
},
|new_file_progress| progress.update_operation_and_emit_progress(
|progress| {
let current_operation = progress.current_operation.as_mut()
.expect("the current_operation field to be Some");
if let DirectoryCopyOperation::CopyingFile {
progress: file_progress,
..
} = current_operation
{
if !updated_bytes_total_with_fresh_value {
file_progress.bytes_total = new_file_progress.bytes_total;
updated_bytes_total_with_fresh_value = true;
}
file_progress.bytes_finished = new_file_progress.bytes_finished;
progress.bytes_finished =
bytes_copied_before + file_progress.bytes_finished;
} else {
panic!(
"BUG: `progress.current_operation` doesn't match DirectoryCopyOperation::CopyingFile"
);
}
},
progress_handler,
)
)
.map_err(|file_error| CopyDirectoryExecutionError::FileCopyError { file_path: destination_path, error: file_error })?;
progress.files_copied += 1;
Ok(())
}
fn execute_create_directory_operation_with_progress<F>(
destination_directory_path: PathBuf,
source_size_bytes: u64,
create_parent_directories: bool,
options: &DirectoryCopyWithProgressOptions,
progress: &mut DirectoryCopyInternalProgress,
progress_handler: &mut F,
) -> Result<(), CopyDirectoryExecutionError>
where
F: FnMut(&DirectoryCopyProgressRef),
{
let destination_directory_exists = try_exists_without_follow(&destination_directory_path)
.map_err(|error| CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_directory_path.clone(),
error,
})?;
if destination_directory_exists {
let destination_directory_metadata = fs::symlink_metadata(&destination_directory_path)
.map_err(|error| CopyDirectoryExecutionError::UnableToAccessDestination {
path: destination_directory_path.clone(),
error,
})?;
if !destination_directory_metadata.is_dir() {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_directory_path,
});
}
if options.destination_directory_rule == DestinationDirectoryRule::DisallowExisting {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: destination_directory_path,
});
}
return Ok(());
}
progress.set_next_operation_and_emit_progress(
DirectoryCopyOperation::CreatingDirectory {
destination_directory_path: destination_directory_path.clone(),
},
progress_handler,
);
if create_parent_directories {
fs::create_dir_all(&destination_directory_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToCreateDirectory {
directory_path: destination_directory_path,
error,
}
})?;
} else {
fs::create_dir(&destination_directory_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToCreateDirectory {
directory_path: destination_directory_path,
error,
}
})?;
}
progress.directories_created += 1;
progress.bytes_finished += source_size_bytes;
Ok(())
}
struct SymlinkCreationInfo {
symlink_path: PathBuf,
symlink_destination_path: PathBuf,
#[cfg(windows)]
symlink_type: crate::directory::prepared::SymlinkType,
unfollowed_symlink_file_size_bytes: u64,
}
fn execute_create_symlink_operation_with_progress<F>(
symlink_info: SymlinkCreationInfo,
options: &DirectoryCopyWithProgressOptions,
progress: &mut DirectoryCopyInternalProgress,
progress_handler: &mut F,
) -> Result<(), CopyDirectoryExecutionError>
where
F: FnMut(&DirectoryCopyProgressRef),
{
let can_overwrite_destination_file = options
.destination_directory_rule
.allows_overwriting_existing_destination_files();
let symlink_path_exists =
try_exists_without_follow(&symlink_info.symlink_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToAccessDestination {
path: symlink_info.symlink_path.clone(),
error,
}
})?;
if symlink_path_exists {
let symlink_path_metadata =
fs::symlink_metadata(&symlink_info.symlink_path).map_err(|error| {
CopyDirectoryExecutionError::UnableToAccessDestination {
path: symlink_info.symlink_path.clone(),
error,
}
})?;
if !symlink_path_metadata.is_symlink() {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: symlink_info.symlink_path,
});
}
if !can_overwrite_destination_file {
return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
path: symlink_info.symlink_path,
});
}
}
progress.set_next_operation_and_emit_progress(
DirectoryCopyOperation::CreatingSymbolicLink {
destination_symbolic_link_file_path: symlink_info.symlink_path.clone(),
},
progress_handler,
);
#[cfg(windows)]
{
use crate::directory::prepared::SymlinkType;
match symlink_info.symlink_type {
SymlinkType::File => {
std::os::windows::fs::symlink_file(
&symlink_info.symlink_destination_path,
&symlink_info.symlink_path,
)
.map_err(|error| {
CopyDirectoryExecutionError::SymlinkCreationError {
symlink_path: symlink_info.symlink_path.clone(),
error,
}
})?;
}
SymlinkType::Directory => {
std::os::windows::fs::symlink_dir(
&symlink_info.symlink_destination_path,
&symlink_info.symlink_path,
)
.map_err(|error| {
CopyDirectoryExecutionError::SymlinkCreationError {
symlink_path: symlink_info.symlink_path.clone(),
error,
}
})?;
}
};
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(
&symlink_info.symlink_destination_path,
&symlink_info.symlink_path,
)
.map_err(|error| CopyDirectoryExecutionError::SymlinkCreationError {
symlink_path: symlink_info.symlink_path.clone(),
error,
})?;
}
progress.symlinks_created += 1;
progress.bytes_finished += symlink_info.unfollowed_symlink_file_size_bytes;
Ok(())
}
pub(crate) fn execute_prepared_copy_directory_with_progress_unchecked<F>(
prepared_copy: DirectoryCopyPrepared,
options: DirectoryCopyWithProgressOptions,
mut progress_handler: F,
) -> Result<DirectoryCopyFinished, CopyDirectoryExecutionError>
where
F: FnMut(&DirectoryCopyProgressRef),
{
let mut progress = DirectoryCopyInternalProgress {
bytes_total: prepared_copy.total_bytes,
bytes_finished: 0,
files_copied: 0,
symlinks_created: 0,
directories_created: 0,
current_operation: None,
current_operation_index: None,
total_operations: prepared_copy.operation_queue.len(),
};
for operation in prepared_copy.operation_queue {
match operation {
QueuedOperation::CopyFile {
source_file_path: source_path,
source_size_bytes,
destination_file_path,
} => execute_copy_file_operation_with_progress(
source_path,
source_size_bytes,
destination_file_path,
&options,
&mut progress,
&mut progress_handler,
)?,
QueuedOperation::CreateDirectory {
source_size_bytes,
destination_directory_path,
create_parent_directories,
} => execute_create_directory_operation_with_progress(
destination_directory_path,
source_size_bytes,
create_parent_directories,
&options,
&mut progress,
&mut progress_handler,
)?,
#[cfg(windows)]
QueuedOperation::CreateSymlink {
symlink_path,
symlink_destination_type: symlink_type,
source_symlink_size_bytes,
symlink_destination_path,
} => execute_create_symlink_operation_with_progress(
SymlinkCreationInfo {
symlink_path,
symlink_destination_path,
symlink_type,
unfollowed_symlink_file_size_bytes: source_symlink_size_bytes,
},
&options,
&mut progress,
&mut progress_handler,
)?,
#[cfg(unix)]
QueuedOperation::CreateSymlink {
symlink_path,
source_symlink_size_bytes,
symlink_destination_path,
} => execute_create_symlink_operation_with_progress(
SymlinkCreationInfo {
symlink_path,
symlink_destination_path,
unfollowed_symlink_file_size_bytes: source_symlink_size_bytes,
},
&options,
&mut progress,
&mut progress_handler,
)?,
}
}
progress_handler(&progress.to_user_facing_progress());
Ok(DirectoryCopyFinished {
total_bytes_copied: progress.bytes_finished,
files_copied: progress.files_copied,
symlinks_created: progress.symlinks_created,
directories_created: progress.directories_created,
})
}
pub fn copy_directory_with_progress<S, T, F>(
source_directory_path: S,
destination_directory_path: T,
options: DirectoryCopyWithProgressOptions,
progress_handler: F,
) -> Result<DirectoryCopyFinished, CopyDirectoryError>
where
S: AsRef<Path>,
T: AsRef<Path>,
F: FnMut(&DirectoryCopyProgressRef),
{
let prepared_copy = DirectoryCopyPrepared::prepare(
source_directory_path.as_ref(),
destination_directory_path.as_ref(),
options.destination_directory_rule,
options.copy_depth_limit,
options.symlink_behaviour,
options.broken_symlink_behaviour,
)?;
let finished_copy = execute_prepared_copy_directory_with_progress_unchecked(
prepared_copy,
options,
progress_handler,
)?;
Ok(finished_copy)
}