use std::{
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use_enabled_fs_module!();
use super::{
common::DestinationDirectoryRule,
is_directory_empty_unchecked,
BrokenSymlinkBehaviour,
DirectoryCopyDepthLimit,
SymlinkBehaviour,
};
use crate::{
directory::common::join_relative_source_path_onto_destination,
error::{
CopyDirectoryPreparationError,
DestinationDirectoryPathValidationError,
DirectoryExecutionPlanError,
SourceDirectoryPathValidationError,
},
};
#[cfg(windows)]
#[derive(Clone, Copy, Debug)]
pub(crate) enum SymlinkType {
File,
Directory,
}
#[derive(Clone, Debug)]
pub(crate) enum QueuedOperation {
CopyFile {
source_file_path: PathBuf,
destination_file_path: PathBuf,
source_size_bytes: u64,
},
CreateDirectory {
destination_directory_path: PathBuf,
create_parent_directories: bool,
source_size_bytes: u64,
},
CreateSymlink {
symlink_path: PathBuf,
#[cfg(windows)]
symlink_destination_type: SymlinkType,
symlink_destination_path: PathBuf,
source_symlink_size_bytes: u64,
},
}
pub(crate) fn try_exists_without_follow(path: &Path) -> std::io::Result<bool> {
match fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) => match error.kind() {
ErrorKind::NotFound => Ok(false),
_ => Err(error),
},
}
}
#[derive(Clone, Debug)]
pub(crate) struct ValidatedSourceDirectory {
pub(crate) directory_path: PathBuf,
pub(crate) unfollowed_directory_path: PathBuf,
pub(crate) original_path_was_symlink_to_directory: bool,
}
pub(super) fn validate_source_directory_path(
source_directory_path: &Path,
) -> Result<ValidatedSourceDirectory, SourceDirectoryPathValidationError> {
match try_exists_without_follow(source_directory_path) {
Ok(exists) => {
if !exists {
return Err(SourceDirectoryPathValidationError::NotFound {
directory_path: source_directory_path.to_path_buf(),
});
}
}
Err(error) => {
return Err(SourceDirectoryPathValidationError::UnableToAccess {
directory_path: source_directory_path.to_path_buf(),
error,
});
}
}
let is_symlink_to_directory = {
let metadata_without_follow =
fs::symlink_metadata(source_directory_path).map_err(|error| {
SourceDirectoryPathValidationError::UnableToAccess {
directory_path: source_directory_path.to_path_buf(),
error,
}
})?;
if metadata_without_follow.is_symlink() {
let metadata_with_follow = fs::metadata(source_directory_path).map_err(|error| {
SourceDirectoryPathValidationError::UnableToAccess {
directory_path: source_directory_path.to_path_buf(),
error,
}
})?;
if !metadata_with_follow.is_dir() {
return Err(SourceDirectoryPathValidationError::NotADirectory {
path: source_directory_path.to_path_buf(),
});
} else {
true
}
} else if metadata_without_follow.is_dir() {
false
} else {
return Err(SourceDirectoryPathValidationError::NotADirectory {
path: source_directory_path.to_path_buf(),
});
}
};
let canonical_source_directory_path =
fs::canonicalize(source_directory_path).map_err(|error| {
SourceDirectoryPathValidationError::UnableToAccess {
directory_path: source_directory_path.to_path_buf(),
error,
}
})?;
#[cfg(feature = "dunce")]
{
let de_unced_canonical_path =
dunce::simplified(&canonical_source_directory_path).to_path_buf();
Ok(ValidatedSourceDirectory {
directory_path: de_unced_canonical_path,
unfollowed_directory_path: source_directory_path.to_path_buf(),
original_path_was_symlink_to_directory: is_symlink_to_directory,
})
}
#[cfg(not(feature = "dunce"))]
{
Ok(ValidatedSourceDirectory {
directory_path: canonical_source_directory_path,
unfollowed_directory_path: source_directory_path.to_path_buf(),
original_path_was_symlink_to_directory: is_symlink_to_directory,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DestinationDirectoryState {
DoesNotExist,
IsEmpty,
IsNotEmpty,
}
impl DestinationDirectoryState {
pub(crate) fn exists(&self) -> bool {
!matches!(self, Self::DoesNotExist)
}
}
#[derive(Clone, Debug)]
pub(crate) struct ValidatedDestinationDirectory {
pub(crate) directory_path: PathBuf,
pub(crate) state: DestinationDirectoryState,
}
pub(super) fn validate_destination_directory_path(
destination_directory_path: &Path,
destination_directory_rule: DestinationDirectoryRule,
) -> Result<ValidatedDestinationDirectory, DestinationDirectoryPathValidationError> {
let destination_directory_exists = try_exists_without_follow(destination_directory_path)
.map_err(|error| DestinationDirectoryPathValidationError::UnableToAccess {
directory_path: destination_directory_path.to_path_buf(),
error,
})?;
if destination_directory_exists && !destination_directory_path.is_dir() {
return Err(DestinationDirectoryPathValidationError::NotADirectory {
directory_path: destination_directory_path.to_path_buf(),
});
}
let resolved_destination_directory_path = if destination_directory_exists {
let canonical_destination_directory_path = fs::canonicalize(destination_directory_path)
.map_err(|error| DestinationDirectoryPathValidationError::UnableToAccess {
directory_path: destination_directory_path.to_path_buf(),
error,
})?;
#[cfg(feature = "dunce")]
{
dunce::simplified(&canonical_destination_directory_path).to_path_buf()
}
#[cfg(not(feature = "dunce"))]
{
canonical_destination_directory_path
}
} else {
destination_directory_path.to_path_buf()
};
let destination_directory_state = if destination_directory_exists {
let is_empty = is_directory_empty_unchecked(&resolved_destination_directory_path).map_err(
|error| DestinationDirectoryPathValidationError::UnableToAccess {
directory_path: resolved_destination_directory_path.to_path_buf(),
error,
},
)?;
if is_empty {
DestinationDirectoryState::IsEmpty
} else {
DestinationDirectoryState::IsNotEmpty
}
} else {
DestinationDirectoryState::DoesNotExist
};
match destination_directory_rule {
DestinationDirectoryRule::DisallowExisting => {
if !matches!(destination_directory_state, DestinationDirectoryState::DoesNotExist) {
return Err(DestinationDirectoryPathValidationError::AlreadyExists {
path: resolved_destination_directory_path,
destination_directory_rule,
});
}
}
DestinationDirectoryRule::AllowEmpty => {
if !matches!(
destination_directory_state,
DestinationDirectoryState::DoesNotExist | DestinationDirectoryState::IsEmpty
) {
return Err(DestinationDirectoryPathValidationError::NotEmpty {
directory_path: resolved_destination_directory_path,
destination_directory_rule,
});
}
}
DestinationDirectoryRule::AllowNonEmpty { .. } => {}
}
Ok(ValidatedDestinationDirectory {
directory_path: resolved_destination_directory_path,
state: destination_directory_state,
})
}
pub(super) fn validate_source_destination_directory_pair(
source_directory_path: &Path,
destination_directory_path: &Path,
) -> Result<(), DestinationDirectoryPathValidationError> {
if destination_directory_path.starts_with(source_directory_path) {
return Err(DestinationDirectoryPathValidationError::DescendantOfSourceDirectory {
destination_directory_path: destination_directory_path.to_path_buf(),
source_directory_path: source_directory_path.to_path_buf(),
});
}
Ok(())
}
fn scan_and_plan_directory_copy(
validated_source_directory: &ValidatedSourceDirectory,
validated_destination_directory: &ValidatedDestinationDirectory,
copy_depth_limit: DirectoryCopyDepthLimit,
symlink_behaviour: SymlinkBehaviour,
broken_symlink_behaviour: BrokenSymlinkBehaviour,
) -> Result<Vec<QueuedOperation>, DirectoryExecutionPlanError> {
let mut operation_queue: Vec<QueuedOperation> = Vec::new();
if symlink_behaviour == SymlinkBehaviour::Keep
&& validated_source_directory.original_path_was_symlink_to_directory
{
let source_symlink_size_bytes =
fs::symlink_metadata(&validated_source_directory.directory_path)
.map_err(|error| DirectoryExecutionPlanError::UnableToAccess {
path: validated_source_directory.directory_path.clone(),
error,
})?
.len();
#[cfg(windows)]
{
operation_queue.push(QueuedOperation::CreateSymlink {
symlink_path: validated_destination_directory.directory_path.to_path_buf(),
symlink_destination_type: SymlinkType::Directory,
source_symlink_size_bytes,
symlink_destination_path: validated_source_directory.directory_path.to_path_buf(),
});
}
#[cfg(not(windows))]
{
operation_queue.push(QueuedOperation::CreateSymlink {
symlink_path: validated_destination_directory.directory_path.to_path_buf(),
source_symlink_size_bytes,
symlink_destination_path: validated_source_directory.directory_path.to_path_buf(),
});
}
return Ok(operation_queue);
}
if !validated_destination_directory.state.exists() {
let source_path_size_bytes =
fs::symlink_metadata(&validated_source_directory.directory_path)
.map_err(|error| DirectoryExecutionPlanError::UnableToAccess {
path: validated_source_directory.directory_path.to_path_buf(),
error,
})?
.len();
operation_queue.push(QueuedOperation::CreateDirectory {
source_size_bytes: source_path_size_bytes,
destination_directory_path: validated_destination_directory
.directory_path
.to_path_buf(),
create_parent_directories: true,
});
}
struct PendingDirectoryScan {
directory_path: PathBuf,
directory_path_without_symlink_follows: PathBuf,
depth: usize,
}
let mut directory_scan_queue = Vec::new();
directory_scan_queue.push(PendingDirectoryScan {
directory_path: validated_source_directory.directory_path.clone(),
directory_path_without_symlink_follows: validated_source_directory.directory_path.clone(),
depth: 0,
});
while let Some(next_directory) = directory_scan_queue.pop() {
let directory_iterator = fs::read_dir(&next_directory.directory_path).map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: next_directory.directory_path.clone(),
error,
}
})?;
for directory_item in directory_iterator {
let directory_item =
directory_item.map_err(|error| DirectoryExecutionPlanError::UnableToAccess {
path: next_directory.directory_path.clone(),
error,
})?;
let directory_item_source_path = directory_item.path();
let directory_item_name = directory_item_source_path.file_name().ok_or_else(|| {
DirectoryExecutionPlanError::UnableToAccess {
path: directory_item_source_path.clone(),
error: io::Error::new(
ErrorKind::Other,
"ReadDir's iterator generated a path that terminates in \"..\"",
),
}
})?;
let new_directory_path_without_symlink_follows = next_directory
.directory_path_without_symlink_follows
.join(directory_item_name);
let directory_item_destination_path = join_relative_source_path_onto_destination(
&validated_source_directory.directory_path,
&new_directory_path_without_symlink_follows,
&validated_destination_directory.directory_path,
)
.map_err(|error| {
DirectoryExecutionPlanError::EntryEscapesSourceDirectory { path: error.path }
})?;
let item_type = directory_item.file_type().map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: directory_item_source_path.clone(),
error,
}
})?;
if item_type.is_file() {
let file_metadata = directory_item.metadata().map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: directory_item_source_path.clone(),
error,
}
})?;
let file_size_in_bytes = file_metadata.len();
operation_queue.push(QueuedOperation::CopyFile {
source_file_path: directory_item_source_path,
source_size_bytes: file_size_in_bytes,
destination_file_path: directory_item_destination_path,
});
} else if item_type.is_dir() {
let directory_metadata = directory_item.metadata().map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: directory_item_source_path.clone(),
error,
}
})?;
let directory_size_in_bytes = directory_metadata.len();
operation_queue.push(QueuedOperation::CreateDirectory {
source_size_bytes: directory_size_in_bytes,
destination_directory_path: directory_item_destination_path,
create_parent_directories: false,
});
match copy_depth_limit {
DirectoryCopyDepthLimit::Limited { maximum_depth } => {
if next_directory.depth < maximum_depth {
directory_scan_queue.push(PendingDirectoryScan {
directory_path: directory_item_source_path.clone(),
directory_path_without_symlink_follows:
new_directory_path_without_symlink_follows,
depth: next_directory.depth + 1,
});
}
}
DirectoryCopyDepthLimit::Unlimited => {
directory_scan_queue.push(PendingDirectoryScan {
directory_path: directory_item_source_path.clone(),
directory_path_without_symlink_follows:
new_directory_path_without_symlink_follows,
depth: next_directory.depth + 1,
});
}
};
} else if item_type.is_symlink() {
let resolved_symlink_path =
fs::read_link(&directory_item_source_path).map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: directory_item_source_path.clone(),
error,
}
})?;
let resolved_absolute_symlink_pathbuf = if resolved_symlink_path.is_relative() {
let symlink_parent_directory_path = &next_directory.directory_path;
let resolved_absolute_symlink_path =
symlink_parent_directory_path.join(&resolved_symlink_path);
Some(resolved_absolute_symlink_path)
} else {
None
};
let resolved_absolute_symlink_path = resolved_absolute_symlink_pathbuf
.as_ref()
.unwrap_or(&resolved_symlink_path);
let resolved_symlink_path_exists =
try_exists_without_follow(resolved_absolute_symlink_path).map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: resolved_absolute_symlink_path.to_path_buf(),
error,
}
})?;
if !resolved_symlink_path_exists {
let unresolved_symlink_metadata =
fs::symlink_metadata(&directory_item_source_path).map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: directory_item_source_path.clone(),
error,
}
})?;
let unresolved_symlink_file_size = unresolved_symlink_metadata.len();
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
match broken_symlink_behaviour {
BrokenSymlinkBehaviour::Keep => {
let unresolved_symlink_file_type =
unresolved_symlink_metadata.file_type();
let symbolic_link_type = if unresolved_symlink_file_type
.is_symlink_file()
{
SymlinkType::File
} else if unresolved_symlink_file_type.is_symlink_dir() {
SymlinkType::Directory
} else {
panic!("Unexpected symbolic link type: neither file nor directory.");
};
operation_queue.push(QueuedOperation::CreateSymlink {
symlink_path: directory_item_destination_path,
symlink_destination_type: symbolic_link_type,
source_symlink_size_bytes: unresolved_symlink_file_size,
symlink_destination_path: resolved_symlink_path,
});
}
BrokenSymlinkBehaviour::Abort => {
return Err(DirectoryExecutionPlanError::SymbolicLinkIsBroken {
path: directory_item_source_path.clone(),
});
}
}
}
#[cfg(unix)]
{
match broken_symlink_behaviour {
BrokenSymlinkBehaviour::Keep => {
operation_queue.push(QueuedOperation::CreateSymlink {
symlink_path: directory_item_destination_path,
source_symlink_size_bytes: unresolved_symlink_file_size,
symlink_destination_path: resolved_symlink_path,
});
}
BrokenSymlinkBehaviour::Abort => {
return Err(DirectoryExecutionPlanError::SymbolicLinkIsBroken {
path: directory_item_source_path.clone(),
});
}
}
}
#[cfg(not(any(windows, unix)))]
{
compile_error!(
"fs-more supports only the following values of target_family: \
unix and windows (notably, wasm is unsupported)."
);
}
continue;
}
let resolved_symlink_metadata = fs::metadata(resolved_absolute_symlink_path)
.map_err(|error| DirectoryExecutionPlanError::UnableToAccess {
path: resolved_symlink_path.clone(),
error,
})?;
let resolved_symlink_file_type = resolved_symlink_metadata.file_type();
let resolved_symlink_file_size = resolved_symlink_metadata.len();
match symlink_behaviour {
SymlinkBehaviour::Keep => {
#[cfg(windows)]
{
let symlink_type = if resolved_symlink_file_type.is_file() {
SymlinkType::File
} else if resolved_symlink_file_type.is_dir() {
SymlinkType::Directory
} else if resolved_symlink_file_type.is_symlink() {
unreachable!(
"unexpected filesystem state: followed symbolic link(s), \
but arrived at another symbolic link"
)
} else {
unreachable!(
"unexpected filesystem state: followed symbolic link(s), \
but arrived at something that is none of: file, directory, symlink"
);
};
operation_queue.push(QueuedOperation::CreateSymlink {
symlink_path: directory_item_destination_path,
symlink_destination_type: symlink_type,
source_symlink_size_bytes: resolved_symlink_file_size,
symlink_destination_path: resolved_symlink_path,
});
}
#[cfg(unix)]
{
operation_queue.push(QueuedOperation::CreateSymlink {
symlink_path: directory_item_destination_path,
source_symlink_size_bytes: resolved_symlink_file_size,
symlink_destination_path: resolved_symlink_path,
});
}
#[cfg(not(any(windows, unix)))]
{
compile_error!(
"fs-more supports only the following values of target_family: \
unix and windows (notably, wasm is unsupported)."
);
}
}
SymlinkBehaviour::Follow => {
if resolved_symlink_file_type.is_file() {
operation_queue.push(QueuedOperation::CopyFile {
source_file_path: resolved_absolute_symlink_path.to_path_buf(),
source_size_bytes: resolved_symlink_file_size,
destination_file_path: directory_item_destination_path,
});
} else if resolved_symlink_file_type.is_dir() {
operation_queue.push(QueuedOperation::CreateDirectory {
source_size_bytes: resolved_symlink_file_size,
destination_directory_path: directory_item_destination_path,
create_parent_directories: false,
});
match copy_depth_limit {
DirectoryCopyDepthLimit::Limited { maximum_depth } => {
if next_directory.depth < maximum_depth {
directory_scan_queue.push(PendingDirectoryScan {
directory_path: directory_item_source_path.clone(),
directory_path_without_symlink_follows:
new_directory_path_without_symlink_follows,
depth: next_directory.depth + 1,
});
}
}
DirectoryCopyDepthLimit::Unlimited => {
directory_scan_queue.push(PendingDirectoryScan {
directory_path: directory_item_source_path,
directory_path_without_symlink_follows:
new_directory_path_without_symlink_follows,
depth: next_directory.depth + 1,
});
}
};
} else if resolved_symlink_file_type.is_symlink() {
unreachable!(
"unexpected filesystem state: followed symbolic link(s), \
but arrived at another symbolic link"
)
}
}
}
}
}
}
Ok(operation_queue)
}
fn check_operation_queue_for_collisions(
queue: &[QueuedOperation],
destination_directory_rules: DestinationDirectoryRule,
) -> Result<(), DirectoryExecutionPlanError> {
let overwriting_existing_destination_files_allowed =
destination_directory_rules.allows_overwriting_existing_destination_files();
let existing_destination_subdirectories_allowed =
destination_directory_rules.allows_existing_destination_subdirectories();
if overwriting_existing_destination_files_allowed && existing_destination_subdirectories_allowed
{
return Ok(());
}
for queue_item in queue {
match queue_item {
QueuedOperation::CopyFile {
destination_file_path,
..
} => {
if !overwriting_existing_destination_files_allowed {
let destination_file_exists = try_exists_without_follow(destination_file_path)
.map_err(|error| DirectoryExecutionPlanError::UnableToAccess {
path: destination_file_path.to_path_buf(),
error,
})?;
if destination_file_exists {
return Err(DirectoryExecutionPlanError::DestinationItemAlreadyExists {
path: destination_file_path.clone(),
});
}
}
}
QueuedOperation::CreateDirectory {
destination_directory_path,
..
} => {
if !existing_destination_subdirectories_allowed {
let destination_directory_exists =
try_exists_without_follow(destination_directory_path).map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: destination_directory_path.to_path_buf(),
error,
}
})?;
if destination_directory_exists {
return Err(DirectoryExecutionPlanError::DestinationItemAlreadyExists {
path: destination_directory_path.clone(),
});
}
}
}
QueuedOperation::CreateSymlink { symlink_path, .. } => {
if !overwriting_existing_destination_files_allowed {
let symlink_path_exists =
try_exists_without_follow(symlink_path).map_err(|error| {
DirectoryExecutionPlanError::UnableToAccess {
path: symlink_path.to_path_buf(),
error,
}
})?;
if symlink_path_exists {
return Err(DirectoryExecutionPlanError::DestinationItemAlreadyExists {
path: symlink_path.to_path_buf(),
});
}
}
}
}
}
Ok(())
}
pub(crate) struct DirectoryCopyPrepared {
pub(crate) operation_queue: Vec<QueuedOperation>,
pub(crate) total_bytes: u64,
}
impl DirectoryCopyPrepared {
pub fn prepare(
source_directory_path: &Path,
destination_directory_path: &Path,
destination_directory_rule: DestinationDirectoryRule,
copy_depth_limit: DirectoryCopyDepthLimit,
symlink_behaviour: SymlinkBehaviour,
broken_symlink_behaviour: BrokenSymlinkBehaviour,
) -> Result<Self, CopyDirectoryPreparationError> {
let (canonical_source_directory_path, validated_destination) =
Self::validate_source_and_destination(
source_directory_path,
destination_directory_path,
destination_directory_rule,
)?;
Self::prepare_with_validated(
canonical_source_directory_path,
validated_destination,
destination_directory_rule,
copy_depth_limit,
symlink_behaviour,
broken_symlink_behaviour,
)
.map_err(CopyDirectoryPreparationError::CopyPlanningError)
}
pub fn prepare_with_validated(
validated_source_directory: ValidatedSourceDirectory,
validated_destination_directory: ValidatedDestinationDirectory,
destination_directory_rule: DestinationDirectoryRule,
copy_depth_limit: DirectoryCopyDepthLimit,
symlink_behaviour: SymlinkBehaviour,
broken_symlink_behaviour: BrokenSymlinkBehaviour,
) -> Result<Self, DirectoryExecutionPlanError> {
let operations = Self::prepare_directory_operations(
&validated_source_directory,
&validated_destination_directory,
destination_directory_rule,
copy_depth_limit,
symlink_behaviour,
broken_symlink_behaviour,
)?;
let bytes_total = Self::calculate_total_bytes_to_be_copied(&operations);
Ok(Self {
operation_queue: operations,
total_bytes: bytes_total,
})
}
fn calculate_total_bytes_to_be_copied(queued_operations: &[QueuedOperation]) -> u64 {
queued_operations
.iter()
.map(|item| match item {
QueuedOperation::CopyFile {
source_size_bytes, ..
} => *source_size_bytes,
QueuedOperation::CreateDirectory {
source_size_bytes, ..
} => *source_size_bytes,
QueuedOperation::CreateSymlink {
source_symlink_size_bytes: source_size_bytes,
..
} => *source_size_bytes,
})
.sum::<u64>()
}
fn validate_source_and_destination(
source_directory_path: &Path,
destination_directory_path: &Path,
destination_directory_rule: DestinationDirectoryRule,
) -> Result<
(ValidatedSourceDirectory, ValidatedDestinationDirectory),
CopyDirectoryPreparationError,
> {
let validated_source_directory = validate_source_directory_path(source_directory_path)?;
let validated_target_directory = validate_destination_directory_path(
destination_directory_path,
destination_directory_rule,
)?;
validate_source_destination_directory_pair(
&validated_source_directory.directory_path,
&validated_target_directory.directory_path,
)?;
Ok((validated_source_directory, validated_target_directory))
}
fn prepare_directory_operations(
validated_source_directory: &ValidatedSourceDirectory,
validated_destination_directory: &ValidatedDestinationDirectory,
destination_directory_rule: DestinationDirectoryRule,
copy_depth_limit: DirectoryCopyDepthLimit,
symlink_behaviour: SymlinkBehaviour,
broken_symlink_behaviour: BrokenSymlinkBehaviour,
) -> Result<Vec<QueuedOperation>, DirectoryExecutionPlanError> {
let copy_queue = scan_and_plan_directory_copy(
validated_source_directory,
validated_destination_directory,
copy_depth_limit,
symlink_behaviour,
broken_symlink_behaviour,
)?;
check_operation_queue_for_collisions(©_queue, destination_directory_rule)?;
Ok(copy_queue)
}
}