fs_more/directory/move.rs
1use std::path::{Path, PathBuf};
2
3use_enabled_fs_module!();
4
5use super::{
6 collected::collect_directory_statistics_via_scan,
7 copy_directory_unchecked,
8 execute_prepared_copy_directory_with_progress_unchecked,
9 prepared::{
10 validate_destination_directory_path,
11 validate_source_destination_directory_pair,
12 validate_source_directory_path,
13 DestinationDirectoryState,
14 DirectoryCopyPrepared,
15 ValidatedDestinationDirectory,
16 ValidatedSourceDirectory,
17 },
18 BrokenSymlinkBehaviour,
19 DestinationDirectoryRule,
20 DirectoryCopyDepthLimit,
21 DirectoryCopyOperation,
22 DirectoryCopyOptions,
23 DirectoryCopyWithProgressOptions,
24 SymlinkBehaviour,
25};
26use crate::{
27 error::{MoveDirectoryError, MoveDirectoryExecutionError, MoveDirectoryPreparationError},
28 file::FileProgress,
29 DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
30 DEFAULT_READ_BUFFER_SIZE,
31 DEFAULT_WRITE_BUFFER_SIZE,
32};
33
34
35/// Options for the copy-and-delete strategy when moving a directory.
36///
37/// See also: [`DirectoryMoveOptions`] and [`move_directory`].
38pub struct DirectoryMoveByCopyOptions {
39 /// Sets the behaviour for symbolic links when moving a directory by copy-and-delete.
40 ///
41 /// Note that setting this to [`SymlinkBehaviour::Follow`] instead of
42 /// [`SymlinkBehaviour::Keep`] (keep is the default) will result in behaviour
43 /// that differs than the rename method (that one will always keep symbolic links).
44 /// In other words, if both strategies are enabled and this is changed from the default,
45 /// you will need to look at which strategy was used after the move to discern
46 /// whether symbolic links were actually preserved or not.
47 ///
48 /// This has the same impact as the [`symlink_behaviour`][dco-symlink_behaviour]
49 /// option under [`DirectoryCopyOptions`].
50 ///
51 ///
52 /// [dco-symlink_behaviour]: crate::directory::DirectoryCopyOptions::symlink_behaviour
53 pub symlink_behaviour: SymlinkBehaviour,
54
55 /// Sets the behaviour for broken symbolic links when moving a directory by copy-and-delete.
56 ///
57 /// This has the same impact as the [`broken_symlink_behaviour`][dco-broken_symlink_behaviour]
58 /// option under [`DirectoryCopyOptions`].
59 ///
60 ///
61 /// [dco-broken_symlink_behaviour]: crate::directory::DirectoryCopyOptions::broken_symlink_behaviour
62 pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
63}
64
65impl Default for DirectoryMoveByCopyOptions {
66 /// Initializes the default options for the copy-and-delete strategy when moving a directory:
67 /// - symbolic links are kept, and
68 /// - broken symbolic links are preserved as-is (i.e. kept broken).
69 fn default() -> Self {
70 Self {
71 symlink_behaviour: SymlinkBehaviour::Keep,
72 broken_symlink_behaviour: BrokenSymlinkBehaviour::Keep,
73 }
74 }
75}
76
77
78/// Describes the allowed strategies for moving a directory.
79///
80/// This ensures at least one of "rename" or "copy-and-delete" strategies are enabled at any point.
81/// Unless you have a good reason for picking something else, [`Self::Either`]
82/// is highly recommended. It ensures we always try to rename the directory if the
83/// conditions are right, and fall back to the slower copy-and-delete strategy if that fails.
84///
85/// See also: [`DirectoryMoveOptions`] and [`move_directory`].
86pub enum DirectoryMoveAllowedStrategies {
87 /// Disables the move by copy-and-delete strategy, leaving only the rename strategy.
88 ///
89 /// If renaming fails, for example due to source and destination being on different mount points,
90 /// the corresponding function will return
91 /// [`ExecutionError`]`(`[`RenameFailedAndNoFallbackStrategy`]`)`.
92 ///
93 ///
94 /// [`ExecutionError`]: crate::error::MoveDirectoryError::ExecutionError
95 /// [`RenameFailedAndNoFallbackStrategy`]: crate::error::MoveDirectoryExecutionError::RenameFailedAndNoFallbackStrategy
96 OnlyRename,
97
98 /// Disables the move by rename strategy, leaving only the less efficient,
99 /// but more general, copy-and-delete strategy.
100 OnlyCopyAndDelete {
101 /// Options for the copy-and-delete strategy.
102 options: DirectoryMoveByCopyOptions,
103 },
104
105 /// Enables both the rename and copy-and-delete strategies,
106 /// leaving the optimal choice in the hands of the library.
107 ///
108 /// Generally speaking, a rename will be attempted under the right conditions,
109 /// with the copy-and-delete performed as a fallback if the rename fails.
110 Either {
111 /// Options for the copy-and-delete strategy.
112 copy_and_delete_options: DirectoryMoveByCopyOptions,
113 },
114}
115
116impl DirectoryMoveAllowedStrategies {
117 /// Returns `true` if the allowed move strategies include moving by rename.
118 ///
119 /// # Invariants
120 /// At least one of [`Self::allowed_to_rename`] and [`Self::into_options_if_may_copy_and_delete`]
121 /// will always return `true` or `Some(...)`, respectively.
122 #[inline]
123 pub(crate) fn allowed_to_rename(&self) -> bool {
124 matches!(self, Self::OnlyRename | Self::Either { .. })
125 }
126
127 /// Returns `Some(`[`DirectoryMoveByCopyOptions`])` if the allowed move strategies include moving by copy-and-delete,
128 /// and returns `None` otherwise.
129 ///
130 /// # Invariants
131 /// At least one of [`Self::allowed_to_rename`] and [`Self::into_options_if_may_copy_and_delete`]
132 /// will always return `true` or `Some(...)`, respectively.
133 #[inline]
134 pub(crate) fn into_options_if_allowed_to_copy_and_delete(
135 self,
136 ) -> Option<DirectoryMoveByCopyOptions> {
137 match self {
138 DirectoryMoveAllowedStrategies::OnlyRename => None,
139 DirectoryMoveAllowedStrategies::OnlyCopyAndDelete { options } => Some(options),
140 DirectoryMoveAllowedStrategies::Either {
141 copy_and_delete_options,
142 } => Some(copy_and_delete_options),
143 }
144 }
145}
146
147impl Default for DirectoryMoveAllowedStrategies {
148 /// Returns the default directory move strategy configuration,
149 /// which is with both rename and copy-and-delete enabled.
150 ///
151 /// For details on the default copy-and-delete options,
152 /// see [`DirectoryMoveByCopyOptions::default`].
153 fn default() -> Self {
154 Self::Either {
155 copy_and_delete_options: DirectoryMoveByCopyOptions::default(),
156 }
157 }
158}
159
160
161/// Options that influence the [`move_directory`] function.
162///
163/// ## `destination_directory_rule` considerations
164/// If you allow the destination directory to exist and be non-empty,
165/// source directory contents will be merged (!) into the destination directory.
166/// This is not the default, and you should probably consider the consequences
167/// very carefully before using that option.
168pub struct DirectoryMoveOptions {
169 /// Specifies whether you allow the target directory to exist before moving
170 /// and whether it must be empty or not.
171 ///
172 /// If you allow a non-empty target directory, you may also specify whether you allow
173 /// destination files or subdirectories to already exist
174 /// (and whether you allow them to be overwritten).
175 ///
176 /// See [`DestinationDirectoryRule`] for more details and examples.
177 pub destination_directory_rule: DestinationDirectoryRule,
178
179 /// Sets the allowed directory move strategies.
180 /// Per-strategy options are also configured here.
181 pub allowed_strategies: DirectoryMoveAllowedStrategies,
182}
183
184impl Default for DirectoryMoveOptions {
185 fn default() -> Self {
186 Self {
187 destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
188 allowed_strategies: DirectoryMoveAllowedStrategies::default(),
189 }
190 }
191}
192
193
194
195/// Describes a strategy usef when a directory move was performed.
196///
197/// This is included in [`DirectoryMoveFinished`] to allow
198/// callers to understand how the directory was moved.
199///
200/// This is used only as a return value; if you want to control the
201/// available directory move strategies, see [`DirectoryMoveAllowedStrategies`]
202/// and the options described in [`move_directory`] / [`move_directory_with_progress`].
203#[derive(Clone, Copy, PartialEq, Eq, Debug)]
204pub enum DirectoryMoveStrategy {
205 /// The source directory was simply renamed from the source path to the target path.
206 ///
207 /// **This is the fastest method**, to the point of being near instantaneous,
208 /// but generally works only if both paths are on the same mount point or drive.
209 Rename,
210
211 /// The source directory was recursively copied to the target directory,
212 /// and the source directory was deleted afterwards.
213 ///
214 /// This method is as fast as a normal recursive copy.
215 /// It is also unavoidable if the directory can't renamed, which can happen when the source and destination
216 /// directory exist on different mount points or drives.
217 CopyAndDelete,
218}
219
220
221
222/// Describes actions taken by the [`move_directory`] function.
223///
224/// This is the return value of [`move_directory`] and [`move_directory_with_progress`].
225#[derive(Clone, Copy, PartialEq, Eq, Debug)]
226pub struct DirectoryMoveFinished {
227 /// Total number of bytes moved.
228 pub total_bytes_moved: u64,
229
230 /// Number of files moved (details depend on strategy).
231 pub files_moved: usize,
232
233 /// Total number of symlinks moved (details depend on strategy).
234 pub symlinks_moved: usize,
235
236 /// Number of directories moved (details depend on strategy).
237 pub directories_moved: usize,
238
239 /// How the directory was moved: was is simply renamed or was it copied and deleted.
240 pub strategy_used: DirectoryMoveStrategy,
241}
242
243
244
245/// Summarizes the contents of a directory for internal use.
246struct DirectoryContentDetails {
247 /// Total size of the directory in bytes.
248 pub(crate) total_bytes: u64,
249
250 /// Total number of files in the directory (recursive).
251 pub(crate) total_files: usize,
252
253 /// Total number of symlinks in the directory (recursive).
254 pub(crate) total_symlinks: usize,
255
256 /// Total number of subdirectories in the directory (recursive).
257 pub(crate) total_directories: usize,
258}
259
260
261
262/// Scans the provided directory for auxiliary details (without a depth limit).
263/// This includes information like the total number of bytes it contains.
264fn collect_source_directory_details(
265 source_directory_path: &Path,
266) -> Result<DirectoryContentDetails, MoveDirectoryPreparationError> {
267 let directory_statistics = collect_directory_statistics_via_scan(source_directory_path)?;
268
269 Ok(DirectoryContentDetails {
270 total_bytes: directory_statistics.total_bytes,
271 total_files: directory_statistics.total_files,
272 total_symlinks: directory_statistics.total_symlinks,
273 total_directories: directory_statistics.total_directories,
274 })
275}
276
277
278/// Describes the result of a [`attempt_directory_move_by_rename`] call,
279/// signalling whether the rename succeeded or not.
280pub(crate) enum DirectoryMoveByRenameAction {
281 /// The directory was successfully moved by renaming it to the destination.
282 Renamed {
283 /// Details of the finished directory move.
284 finished_move: DirectoryMoveFinished,
285 },
286
287 /// The directory could not be moved by renaming it,
288 /// be it either due to the destination being non-empty or due to
289 /// failing the actual directory rename call.
290 FailedOrImpossible,
291}
292
293
294/// Attempts a directory move by using [`std::fs::rename`]
295/// (or `fs_err::rename` if the `fs-err` feature flag is enabled).
296///
297/// Returns [`DirectoryMoveByRenameAction`], which indicates whether the move by rename
298/// succeeded or failed due to source and destination being on different mount points or drives.
299fn attempt_directory_move_by_rename(
300 validated_source_directory: &ValidatedSourceDirectory,
301 source_directory_details: &DirectoryContentDetails,
302 validated_destination_directory: &ValidatedDestinationDirectory,
303) -> Result<DirectoryMoveByRenameAction, MoveDirectoryExecutionError> {
304 // We can attempt to simply rename the directory. This is much faster,
305 // but will fail if the source and target paths aren't on the same mount point or filesystem
306 // or, if on Windows, the target directory already exists.
307
308 #[cfg(unix)]
309 {
310 // If the destination directory either does not exist or is empty,
311 // a move by rename might be possible, but not otherwise.
312 if !matches!(
313 validated_destination_directory.state,
314 DestinationDirectoryState::DoesNotExist | DestinationDirectoryState::IsEmpty
315 ) {
316 return Ok(DirectoryMoveByRenameAction::FailedOrImpossible);
317 }
318
319
320 // Let's try to rename the source directory to the target.
321 // This might still fail due to different mount points.
322 if fs::rename(
323 &validated_source_directory.unfollowed_directory_path,
324 &validated_destination_directory.directory_path,
325 )
326 .is_ok()
327 {
328 return Ok(DirectoryMoveByRenameAction::Renamed {
329 finished_move: DirectoryMoveFinished {
330 total_bytes_moved: source_directory_details.total_bytes,
331 files_moved: source_directory_details.total_files,
332 symlinks_moved: source_directory_details.total_symlinks,
333 directories_moved: source_directory_details.total_directories,
334 strategy_used: DirectoryMoveStrategy::Rename,
335 },
336 });
337 }
338
339 Ok(DirectoryMoveByRenameAction::FailedOrImpossible)
340 }
341
342 #[cfg(windows)]
343 {
344 // If the destination directory does not exist,
345 // a move by rename might be possible, but not otherwise.
346 // This is because we're on Windows, where renames are only possible with non-existing destinations.
347 if !matches!(
348 validated_destination_directory.state,
349 DestinationDirectoryState::DoesNotExist
350 ) {
351 return Ok(DirectoryMoveByRenameAction::FailedOrImpossible);
352 }
353
354
355 // On Windows, the destination directory in call to `rename` must not exist for it to work.
356 if !validated_destination_directory.state.exists()
357 && fs::rename(
358 &validated_source_directory.unfollowed_directory_path,
359 &validated_destination_directory.directory_path,
360 )
361 .is_ok()
362 {
363 return Ok(DirectoryMoveByRenameAction::Renamed {
364 finished_move: DirectoryMoveFinished {
365 total_bytes_moved: source_directory_details.total_bytes,
366 files_moved: source_directory_details.total_files,
367 symlinks_moved: source_directory_details.total_symlinks,
368 directories_moved: source_directory_details.total_directories,
369 strategy_used: DirectoryMoveStrategy::Rename,
370 },
371 });
372 }
373
374 Ok(DirectoryMoveByRenameAction::FailedOrImpossible)
375 }
376
377 #[cfg(not(any(unix, windows)))]
378 {
379 compile_error!(
380 "fs-more supports only the following values of target_family: unix and windows.\
381 WASM is unsupported."
382 );
383 }
384}
385
386
387
388/// Moves a directory from the source to the destination directory.
389///
390/// `source_directory_path` must point to an existing directory.
391///
392/// # Symbolic links
393/// If `source_directory_path` is itself a symlink to a directory,
394/// we'll try to move the link itself by renaming it to the destination.
395/// If the rename fails, the link will be followed and not preserved
396/// by performing a directory copy, after which the symlink will be removed.
397///
398/// For symlinks *inside* the source directory, the behaviour is different depending on the move strategy
399/// (individual strategies can be disabled, see section below):
400/// - If the destination is non-existent (or empty), a move by rename will be attempted first.
401/// In that case, any symbolic links inside the source directory, valid or not, will be preserved.
402/// - If the copy-and-delete fallback is used, the behaviour depends on the [`symlink_behaviour`]
403/// option for that particular strategy (the default is to keep symbolic links as-is).
404///
405///
406/// # Options
407/// See [`DirectoryMoveOptions`] for a full set of available directory moving options.
408/// Note that certain strategy-specific options, such as copy-and-delete settings,
409/// are available under the [`allowed_strategies`] options field
410/// (see e.g. [`DirectoryMoveAllowedStrategies::Either`]).
411///
412/// ### `destination_directory_rule` considerations
413/// If you allow the destination directory to exist and be non-empty,
414/// source directory contents will be merged (!) into the destination directory.
415/// This is *not* the default, and you should probably consider the consequences
416/// very carefully before setting the corresponding [`options.destination_directory_rule`]
417/// option to anything other than [`DisallowExisting`] or [`AllowEmpty`].
418///
419///
420/// # Move strategies
421/// The move can be performed using either of the two available strategies:
422/// - The source directory can be simply renamed to the destination directory.
423/// This is the preferred (and fastest) method. Additionally, if `source_directory_path` is itself
424/// a symbolic link it has the side effect of preserving that.
425/// This strategy requires that the destination directory is either empty or doesn't exist,
426/// though precise conditions depend on platform<sup>*</sup>.
427/// - If the directory can't be renamed, the function will fall back to a copy-and-rename strategy.
428///
429/// **By default, a rename is attempted first, with copy-and-delete available as a fallback.**
430/// Either of these strategies can be disabled in the options struct (see [`allowed_strategies`]),
431/// but at least one must always be enabled.
432///
433///
434/// For more information, see [`DirectoryMoveStrategy`].
435///
436/// <sup>* Windows: the destination directory must not exist at all; if it does,
437/// *even if it is empty*, the rename strategy will fail.</sup>
438///
439///
440/// # Return value
441/// Upon success, the function returns the number of files and directories that were moved
442/// as well as the total number of bytes moved and how the move was performed
443/// (see [`DirectoryMoveFinished`]).
444///
445///
446///
447/// <br>
448///
449/// #### See also
450/// If you are looking for a directory moving function function that reports progress,
451/// see [`move_directory_with_progress`].
452///
453///
454/// [`copy_directory`]: super::copy_directory
455/// [`symlink_behaviour`]: DirectoryMoveByCopyOptions::symlink_behaviour
456/// [`allowed_strategies`]: DirectoryMoveOptions::allowed_strategies
457/// [`options.destination_directory_rule`]: DirectoryMoveOptions::destination_directory_rule
458/// [`DisallowExisting`]: DestinationDirectoryRule::DisallowExisting
459/// [`AllowEmpty`]: DestinationDirectoryRule::AllowEmpty
460pub fn move_directory<S, T>(
461 source_directory_path: S,
462 destination_directory_path: T,
463 options: DirectoryMoveOptions,
464) -> Result<DirectoryMoveFinished, MoveDirectoryError>
465where
466 S: AsRef<Path>,
467 T: AsRef<Path>,
468{
469 let validated_source_directory = validate_source_directory_path(source_directory_path.as_ref())
470 .map_err(MoveDirectoryPreparationError::SourceDirectoryValidationError)?;
471
472
473 let validated_destination_directory = validate_destination_directory_path(
474 destination_directory_path.as_ref(),
475 options.destination_directory_rule,
476 )
477 .map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
478
479 validate_source_destination_directory_pair(
480 &validated_source_directory.directory_path,
481 &validated_destination_directory.directory_path,
482 )
483 .map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
484
485
486 let source_details =
487 collect_source_directory_details(&validated_source_directory.directory_path)?;
488
489
490 if options.allowed_strategies.allowed_to_rename() {
491 match attempt_directory_move_by_rename(
492 &validated_source_directory,
493 &source_details,
494 &validated_destination_directory,
495 )? {
496 DirectoryMoveByRenameAction::Renamed { finished_move } => {
497 return Ok(finished_move);
498 }
499 DirectoryMoveByRenameAction::FailedOrImpossible => {}
500 };
501 }
502
503
504 let Some(copy_and_delete_options) = options
505 .allowed_strategies
506 .into_options_if_allowed_to_copy_and_delete()
507 else {
508 // This branch can execute only when a rename was attempted and failed,
509 // and the user disabled the copy-and-delete fallback strategy.
510 return Err(MoveDirectoryError::ExecutionError(
511 MoveDirectoryExecutionError::RenameFailedAndNoFallbackStrategy,
512 ));
513 };
514
515
516 // At this point a simple rename was either impossible or failed,
517 // but the copy-and-delete fallback is enabled, so we should do that.
518 let prepared_copy = DirectoryCopyPrepared::prepare_with_validated(
519 validated_source_directory.clone(),
520 validated_destination_directory,
521 options.destination_directory_rule,
522 DirectoryCopyDepthLimit::Unlimited,
523 copy_and_delete_options.symlink_behaviour,
524 copy_and_delete_options.broken_symlink_behaviour,
525 )
526 .map_err(MoveDirectoryPreparationError::CopyPlanningError)?;
527
528 copy_directory_unchecked(
529 prepared_copy,
530 DirectoryCopyOptions {
531 destination_directory_rule: options.destination_directory_rule,
532 copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
533 symlink_behaviour: copy_and_delete_options.symlink_behaviour,
534 broken_symlink_behaviour: copy_and_delete_options.broken_symlink_behaviour,
535 },
536 )
537 .map_err(MoveDirectoryExecutionError::CopyDirectoryError)?;
538
539
540 let directory_path_to_remove =
541 if validated_source_directory.original_path_was_symlink_to_directory {
542 source_directory_path.as_ref()
543 } else {
544 validated_source_directory.directory_path.as_path()
545 };
546
547 fs::remove_dir_all(directory_path_to_remove).map_err(|error| {
548 MoveDirectoryExecutionError::UnableToAccessSource {
549 path: validated_source_directory.directory_path,
550 error,
551 }
552 })?;
553
554
555 Ok(DirectoryMoveFinished {
556 total_bytes_moved: source_details.total_bytes,
557 files_moved: source_details.total_files,
558 symlinks_moved: source_details.total_symlinks,
559 directories_moved: source_details.total_directories,
560 strategy_used: DirectoryMoveStrategy::CopyAndDelete,
561 })
562}
563
564
565
566
567/// Options for the copy-and-delete strategy when
568/// configuring a directory move with progress tracking.
569///
570/// See also: [`DirectoryMoveWithProgressOptions`] and [`move_directory_with_progress`].
571pub struct DirectoryMoveWithProgressByCopyOptions {
572 /// Sets the behaviour for symbolic links when moving a directory by copy-and-delete.
573 ///
574 /// Note that setting this to [`SymlinkBehaviour::Follow`] instead of
575 /// [`SymlinkBehaviour::Keep`] (keep is the default) will result in behaviour
576 /// that differs than the rename method (that one will always keep symbolic links).
577 /// In other words, if both strategies are enabled and this is changed from the default,
578 /// you will need to look at which strategy was used after the move to discern
579 /// whether symbolic links were actually preserved or not.
580 ///
581 /// This has the same impact as the [`symlink_behaviour`][dco-symlink_behaviour] option
582 /// under [`DirectoryCopyWithProgressOptions`].
583 ///
584 ///
585 /// [dco-symlink_behaviour]: crate::directory::DirectoryCopyWithProgressOptions::symlink_behaviour
586 pub symlink_behaviour: SymlinkBehaviour,
587
588 /// Sets the behaviour for broken symbolic links when moving a directory by copy-and-delete.
589 ///
590 /// This has the same impact as the [`broken_symlink_behaviour`][dco-broken_symlink_behaviour] option
591 /// under [`DirectoryCopyWithProgressOptions`].
592 ///
593 ///
594 /// [dco-broken_symlink_behaviour]: crate::directory::DirectoryCopyWithProgressOptions::broken_symlink_behaviour
595 pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
596
597 /// Internal buffer size used for reading from source files.
598 ///
599 /// Defaults to 64 KiB.
600 pub read_buffer_size: usize,
601
602 /// Internal buffer size used for writing to destination files.
603 ///
604 /// Defaults to 64 KiB.
605 pub write_buffer_size: usize,
606
607 /// *Minimum* number of bytes written between two consecutive progress reports.
608 ///
609 /// Defaults to 512 KiB.
610 ///
611 /// *Note that the real reporting interval can be larger.*
612 pub progress_update_byte_interval: u64,
613}
614
615impl Default for DirectoryMoveWithProgressByCopyOptions {
616 fn default() -> Self {
617 Self {
618 symlink_behaviour: SymlinkBehaviour::Keep,
619 broken_symlink_behaviour: BrokenSymlinkBehaviour::Keep,
620 read_buffer_size: DEFAULT_READ_BUFFER_SIZE,
621 write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
622 progress_update_byte_interval: DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
623 }
624 }
625}
626
627
628/// Describes the allowed strategies for moving a directory
629/// (with progress tracking).
630///
631/// This ensures at least one of "rename" or "copy-and-delete" strategies are enabled at any point.
632/// Unless you have a good reason for picking something else, [`Self::Either`]
633/// is highly recommended. It ensures we always try to rename the directory if the
634/// conditions are right, and fall back to the slower copy-and-delete strategy if that fails.
635///
636/// See also: [`DirectoryMoveWithProgressOptions`] and [`move_directory_with_progress`].
637pub enum DirectoryMoveWithProgressAllowedStrategies {
638 /// Disables the move by copy-and-delete strategy, leaving only the rename strategy.
639 ///
640 /// If renaming fails, for example due to source and destination being on different
641 /// mount points, the corresponding function will return
642 /// [`ExecutionError`]`(`[`RenameFailedAndNoFallbackStrategy`]`)`.
643 ///
644 ///
645 /// [`ExecutionError`]: crate::error::MoveDirectoryError::ExecutionError
646 /// [`RenameFailedAndNoFallbackStrategy`]: crate::error::MoveDirectoryExecutionError::RenameFailedAndNoFallbackStrategy
647 OnlyRename,
648
649 /// Disables the move by rename strategy, leaving only the less efficient,
650 /// but more general, copy-and-delete strategy.
651 OnlyCopyAndDelete {
652 /// Options for the copy-and-delete strategy.
653 options: DirectoryMoveWithProgressByCopyOptions,
654 },
655
656 /// Enables both the rename and copy-and-delete strategies,
657 /// leaving the optimal choice in the hands of the library.
658 ///
659 /// Generally speaking, a rename will be attempted under the right conditions,
660 /// with the copy-and-delete performed as a fallback if the rename fails.
661 Either {
662 /// Options for the copy-and-delete strategy.
663 copy_and_delete_options: DirectoryMoveWithProgressByCopyOptions,
664 },
665}
666
667impl DirectoryMoveWithProgressAllowedStrategies {
668 ///
669 /// Returns `true` if the allowed strategies include moving by rename.
670 ///
671 /// # Invariants
672 /// At least one of [`Self::into_options_if_may_copy_and_delete`] and [`Self::may_rename`] will always return `true` / `Some`.
673 #[inline]
674 pub(crate) fn allowed_to_rename(&self) -> bool {
675 matches!(self, Self::OnlyRename | Self::Either { .. })
676 }
677
678 /// Returns `Some(`[`DirectoryMoveWithProgressByCopyOptions`])` if the allowed strategies include moving by copy-and-delete,
679 /// `None` otherwise.
680 ///
681 /// # Invariants
682 /// At least one of [`Self::into_options_if_may_copy_and_delete`] and [`Self::may_rename`] will always return `true` / `Some`.
683 #[inline]
684 pub(crate) fn into_options_if_allowed_to_copy_and_delete(
685 self,
686 ) -> Option<DirectoryMoveWithProgressByCopyOptions> {
687 match self {
688 DirectoryMoveWithProgressAllowedStrategies::OnlyRename => None,
689 DirectoryMoveWithProgressAllowedStrategies::OnlyCopyAndDelete { options } => {
690 Some(options)
691 }
692 DirectoryMoveWithProgressAllowedStrategies::Either {
693 copy_and_delete_options,
694 } => Some(copy_and_delete_options),
695 }
696 }
697}
698
699impl Default for DirectoryMoveWithProgressAllowedStrategies {
700 /// Returns the default directory move strategy configuration,
701 /// which is with both rename and copy-and-delete enabled.
702 ///
703 /// For details on the default copy-and-delete options,
704 /// see [`DirectoryMoveWithProgressByCopyOptions::default`].
705 fn default() -> Self {
706 Self::Either {
707 copy_and_delete_options: DirectoryMoveWithProgressByCopyOptions::default(),
708 }
709 }
710}
711
712
713
714/// Options that influence the [`move_directory_with_progress`] function.
715///
716/// ## `destination_directory_rule` considerations
717/// If you allow the destination directory to exist and be non-empty,
718/// source directory contents will be merged (!) into the destination directory.
719/// This is not the default, and you should probably consider the consequences
720/// very carefully before using that option.
721pub struct DirectoryMoveWithProgressOptions {
722 /// Specifies whether you allow the destination directory to exist before moving
723 /// and whether it must be empty or not.
724 ///
725 /// If you allow a non-empty destination directory, you may also specify whether you allow
726 /// destination files or subdirectories to already exist (and be overwritten).
727 ///
728 /// See [`DestinationDirectoryRule`] for more details and examples.
729 pub destination_directory_rule: DestinationDirectoryRule,
730
731 /// Sets the allowed directory move strategies.
732 /// Per-strategy options are also configured here.
733 pub allowed_strategies: DirectoryMoveWithProgressAllowedStrategies,
734}
735
736impl Default for DirectoryMoveWithProgressOptions {
737 fn default() -> Self {
738 Self {
739 destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
740 allowed_strategies: DirectoryMoveWithProgressAllowedStrategies::default(),
741 }
742 }
743}
744
745
746/// Describes a directory move operation.
747///
748/// Used in progress reporting in [`move_directory_with_progress`].
749#[derive(Clone, PartialEq, Eq, Debug)]
750pub enum DirectoryMoveOperation {
751 /// Describes a directory creation operation.
752 CreatingDirectory {
753 /// Path of the directory that is being created.
754 target_path: PathBuf,
755 },
756
757 /// Describes a file being copied.
758 /// For more precise copying progress, see the `progress` field.
759 CopyingFile {
760 /// Path of the file is being created.
761 target_path: PathBuf,
762
763 /// Progress of the file operation.
764 progress: FileProgress,
765 },
766
767 /// Describes a symbolic link being created.
768 CreatingSymbolicLink {
769 /// Path to the symlink being created.
770 destination_symbolic_link_file_path: PathBuf,
771 },
772
773 /// Describes removal of the source directory.
774 /// This happens at the very end when moving a directory.
775 RemovingSourceDirectory,
776}
777
778
779/// Represents the progress of moving a directory.
780///
781/// Used to report directory moving progress to a user-provided closure,
782/// see [`move_directory_with_progress`].
783#[derive(Clone, PartialEq, Eq, Debug)]
784pub struct DirectoryMoveProgress {
785 /// Number of bytes that need to be moved for the directory move to be complete.
786 pub bytes_total: u64,
787
788 /// Number of bytes that have been moved so far.
789 pub bytes_finished: u64,
790
791 /// Number of files that have been moved so far.
792 ///
793 /// If the copy-and-delete strategy is used under the hood,
794 /// this can instead mean how many files have been *copied* so far
795 /// (deletion will come at the end). For more information, see [`DirectoryMoveStrategy`].
796 pub files_moved: usize,
797
798 /// Number of directories that have been created so far.
799 pub directories_created: usize,
800
801 /// The current operation being performed.
802 pub current_operation: DirectoryMoveOperation,
803
804 /// The index of the current operation (starts at `0`, goes to `total_operations - 1`).
805 pub current_operation_index: usize,
806
807 /// The total number of operations that need to be performed to move the requested directory.
808 ///
809 /// A single operation is one of (see [`DirectoryMoveProgress`]):
810 /// - copying a file,
811 /// - creating a directory or
812 /// - removing the source directory (at the very end).
813 pub total_operations: usize,
814}
815
816
817/// Moves a directory from the source to the destination directory, with progress reporting.
818///
819/// `source_directory_path` must point to an existing directory.
820///
821/// # Symbolic links
822/// If `source_directory_path` is itself a symlink to a directory,
823/// we'll try to move the link itself by renaming it to the destination.
824/// If the rename fails, the link will be followed and not preserved
825/// by performing a directory copy, after which the symlink will be removed.
826///
827/// For symlinks *inside* the source directory, the behaviour is different depending on the move strategy
828/// (individual strategies can be disabled, see section below):
829/// - If the destination is non-existent (or empty), a move by rename will be attempted first.
830/// In that case, any symbolic links inside the source directory, valid or not, will be preserved.
831/// - If the copy-and-delete fallback is used, the behaviour depends on the [`symlink_behaviour`]
832/// option for that particular strategy (the default is to keep symbolic links as-is).
833///
834///
835/// # Options
836/// See [`DirectoryMoveWithProgressOptions`] for a full set of available directory moving options.
837/// Note that certain strategy-specific options, such as copy-and-delete settings,
838/// are available under the [`allowed_strategies`] options field
839/// (see e.g. [`DirectoryMoveWithProgressAllowedStrategies::Either`]).
840///
841/// ### `destination_directory_rule` considerations
842/// If you allow the destination directory to exist and be non-empty,
843/// source directory contents will be merged (!) into the destination directory.
844/// This is *not* the default, and you should probably consider the consequences
845/// very carefully before setting the corresponding [`options.destination_directory_rule`]
846/// option to anything other than [`DisallowExisting`] or [`AllowEmpty`].
847///
848///
849/// # Move strategies
850/// The move can be performed using either of the two available strategies:
851/// - The source directory can be simply renamed to the destination directory.
852/// This is the preferred (and fastest) method. Additionally, if `source_directory_path` is itself
853/// a symbolic link it has the side effect of preserving that.
854/// This strategy requires that the destination directory is either empty or doesn't exist,
855/// though precise conditions depend on platform<sup>*</sup>.
856/// - If the directory can't be renamed, the function will fall back to a copy-and-rename strategy.
857///
858/// **By default, a rename is attempted first, with copy-and-delete available as a fallback.**
859/// Either of these strategies can be disabled in the options struct (see [`allowed_strategies`]),
860/// but at least one must always be enabled.
861///
862///
863/// For more information, see [`DirectoryMoveStrategy`].
864///
865/// <sup>* Windows: the destination directory must not exist at all; if it does,
866/// *even if it is empty*, the rename strategy will fail.</sup>
867///
868///
869/// # Return value
870/// Upon success, the function returns the number of files and directories that were moved
871/// as well as the total number of bytes moved and how the move was performed
872/// (see [`DirectoryMoveFinished`]).
873///
874///
875/// ### Progress reporting
876/// This function allows you to receive progress reports by providing
877/// a `progress_handler` closure. It will be called with
878/// a reference to [`DirectoryMoveProgress`] regularly.
879///
880/// You can control the progress reporting frequency by setting the
881/// [`progress_update_byte_interval`] option to a sufficiencly small or large value,
882/// but note that smaller intervals are likely to have an additional impact on performance.
883/// The value of this option if the minimum number of bytes written to a file between
884/// two calls to the provided `progress_handler`.
885///
886/// This function does not guarantee a precise number of progress reports;
887/// it does, however, guarantee at least one progress report per file copy, symlink and directory operation.
888/// It also guarantees one final progress report, when the state indicates the move has been completed.
889///
890/// If the move can be performed by renaming the directory, only one progress report will be emitted.
891///
892///
893/// <br>
894///
895/// #### See also
896/// If you are looking for a directory moving function function that does not report progress,
897/// see [`move_directory`].
898///
899///
900/// [`copy_directory_with_progress`]: super::copy_directory_with_progress
901/// [`symlink_behaviour`]: DirectoryMoveWithProgressByCopyOptions::symlink_behaviour
902/// [`allowed_strategies`]: DirectoryMoveWithProgressOptions::allowed_strategies
903/// [`options.destination_directory_rule`]: DirectoryMoveWithProgressOptions::destination_directory_rule
904/// [`progress_update_byte_interval`]: DirectoryMoveWithProgressByCopyOptions::progress_update_byte_interval
905/// [`DisallowExisting`]: DestinationDirectoryRule::DisallowExisting
906/// [`AllowEmpty`]: DestinationDirectoryRule::AllowEmpty
907pub fn move_directory_with_progress<S, T, F>(
908 source_directory_path: S,
909 target_directory_path: T,
910 options: DirectoryMoveWithProgressOptions,
911 mut progress_handler: F,
912) -> Result<DirectoryMoveFinished, MoveDirectoryError>
913where
914 S: AsRef<Path>,
915 T: AsRef<Path>,
916 F: FnMut(&DirectoryMoveProgress),
917{
918 let validated_source_directory = validate_source_directory_path(source_directory_path.as_ref())
919 .map_err(MoveDirectoryPreparationError::SourceDirectoryValidationError)?;
920
921 let validated_destination_directory = validate_destination_directory_path(
922 target_directory_path.as_ref(),
923 options.destination_directory_rule,
924 )
925 .map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
926
927 validate_source_destination_directory_pair(
928 &validated_source_directory.directory_path,
929 &validated_destination_directory.directory_path,
930 )
931 .map_err(MoveDirectoryPreparationError::DestinationDirectoryValidationError)?;
932
933
934 let source_details =
935 collect_source_directory_details(&validated_source_directory.directory_path)?;
936
937
938 // We'll first attempt to move the directory by renaming it.
939 // If we don't succeed (e.g. source and target paths are on different drives),
940 // we'll copy and delete instead.
941
942
943 if options.allowed_strategies.allowed_to_rename() {
944 match attempt_directory_move_by_rename(
945 &validated_source_directory,
946 &source_details,
947 &validated_destination_directory,
948 )? {
949 DirectoryMoveByRenameAction::Renamed { finished_move } => {
950 let final_progress_report = DirectoryMoveProgress {
951 bytes_total: source_details.total_bytes,
952 bytes_finished: source_details.total_bytes,
953 files_moved: source_details.total_files,
954 directories_created: source_details.total_directories,
955 // Clarification: this is in the past tense, but in reality `attempt_directory_move_by_rename`
956 // has already removed the empty source directory if needed.
957 // Point is, all operations have finished at this point.
958 current_operation: DirectoryMoveOperation::RemovingSourceDirectory,
959 current_operation_index: 1,
960 total_operations: 2,
961 };
962
963 progress_handler(&final_progress_report);
964
965
966 return Ok(finished_move);
967 }
968 DirectoryMoveByRenameAction::FailedOrImpossible => {}
969 };
970 }
971
972 let Some(copy_and_delete_options) = options
973 .allowed_strategies
974 .into_options_if_allowed_to_copy_and_delete()
975 else {
976 // This branch can execute only when a rename was attempted and failed,
977 // and the user disabled the copy-and-delete fallback strategy.
978 return Err(MoveDirectoryError::ExecutionError(
979 MoveDirectoryExecutionError::RenameFailedAndNoFallbackStrategy,
980 ));
981 };
982
983
984 // At this point a simple rename was either impossible or failed.
985 // We need to copy and delete instead.
986
987 let copy_options = DirectoryCopyWithProgressOptions {
988 destination_directory_rule: options.destination_directory_rule,
989 read_buffer_size: copy_and_delete_options.read_buffer_size,
990 write_buffer_size: copy_and_delete_options.write_buffer_size,
991 progress_update_byte_interval: copy_and_delete_options.progress_update_byte_interval,
992 copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
993 symlink_behaviour: copy_and_delete_options.symlink_behaviour,
994 broken_symlink_behaviour: copy_and_delete_options.broken_symlink_behaviour,
995 };
996
997 let prepared_copy = DirectoryCopyPrepared::prepare_with_validated(
998 validated_source_directory.clone(),
999 validated_destination_directory,
1000 copy_options.destination_directory_rule,
1001 copy_options.copy_depth_limit,
1002 copy_and_delete_options.symlink_behaviour,
1003 copy_and_delete_options.broken_symlink_behaviour,
1004 )
1005 .map_err(MoveDirectoryPreparationError::CopyPlanningError)?;
1006
1007
1008 let directory_copy_result = execute_prepared_copy_directory_with_progress_unchecked(
1009 prepared_copy,
1010 copy_options,
1011 |progress| {
1012 let move_operation = match progress.current_operation.clone() {
1013 DirectoryCopyOperation::CreatingDirectory {
1014 destination_directory_path: target_path,
1015 } => DirectoryMoveOperation::CreatingDirectory { target_path },
1016 DirectoryCopyOperation::CopyingFile {
1017 destination_file_path: target_path,
1018 progress,
1019 } => DirectoryMoveOperation::CopyingFile {
1020 target_path,
1021 progress,
1022 },
1023 DirectoryCopyOperation::CreatingSymbolicLink {
1024 destination_symbolic_link_file_path,
1025 } => DirectoryMoveOperation::CreatingSymbolicLink {
1026 destination_symbolic_link_file_path,
1027 },
1028 };
1029
1030
1031 let move_progress = DirectoryMoveProgress {
1032 bytes_total: progress.bytes_total,
1033 bytes_finished: progress.bytes_finished,
1034 current_operation: move_operation,
1035 current_operation_index: progress.current_operation_index,
1036 total_operations: progress.total_operations,
1037 files_moved: progress.files_copied,
1038 directories_created: progress.directories_created,
1039 };
1040
1041 progress_handler(&move_progress)
1042 },
1043 )
1044 .map_err(MoveDirectoryExecutionError::CopyDirectoryError)?;
1045
1046
1047 // Having fully copied the directory to the target, we now
1048 // remove the original (source) directory.
1049 let directory_path_to_remove =
1050 if validated_source_directory.original_path_was_symlink_to_directory {
1051 source_directory_path.as_ref()
1052 } else {
1053 validated_source_directory.directory_path.as_path()
1054 };
1055
1056 fs::remove_dir_all(directory_path_to_remove).map_err(|error| {
1057 MoveDirectoryExecutionError::UnableToAccessSource {
1058 path: validated_source_directory.directory_path,
1059 error,
1060 }
1061 })?;
1062
1063
1064 Ok(DirectoryMoveFinished {
1065 directories_moved: directory_copy_result.directories_created,
1066 total_bytes_moved: directory_copy_result.total_bytes_copied,
1067 files_moved: directory_copy_result.files_copied,
1068 symlinks_moved: directory_copy_result.symlinks_created,
1069 strategy_used: DirectoryMoveStrategy::CopyAndDelete,
1070 })
1071}