use std::path::{Path, PathBuf};
use_enabled_fs_module!();
use super::{
collected::collect_directory_statistics_via_scan,
copy_directory_unchecked,
execute_prepared_copy_directory_with_progress_unchecked,
prepared::{
validate_destination_directory_path,
validate_source_destination_directory_pair,
validate_source_directory_path,
DestinationDirectoryState,
DirectoryCopyPrepared,
ValidatedDestinationDirectory,
ValidatedSourceDirectory,
},
BrokenSymlinkBehaviour,
DestinationDirectoryRule,
DirectoryCopyDepthLimit,
DirectoryCopyOperation,
DirectoryCopyOptions,
DirectoryCopyWithProgressOptions,
SymlinkBehaviour,
};
use crate::{
error::{MoveDirectoryError, MoveDirectoryExecutionError, MoveDirectoryPreparationError},
file::FileProgress,
DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
DEFAULT_READ_BUFFER_SIZE,
DEFAULT_WRITE_BUFFER_SIZE,
};
pub struct DirectoryMoveByCopyOptions {
pub symlink_behaviour: SymlinkBehaviour,
pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
}
impl Default for DirectoryMoveByCopyOptions {
fn default() -> Self {
Self {
symlink_behaviour: SymlinkBehaviour::Keep,
broken_symlink_behaviour: BrokenSymlinkBehaviour::Keep,
}
}
}
pub enum DirectoryMoveAllowedStrategies {
OnlyRename,
OnlyCopyAndDelete {
options: DirectoryMoveByCopyOptions,
},
Either {
copy_and_delete_options: DirectoryMoveByCopyOptions,
},
}
impl DirectoryMoveAllowedStrategies {
#[inline]
pub(crate) fn allowed_to_rename(&self) -> bool {
matches!(self, Self::OnlyRename | Self::Either { .. })
}
#[inline]
pub(crate) fn into_options_if_allowed_to_copy_and_delete(
self,
) -> Option<DirectoryMoveByCopyOptions> {
match self {
DirectoryMoveAllowedStrategies::OnlyRename => None,
DirectoryMoveAllowedStrategies::OnlyCopyAndDelete { options } => Some(options),
DirectoryMoveAllowedStrategies::Either {
copy_and_delete_options,
} => Some(copy_and_delete_options),
}
}
}
impl Default for DirectoryMoveAllowedStrategies {
fn default() -> Self {
Self::Either {
copy_and_delete_options: DirectoryMoveByCopyOptions::default(),
}
}
}
pub struct DirectoryMoveOptions {
pub destination_directory_rule: DestinationDirectoryRule,
pub allowed_strategies: DirectoryMoveAllowedStrategies,
}
impl Default for DirectoryMoveOptions {
fn default() -> Self {
Self {
destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
allowed_strategies: DirectoryMoveAllowedStrategies::default(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DirectoryMoveStrategy {
Rename,
CopyAndDelete,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct DirectoryMoveFinished {
pub total_bytes_moved: u64,
pub files_moved: usize,
pub symlinks_moved: usize,
pub directories_moved: usize,
pub strategy_used: DirectoryMoveStrategy,
}
struct DirectoryContentDetails {
pub(crate) total_bytes: u64,
pub(crate) total_files: usize,
pub(crate) total_symlinks: usize,
pub(crate) total_directories: usize,
}
fn collect_source_directory_details(
source_directory_path: &Path,
) -> Result<DirectoryContentDetails, MoveDirectoryPreparationError> {
let directory_statistics = collect_directory_statistics_via_scan(source_directory_path)?;
Ok(DirectoryContentDetails {
total_bytes: directory_statistics.total_bytes,
total_files: directory_statistics.total_files,
total_symlinks: directory_statistics.total_symlinks,
total_directories: directory_statistics.total_directories,
})
}
pub(crate) enum DirectoryMoveByRenameAction {
Renamed {
finished_move: DirectoryMoveFinished,
},
FailedOrImpossible,
}
fn attempt_directory_move_by_rename(
validated_source_directory: &ValidatedSourceDirectory,
source_directory_details: &DirectoryContentDetails,
validated_destination_directory: &ValidatedDestinationDirectory,
) -> Result<DirectoryMoveByRenameAction, MoveDirectoryExecutionError> {
#[cfg(unix)]
{
if !matches!(
validated_destination_directory.state,
DestinationDirectoryState::DoesNotExist | DestinationDirectoryState::IsEmpty
) {
return Ok(DirectoryMoveByRenameAction::FailedOrImpossible);
}
if fs::rename(
&validated_source_directory.unfollowed_directory_path,
&validated_destination_directory.directory_path,
)
.is_ok()
{
return Ok(DirectoryMoveByRenameAction::Renamed {
finished_move: DirectoryMoveFinished {
total_bytes_moved: source_directory_details.total_bytes,
files_moved: source_directory_details.total_files,
symlinks_moved: source_directory_details.total_symlinks,
directories_moved: source_directory_details.total_directories,
strategy_used: DirectoryMoveStrategy::Rename,
},
});
}
Ok(DirectoryMoveByRenameAction::FailedOrImpossible)
}
#[cfg(windows)]
{
if !matches!(
validated_destination_directory.state,
DestinationDirectoryState::DoesNotExist
) {
return Ok(DirectoryMoveByRenameAction::FailedOrImpossible);
}
if !validated_destination_directory.state.exists()
&& fs::rename(
&validated_source_directory.unfollowed_directory_path,
&validated_destination_directory.directory_path,
)
.is_ok()
{
return Ok(DirectoryMoveByRenameAction::Renamed {
finished_move: DirectoryMoveFinished {
total_bytes_moved: source_directory_details.total_bytes,
files_moved: source_directory_details.total_files,
symlinks_moved: source_directory_details.total_symlinks,
directories_moved: source_directory_details.total_directories,
strategy_used: DirectoryMoveStrategy::Rename,
},
});
}
Ok(DirectoryMoveByRenameAction::FailedOrImpossible)
}
#[cfg(not(any(unix, windows)))]
{
compile_error!(
"fs-more supports only the following values of target_family: unix and windows.\
WASM is unsupported."
);
}
}
pub fn move_directory<S, T>(
source_directory_path: S,
destination_directory_path: T,
options: DirectoryMoveOptions,
) -> Result<DirectoryMoveFinished, MoveDirectoryError>
where
S: AsRef<Path>,
T: AsRef<Path>,
{
let validated_source_directory = validate_source_directory_path(source_directory_path.as_ref())
.map_err(MoveDirectoryPreparationError::SourceDirectoryValidationError)?;
let validated_destination_directory = validate_destination_directory_path(
destination_directory_path.as_ref(),
options.destination_directory_rule,
)
.map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
validate_source_destination_directory_pair(
&validated_source_directory.directory_path,
&validated_destination_directory.directory_path,
)
.map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
let source_details =
collect_source_directory_details(&validated_source_directory.directory_path)?;
if options.allowed_strategies.allowed_to_rename() {
match attempt_directory_move_by_rename(
&validated_source_directory,
&source_details,
&validated_destination_directory,
)? {
DirectoryMoveByRenameAction::Renamed { finished_move } => {
return Ok(finished_move);
}
DirectoryMoveByRenameAction::FailedOrImpossible => {}
};
}
let Some(copy_and_delete_options) = options
.allowed_strategies
.into_options_if_allowed_to_copy_and_delete()
else {
return Err(MoveDirectoryError::ExecutionError(
MoveDirectoryExecutionError::RenameFailedAndNoFallbackStrategy,
));
};
let prepared_copy = DirectoryCopyPrepared::prepare_with_validated(
validated_source_directory.clone(),
validated_destination_directory,
options.destination_directory_rule,
DirectoryCopyDepthLimit::Unlimited,
copy_and_delete_options.symlink_behaviour,
copy_and_delete_options.broken_symlink_behaviour,
)
.map_err(MoveDirectoryPreparationError::CopyPlanningError)?;
copy_directory_unchecked(
prepared_copy,
DirectoryCopyOptions {
destination_directory_rule: options.destination_directory_rule,
copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
symlink_behaviour: copy_and_delete_options.symlink_behaviour,
broken_symlink_behaviour: copy_and_delete_options.broken_symlink_behaviour,
},
)
.map_err(MoveDirectoryExecutionError::CopyDirectoryError)?;
let directory_path_to_remove =
if validated_source_directory.original_path_was_symlink_to_directory {
source_directory_path.as_ref()
} else {
validated_source_directory.directory_path.as_path()
};
fs::remove_dir_all(directory_path_to_remove).map_err(|error| {
MoveDirectoryExecutionError::UnableToAccessSource {
path: validated_source_directory.directory_path,
error,
}
})?;
Ok(DirectoryMoveFinished {
total_bytes_moved: source_details.total_bytes,
files_moved: source_details.total_files,
symlinks_moved: source_details.total_symlinks,
directories_moved: source_details.total_directories,
strategy_used: DirectoryMoveStrategy::CopyAndDelete,
})
}
pub struct DirectoryMoveWithProgressByCopyOptions {
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 DirectoryMoveWithProgressByCopyOptions {
fn default() -> Self {
Self {
symlink_behaviour: SymlinkBehaviour::Keep,
broken_symlink_behaviour: BrokenSymlinkBehaviour::Keep,
read_buffer_size: DEFAULT_READ_BUFFER_SIZE,
write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
progress_update_byte_interval: DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
}
}
}
pub enum DirectoryMoveWithProgressAllowedStrategies {
OnlyRename,
OnlyCopyAndDelete {
options: DirectoryMoveWithProgressByCopyOptions,
},
Either {
copy_and_delete_options: DirectoryMoveWithProgressByCopyOptions,
},
}
impl DirectoryMoveWithProgressAllowedStrategies {
#[inline]
pub(crate) fn allowed_to_rename(&self) -> bool {
matches!(self, Self::OnlyRename | Self::Either { .. })
}
#[inline]
pub(crate) fn into_options_if_allowed_to_copy_and_delete(
self,
) -> Option<DirectoryMoveWithProgressByCopyOptions> {
match self {
DirectoryMoveWithProgressAllowedStrategies::OnlyRename => None,
DirectoryMoveWithProgressAllowedStrategies::OnlyCopyAndDelete { options } => {
Some(options)
}
DirectoryMoveWithProgressAllowedStrategies::Either {
copy_and_delete_options,
} => Some(copy_and_delete_options),
}
}
}
impl Default for DirectoryMoveWithProgressAllowedStrategies {
fn default() -> Self {
Self::Either {
copy_and_delete_options: DirectoryMoveWithProgressByCopyOptions::default(),
}
}
}
pub struct DirectoryMoveWithProgressOptions {
pub destination_directory_rule: DestinationDirectoryRule,
pub allowed_strategies: DirectoryMoveWithProgressAllowedStrategies,
}
impl Default for DirectoryMoveWithProgressOptions {
fn default() -> Self {
Self {
destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
allowed_strategies: DirectoryMoveWithProgressAllowedStrategies::default(),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum DirectoryMoveOperation {
CreatingDirectory {
target_path: PathBuf,
},
CopyingFile {
target_path: PathBuf,
progress: FileProgress,
},
CreatingSymbolicLink {
destination_symbolic_link_file_path: PathBuf,
},
RemovingSourceDirectory,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DirectoryMoveProgress {
pub bytes_total: u64,
pub bytes_finished: u64,
pub files_moved: usize,
pub directories_created: usize,
pub current_operation: DirectoryMoveOperation,
pub current_operation_index: usize,
pub total_operations: usize,
}
pub fn move_directory_with_progress<S, T, F>(
source_directory_path: S,
target_directory_path: T,
options: DirectoryMoveWithProgressOptions,
mut progress_handler: F,
) -> Result<DirectoryMoveFinished, MoveDirectoryError>
where
S: AsRef<Path>,
T: AsRef<Path>,
F: FnMut(&DirectoryMoveProgress),
{
let validated_source_directory = validate_source_directory_path(source_directory_path.as_ref())
.map_err(MoveDirectoryPreparationError::SourceDirectoryValidationError)?;
let validated_destination_directory = validate_destination_directory_path(
target_directory_path.as_ref(),
options.destination_directory_rule,
)
.map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
validate_source_destination_directory_pair(
&validated_source_directory.directory_path,
&validated_destination_directory.directory_path,
)
.map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
let source_details =
collect_source_directory_details(&validated_source_directory.directory_path)?;
if options.allowed_strategies.allowed_to_rename() {
match attempt_directory_move_by_rename(
&validated_source_directory,
&source_details,
&validated_destination_directory,
)? {
DirectoryMoveByRenameAction::Renamed { finished_move } => {
let final_progress_report = DirectoryMoveProgress {
bytes_total: source_details.total_bytes,
bytes_finished: source_details.total_bytes,
files_moved: source_details.total_files,
directories_created: source_details.total_directories,
current_operation: DirectoryMoveOperation::RemovingSourceDirectory,
current_operation_index: 1,
total_operations: 2,
};
progress_handler(&final_progress_report);
return Ok(finished_move);
}
DirectoryMoveByRenameAction::FailedOrImpossible => {}
};
}
let Some(copy_and_delete_options) = options
.allowed_strategies
.into_options_if_allowed_to_copy_and_delete()
else {
return Err(MoveDirectoryError::ExecutionError(
MoveDirectoryExecutionError::RenameFailedAndNoFallbackStrategy,
));
};
let copy_options = DirectoryCopyWithProgressOptions {
destination_directory_rule: options.destination_directory_rule,
read_buffer_size: copy_and_delete_options.read_buffer_size,
write_buffer_size: copy_and_delete_options.write_buffer_size,
progress_update_byte_interval: copy_and_delete_options.progress_update_byte_interval,
copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
symlink_behaviour: copy_and_delete_options.symlink_behaviour,
broken_symlink_behaviour: copy_and_delete_options.broken_symlink_behaviour,
};
let prepared_copy = DirectoryCopyPrepared::prepare_with_validated(
validated_source_directory.clone(),
validated_destination_directory,
copy_options.destination_directory_rule,
copy_options.copy_depth_limit,
copy_and_delete_options.symlink_behaviour,
copy_and_delete_options.broken_symlink_behaviour,
)
.map_err(MoveDirectoryPreparationError::CopyPlanningError)?;
let directory_copy_result = execute_prepared_copy_directory_with_progress_unchecked(
prepared_copy,
copy_options,
|progress| {
let move_operation = match progress.current_operation.clone() {
DirectoryCopyOperation::CreatingDirectory {
destination_directory_path: target_path,
} => DirectoryMoveOperation::CreatingDirectory { target_path },
DirectoryCopyOperation::CopyingFile {
destination_file_path: target_path,
progress,
} => DirectoryMoveOperation::CopyingFile {
target_path,
progress,
},
DirectoryCopyOperation::CreatingSymbolicLink {
destination_symbolic_link_file_path,
} => DirectoryMoveOperation::CreatingSymbolicLink {
destination_symbolic_link_file_path,
},
};
let move_progress = DirectoryMoveProgress {
bytes_total: progress.bytes_total,
bytes_finished: progress.bytes_finished,
current_operation: move_operation,
current_operation_index: progress.current_operation_index,
total_operations: progress.total_operations,
files_moved: progress.files_copied,
directories_created: progress.directories_created,
};
progress_handler(&move_progress)
},
)
.map_err(MoveDirectoryExecutionError::CopyDirectoryError)?;
let directory_path_to_remove =
if validated_source_directory.original_path_was_symlink_to_directory {
source_directory_path.as_ref()
} else {
validated_source_directory.directory_path.as_path()
};
fs::remove_dir_all(directory_path_to_remove).map_err(|error| {
MoveDirectoryExecutionError::UnableToAccessSource {
path: validated_source_directory.directory_path,
error,
}
})?;
Ok(DirectoryMoveFinished {
directories_moved: directory_copy_result.directories_created,
total_bytes_moved: directory_copy_result.total_bytes_copied,
files_moved: directory_copy_result.files_copied,
symlinks_moved: directory_copy_result.symlinks_created,
strategy_used: DirectoryMoveStrategy::CopyAndDelete,
})
}