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}