fs_more/directory/
copy.rs

1use std::path::{Path, PathBuf};
2
3use_enabled_fs_module!();
4
5use super::{
6    common::DestinationDirectoryRule,
7    prepared::{try_exists_without_follow, DirectoryCopyPrepared, QueuedOperation},
8};
9use crate::{
10    error::{CopyDirectoryError, CopyDirectoryExecutionError},
11    file::{
12        copy_file,
13        copy_file_with_progress,
14        CollidingFileBehaviour,
15        FileCopyOptions,
16        FileCopyWithProgressOptions,
17        FileProgress,
18    },
19    DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
20    DEFAULT_READ_BUFFER_SIZE,
21    DEFAULT_WRITE_BUFFER_SIZE,
22};
23
24
25/// The maximum depth of a directory copy operation.
26#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27pub enum DirectoryCopyDepthLimit {
28    /// No depth limit - the entire directory tree will be copied.
29    Unlimited,
30
31    /// Copy depth is limited to `maximum_depth`, where the value refers to
32    /// the maximum depth of the subdirectory whose contents are still copied.
33    ///
34    ///
35    /// # Examples
36    /// `maximum_depth = 0` indicates a copy operation that will cover only the files and directories
37    /// directly in the source directory.
38    ///
39    /// ```md
40    /// ~/source-directory
41    ///  |- foo.csv
42    ///  |- foo-2.csv
43    ///  |- bar/
44    ///     (no entries)
45    /// ```
46    ///
47    /// Note that the `~/source-directory/bar` directory will still be created,
48    /// but the corresponding files inside it in the source directory won't be copied.
49    ///
50    ///
51    /// <br>
52    ///
53    /// `maximum_depth = 1` will cover the files and directories directly in the source directory
54    /// plus one level of files and subdirectories deeper.
55    ///
56    /// ```md
57    /// ~/source-directory
58    ///  |- foo.csv
59    ///  |- foo-2.csv
60    ///  |- bar/
61    ///     |- hello-world.txt
62    ///     |- bar2/
63    ///        (no entries)
64    /// ```
65    ///
66    /// Notice how direct contents of `~/source-directory` and `~/source-directory/bar` are copied,
67    /// but `~/source-directory/bar/bar2` is created, but stays empty on the destination.
68    Limited {
69        /// Maximum copy depth.
70        maximum_depth: usize,
71    },
72}
73
74
75
76/// How to behave when encountering symbolic links during directory copies or moves.
77#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78pub enum SymlinkBehaviour {
79    /// Indicates the symbolic link should be preserved on the destination.
80    ///
81    /// It is possible that a symbolic link cannot be created on the destination,
82    /// for example in certain cases when source and destination are on different
83    /// mount points, in which case an error will be returned.
84    ///
85    /// In this mode, broken symbolic links will be handled with the
86    /// active [`BrokenSymlinkBehaviour`] option used alongside it.
87    Keep,
88
89    /// Indicates the symbolic link should be resolved and its destination content
90    /// should be copied or moved to the destination instead of preserving the symbolic link.
91    ///
92    /// In this mode, broken symbolic links will always cause errors,
93    /// regardless of the active [`BrokenSymlinkBehaviour`].
94    Follow,
95}
96
97
98
99/// How to behave when encountering broken symbolic links during directory copies or moves.
100///
101/// This option is generally available alongside [`SymlinkBehaviour`].
102/// Note that [`BrokenSymlinkBehaviour`] options are only used when
103/// [`SymlinkBehaviour::Keep`] is used.
104#[derive(Clone, Copy, PartialEq, Eq, Debug)]
105pub enum BrokenSymlinkBehaviour {
106    /// Indicates that the broken symbolic link should be kept as-is on the destination, i.e. broken.
107    ///
108    /// Just like for normal symbolic links,
109    /// it is possible that a symbolic link cannot be created on the destination —
110    /// for example in certain cases when source and destination are on different
111    /// mount points — in which case an error will be returned.
112    Keep,
113
114    /// Indicates a broken symbolic link should result in an error while preparing the copy or move.
115    ///
116    /// Note that unless symbolic link following is enabled alongside this option,
117    /// [`BrokenSymlinkBehaviour::Abort`] will have no effect.
118    Abort,
119}
120
121
122
123/// Options that influence the [`copy_directory`] function.
124#[derive(Clone, Copy, PartialEq, Eq, Debug)]
125pub struct DirectoryCopyOptions {
126    /// Specifies whether you allow the destination directory to exist before copying
127    /// and whether it must be empty or not.
128    /// If you allow a non-empty destination directory, you may also specify
129    /// how to behave for existing destination files and sub-directories.
130    ///
131    /// See [`DestinationDirectoryRule`] for more details and examples.
132    pub destination_directory_rule: DestinationDirectoryRule,
133
134    /// Maximum depth of the source directory to copy over to the destination.
135    pub copy_depth_limit: DirectoryCopyDepthLimit,
136
137    /// Sets the behaviour for symbolic links when copying a directory.
138    pub symlink_behaviour: SymlinkBehaviour,
139
140    /// Sets the behaviour for broken symbolic links when copying a directory.
141    pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
142}
143
144impl Default for DirectoryCopyOptions {
145    /// Constructs defaults for copying a directory, which are:
146    /// - [`DestinationDirectoryRule::AllowEmpty`]: if the destination directory already exists, it must be empty,
147    /// - [`DirectoryCopyDepthLimit::Unlimited`]: there is no copy depth limit,
148    /// - [`SymlinkBehaviour::Keep`]: symbolic links are not followed, and
149    /// - [`BrokenSymlinkBehaviour::Keep`]: broken symbolic links are kept as-is, i.e. broken.
150    fn default() -> Self {
151        Self {
152            destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
153            copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
154            symlink_behaviour: SymlinkBehaviour::Keep,
155            broken_symlink_behaviour: BrokenSymlinkBehaviour::Keep,
156        }
157    }
158}
159
160
161/// Describes a successful directory copy operation.
162#[derive(Clone, Copy, PartialEq, Eq, Debug)]
163pub struct DirectoryCopyFinished {
164    /// Total number of bytes copied.
165    pub total_bytes_copied: u64,
166
167    /// Total number of files copied.
168    pub files_copied: usize,
169
170    /// Total number of symlinks (re)created.
171    ///
172    /// If the [`DirectoryCopyOptions::symlink_behaviour`] option is set to
173    /// [`SymlinkBehaviour::Follow`], this will always be `0`.
174    pub symlinks_created: usize,
175
176    /// Total number of directories created.
177    pub directories_created: usize,
178}
179
180
181
182/// Perform a copy using prepared data from [`DirectoryCopyPrepared`].
183///
184/// For more details, see [`copy_directory`].
185pub(crate) fn copy_directory_unchecked(
186    prepared_directory_copy: DirectoryCopyPrepared,
187    options: DirectoryCopyOptions,
188) -> Result<DirectoryCopyFinished, CopyDirectoryExecutionError> {
189    let can_overwrite_files = options
190        .destination_directory_rule
191        .allows_overwriting_existing_destination_files();
192
193    let can_ignore_existing_sub_directories = options
194        .destination_directory_rule
195        .allows_existing_destination_subdirectories();
196
197
198    // We have the entire queue of operations, and we've made sure there are
199    // no collisions we should worry about. What's left is performing the file copy
200    // and directory creation operations *precisely in the order they have been prepared*.
201    // If we ignore the order, we could get into situations where
202    // some destination directory doesn't exist yet, but we would try to copy a file into it.
203
204    let mut total_bytes_copied = 0;
205
206    let mut num_files_copied = 0;
207    let mut num_symlinks_recreated = 0;
208    let mut num_directories_created = 0;
209
210
211    // Execute all queued operations. This means copying files, creating symbolic links,
212    // and creating directories in the order the queue specifies.
213    for operation in prepared_directory_copy.operation_queue {
214        match operation {
215            QueuedOperation::CopyFile {
216                source_file_path,
217                source_size_bytes,
218                destination_file_path,
219            } => {
220                let destination_file_exists = try_exists_without_follow(&destination_file_path)
221                    .map_err(|error| CopyDirectoryExecutionError::UnableToAccessDestination {
222                        path: destination_file_path.clone(),
223                        error,
224                    })?;
225
226                if destination_file_exists {
227                    let destination_file_metadata = fs::symlink_metadata(&destination_file_path)
228                        .map_err(|error| {
229                            CopyDirectoryExecutionError::UnableToAccessDestination {
230                                path: destination_file_path.clone(),
231                                error,
232                            }
233                        })?;
234
235
236                    if !destination_file_metadata.is_file() {
237                        return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
238                            path: destination_file_path.clone(),
239                        });
240                    }
241
242                    if !can_overwrite_files {
243                        return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
244                            path: destination_file_path.clone(),
245                        });
246                    }
247                }
248
249
250                copy_file(
251                    source_file_path,
252                    &destination_file_path,
253                    FileCopyOptions {
254                        colliding_file_behaviour: match can_overwrite_files {
255                            true => CollidingFileBehaviour::Overwrite,
256                            false => CollidingFileBehaviour::Abort,
257                        },
258                    },
259                )
260                .map_err(|file_error| {
261                    CopyDirectoryExecutionError::FileCopyError {
262                        file_path: destination_file_path,
263                        error: file_error,
264                    }
265                })?;
266
267
268                num_files_copied += 1;
269                total_bytes_copied += source_size_bytes;
270            }
271
272            QueuedOperation::CreateDirectory {
273                source_size_bytes,
274                destination_directory_path,
275                create_parent_directories,
276            } => {
277                let destination_directory_exists =
278                    try_exists_without_follow(&destination_directory_path).map_err(|error| {
279                        CopyDirectoryExecutionError::UnableToAccessDestination {
280                            path: destination_directory_path.clone(),
281                            error,
282                        }
283                    })?;
284
285
286                if destination_directory_exists {
287                    if !destination_directory_path.is_dir() {
288                        return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
289                            path: destination_directory_path.clone(),
290                        });
291                    }
292
293                    if !can_ignore_existing_sub_directories {
294                        return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
295                            path: destination_directory_path.clone(),
296                        });
297                    }
298
299                    continue;
300                }
301
302
303                if create_parent_directories {
304                    fs::create_dir_all(&destination_directory_path).map_err(|error| {
305                        CopyDirectoryExecutionError::UnableToCreateDirectory {
306                            directory_path: destination_directory_path,
307                            error,
308                        }
309                    })?;
310                } else {
311                    fs::create_dir(&destination_directory_path).map_err(|error| {
312                        CopyDirectoryExecutionError::UnableToCreateDirectory {
313                            directory_path: destination_directory_path,
314                            error,
315                        }
316                    })?;
317                }
318
319
320                num_directories_created += 1;
321                total_bytes_copied += source_size_bytes;
322            }
323
324            #[cfg(windows)]
325            QueuedOperation::CreateSymlink {
326                symlink_path,
327                symlink_destination_type: symlink_type,
328                source_symlink_size_bytes,
329                symlink_destination_path,
330            } => {
331                use crate::directory::prepared::SymlinkType;
332
333                match symlink_type {
334                    SymlinkType::File => {
335                        std::os::windows::fs::symlink_file(
336                            &symlink_destination_path,
337                            &symlink_path,
338                        )
339                        .map_err(|error| {
340                            CopyDirectoryExecutionError::SymlinkCreationError {
341                                symlink_path: symlink_path.clone(),
342                                error,
343                            }
344                        })?;
345                    }
346                    SymlinkType::Directory => {
347                        std::os::windows::fs::symlink_dir(&symlink_destination_path, &symlink_path)
348                            .map_err(|error| CopyDirectoryExecutionError::SymlinkCreationError {
349                                symlink_path: symlink_path.clone(),
350                                error,
351                            })?;
352                    }
353                }
354
355
356                num_symlinks_recreated += 1;
357                total_bytes_copied += source_symlink_size_bytes;
358            }
359
360            #[cfg(unix)]
361            QueuedOperation::CreateSymlink {
362                symlink_path,
363                source_symlink_size_bytes,
364                symlink_destination_path,
365            } => {
366                std::os::unix::fs::symlink(&symlink_destination_path, &symlink_path).map_err(
367                    |error| CopyDirectoryExecutionError::SymlinkCreationError {
368                        symlink_path: symlink_path.clone(),
369                        error,
370                    },
371                )?;
372
373
374                num_symlinks_recreated += 1;
375                total_bytes_copied += source_symlink_size_bytes;
376            }
377        };
378    }
379
380
381    Ok(DirectoryCopyFinished {
382        total_bytes_copied,
383        files_copied: num_files_copied,
384        symlinks_created: num_symlinks_recreated,
385        directories_created: num_directories_created,
386    })
387}
388
389
390/// Copies a directory from the source to the destination directory.
391///
392/// Contents of the source directory will be copied into the destination directory.
393/// If needed, the destination directory will be created before copying begins.
394///
395///
396/// # Symbolic links
397/// Symbolic links inside the source directory are handled according to the [`symlink_behaviour`] option.
398///
399/// If a symbolic link is relative and symbolic link behaviour is set to [`SymlinkBehaviour::Keep`],
400/// the relative path of the symlink will be preserved, even if this results in the symbolic link
401/// now being broken on the destination side.
402///
403/// Additionally, if the provided `source_directory_path` is itself a symlink to a directory,
404/// and the symbolic link behaviour is set to [`SymlinkBehaviour::Keep`], the link will be preserved
405/// on the destination, meaning `destination_directory_path` will be a symbolic link as well.
406///
407/// This matches the behaviour of `cp` with `--recursive` (and optionally `--dereference`)
408/// flags on Unix[^unix-cp-rd].
409///
410///
411/// # Options
412/// See [`DirectoryCopyOptions`] for the full set of available directory copying options.
413///
414/// ### `destination_directory_rule` considerations
415/// If you allow the destination directory to exist and be non-empty,
416/// source directory contents will be merged (!) into the destination directory.
417/// This is *not* the default, and you should probably consider the consequences
418/// very carefully before setting the corresponding [`options.destination_directory_rule`]
419/// option to anything other than [`DisallowExisting`] or [`AllowEmpty`].
420///
421///
422/// # Return value
423/// Upon success, the function returns information about the files and directories that were copied or created
424/// as well as the total number of bytes copied; see [`DirectoryCopyFinished`].
425///
426///
427/// # Errors
428/// If the directory cannot be copied to the destination, a [`CopyDirectoryError`] is returned;
429/// see its documentation for more details.
430///
431/// Errors for this function are quite granular, and are split into two main groups:
432/// - Preparation errors ([`CopyDirectoryError::PreparationError`]) are emitted during
433///   the preparation phase of copying. Importantly, if an error from this group is returned,
434///   the destination directory *hasn't been changed yet* in any way.
435/// - Copy execution errors ([`CopyDirectoryError::ExecutionError`]) are emitted during
436///   the actual copying phase. If an error from this group is returned,
437///   it is very likely that the destination directory is in an unpredictable state, since
438///   the error occurred while trying to copy a file or create a directory.
439///
440///
441/// [`options.destination_directory_rule`]: DirectoryCopyOptions::destination_directory_rule
442/// [`options.copy_depth_limit`]: DirectoryCopyOptions::copy_depth_limit
443/// [`symlink_behaviour`]: DirectoryCopyOptions::symlink_behaviour
444/// [`DisallowExisting`]: DestinationDirectoryRule::DisallowExisting
445/// [`AllowEmpty`]: DestinationDirectoryRule::AllowEmpty
446/// [`AllowNonEmpty`]: DestinationDirectoryRule::AllowNonEmpty
447/// [`copy_file`]: crate::file::copy_file
448/// [^unix-cp-rd]: Source for coreutils' `cp` is available
449///     [here](https://github.com/coreutils/coreutils/blob/ccf47cad93bc0b85da0401b0a9d4b652e4c930e4/src/cp.c).
450pub fn copy_directory<S, T>(
451    source_directory_path: S,
452    destination_directory_path: T,
453    options: DirectoryCopyOptions,
454) -> Result<DirectoryCopyFinished, CopyDirectoryError>
455where
456    S: AsRef<Path>,
457    T: AsRef<Path>,
458{
459    let prepared_copy = DirectoryCopyPrepared::prepare(
460        source_directory_path.as_ref(),
461        destination_directory_path.as_ref(),
462        options.destination_directory_rule,
463        options.copy_depth_limit,
464        options.symlink_behaviour,
465        options.broken_symlink_behaviour,
466    )?;
467
468    let finished_copy = copy_directory_unchecked(prepared_copy, options)?;
469
470
471    Ok(finished_copy)
472}
473
474
475/// Describes a directory copy operation.
476///
477/// Used for progress reporting in [`copy_directory_with_progress`].
478#[derive(Clone, PartialEq, Eq, Debug)]
479pub enum DirectoryCopyOperation {
480    /// A directory is being created.
481    CreatingDirectory {
482        /// Path to the directory that is being created.
483        destination_directory_path: PathBuf,
484    },
485
486    /// A file is being copied.
487    ///
488    /// For more precise copying progress, see the `progress` field.
489    CopyingFile {
490        /// Path to the file that is being created.
491        destination_file_path: PathBuf,
492
493        /// Progress of the file copy operation.
494        progress: FileProgress,
495    },
496
497    /// A symbolic link is being created.
498    CreatingSymbolicLink {
499        /// Path to the symlink being created.
500        destination_symbolic_link_file_path: PathBuf,
501    },
502}
503
504
505/// Directory copying progress.
506///
507/// This struct is used to report progress to a user-provided closure
508/// (see usage in [`copy_directory_with_progress`]).
509///
510/// Note that the data inside this struct isn't fully owned - the `current_operation`
511/// field is borrowed, and cloning will not have the desired effect.
512/// To obtain a fully-owned clone of this state, call
513/// [`DirectoryCopyProgressRef::to_owned_progress`].
514#[derive(Clone, PartialEq, Eq, Debug)]
515pub struct DirectoryCopyProgressRef<'o> {
516    /// Total number of bytes that need to be copied
517    /// for the directory copy to be complete.
518    pub bytes_total: u64,
519
520    /// Number of bytes that have been copied so far.
521    pub bytes_finished: u64,
522
523    /// Number of files that have been copied so far.
524    pub files_copied: usize,
525
526    /// Number of symlinks that have been (re)created so far.
527    ///
528    /// If the [`DirectoryCopyOptions::symlink_behaviour`] option is set to
529    /// [`SymlinkBehaviour::Follow`], this will always be `0`.
530    pub symlinks_created: usize,
531
532    /// Number of directories that have been created so far.
533    pub directories_created: usize,
534
535    /// The current operation being performed.
536    pub current_operation: &'o DirectoryCopyOperation,
537
538    /// The index of the current operation.
539    ///
540    /// Starts at `0`, goes up to (including) `total_operations - 1`.
541    pub current_operation_index: usize,
542
543    /// The total number of operations that need to be performed to
544    /// copy the requested directory.
545    ///
546    /// A single operation is either copying a file or creating a directory,
547    /// see [`DirectoryCopyOperation`].
548    pub total_operations: usize,
549}
550
551impl DirectoryCopyProgressRef<'_> {
552    /// Clones the required data from this progress struct
553    /// into an [`DirectoryCopyProgress`] - this way you own
554    /// the entire state.
555    pub fn to_owned_progress(&self) -> DirectoryCopyProgress {
556        DirectoryCopyProgress {
557            bytes_total: self.bytes_total,
558            bytes_finished: self.bytes_finished,
559            files_copied: self.files_copied,
560            symlinks_created: self.symlinks_created,
561            directories_created: self.directories_created,
562            current_operation: self.current_operation.to_owned(),
563            current_operation_index: self.current_operation_index,
564            total_operations: self.total_operations,
565        }
566    }
567}
568
569
570/// Directory copying progress.
571///
572/// This is a fully-owned version of [`DirectoryCopyProgress`],
573/// where the `current_operation` field is borrowed.
574///
575/// Obtainable from a reference to [`DirectoryCopyProgressRef`]
576/// by calling [`DirectoryCopyProgressRef::to_owned_progress`].
577#[derive(Clone, PartialEq, Eq, Debug)]
578pub struct DirectoryCopyProgress {
579    /// Total number of bytes that need to be copied
580    /// for the directory copy to be complete.
581    pub bytes_total: u64,
582
583    /// Number of bytes that have been copied so far.
584    pub bytes_finished: u64,
585
586    /// Number of files that have been copied so far.
587    pub files_copied: usize,
588
589    /// Number of symlinks that have been (re)created so far.
590    ///
591    /// If the [`DirectoryCopyOptions::symlink_behaviour`] option is set to
592    /// [`SymlinkBehaviour::Follow`], this will always be `0`.
593    pub symlinks_created: usize,
594
595    /// Number of directories that have been created so far.
596    pub directories_created: usize,
597
598    /// The current operation being performed.
599    pub current_operation: DirectoryCopyOperation,
600
601    /// The index of the current operation.
602    ///
603    /// Starts at `0`, goes up to (including) `total_operations - 1`.
604    pub current_operation_index: usize,
605
606    /// The total number of operations that need to be performed to
607    /// copy the requested directory.
608    ///
609    /// A single operation is either copying a file or creating a directory,
610    /// see [`DirectoryCopyOperation`].
611    pub total_operations: usize,
612}
613
614
615
616#[derive(Clone, PartialEq, Eq, Debug)]
617struct DirectoryCopyInternalProgress {
618    /// Total number of bytes that need to be copied
619    /// for the directory copy to be complete.
620    bytes_total: u64,
621
622    /// Number of bytes that have been copied so far.
623    bytes_finished: u64,
624
625    /// Number of files that have been copied so far.
626    files_copied: usize,
627
628    /// Number of symlinks that have been (re)created so far.
629    ///
630    /// If the [`DirectoryCopyOptions::symlink_behaviour`] option is set to
631    /// [`SymlinkBehaviour::Follow`], this will always be `0`.
632    symlinks_created: usize,
633
634    /// Number of directories that have been created so far.
635    directories_created: usize,
636
637    /// The current operation being performed.
638    current_operation: Option<DirectoryCopyOperation>,
639
640    /// The index of the current operation.
641    ///
642    /// Starts at `0`, goes up to (including) `total_operations - 1`.
643    current_operation_index: Option<usize>,
644
645    /// The total number of operations that need to be performed to
646    /// copy the requested directory.
647    ///
648    /// A single operation is either copying a file or creating a directory,
649    /// see [`DirectoryCopyOperation`].
650    total_operations: usize,
651}
652
653impl DirectoryCopyInternalProgress {
654    /// Modifies `self` with the provided `FnMut` closure.
655    /// Then, the provided progress handler closure is called.
656    fn update_operation_and_emit_progress<M, F>(
657        &mut self,
658        mut self_modifier_closure: M,
659        progress_handler: &mut F,
660    ) where
661        M: FnMut(&mut Self),
662        F: FnMut(&DirectoryCopyProgressRef),
663    {
664        self_modifier_closure(self);
665        progress_handler(&self.to_user_facing_progress());
666    }
667
668    /// Replaces the current [`current_operation`][Self::current_operation]
669    /// with the next one.
670    ///
671    /// The [`current_operation_index`][Self::current_operation_index]
672    /// is incremented or set to 0, if previously unset.
673    ///
674    /// Finally, the provided progress handler closure is called.
675    fn set_next_operation_and_emit_progress<F>(
676        &mut self,
677        operation: DirectoryCopyOperation,
678        progress_handler: &mut F,
679    ) where
680        F: FnMut(&DirectoryCopyProgressRef),
681    {
682        if let Some(existing_operation_index) = self.current_operation_index.as_mut() {
683            *existing_operation_index += 1;
684        } else {
685            self.current_operation_index = Some(0);
686        }
687
688        self.current_operation = Some(operation);
689
690        progress_handler(&self.to_user_facing_progress())
691    }
692
693    /// Converts the [`DirectoryCopyInternalProgress`] to a [`DirectoryCopyProgress`],
694    /// copying only the small fields, and passing the `current_operation` as a reference.
695    ///
696    /// # Panics
697    /// Panics if the `current_operation` or `current_operation_index` field is `None`.
698    fn to_user_facing_progress(&self) -> DirectoryCopyProgressRef<'_> {
699        let current_operation_reference = self
700            .current_operation
701            .as_ref()
702            // PANIC SATEFY: The caller is responsible.
703            .expect("current_operation field to be Some");
704
705        let current_operation_index = self
706            .current_operation_index
707            // PANIC SATEFY: The caller is responsible.
708            .expect("current_operation_index to be Some");
709
710        DirectoryCopyProgressRef {
711            bytes_total: self.bytes_total,
712            bytes_finished: self.bytes_finished,
713            files_copied: self.files_copied,
714            symlinks_created: self.symlinks_created,
715            directories_created: self.directories_created,
716            current_operation: current_operation_reference,
717            current_operation_index,
718            total_operations: self.total_operations,
719        }
720    }
721}
722
723
724
725
726/// Options that influence the [`copy_directory_with_progress`] function.
727#[derive(Clone, Copy, PartialEq, Eq, Debug)]
728pub struct DirectoryCopyWithProgressOptions {
729    /// Specifies whether you allow the destination directory to exist before copying,
730    /// and whether you require it to be empty. If you allow a non-empty destination directory,
731    /// you may also specify how to handle existing destination files and sub-directories.
732    ///
733    /// See [`DestinationDirectoryRule`] documentation for more details and examples.
734    pub destination_directory_rule: DestinationDirectoryRule,
735
736    /// Maximum depth of the source directory to copy.
737    pub copy_depth_limit: DirectoryCopyDepthLimit,
738
739    /// Sets the behaviour for symbolic links when copying a directory.
740    pub symlink_behaviour: SymlinkBehaviour,
741
742    /// Sets the behaviour for broken symbolic links when copying a directory.
743    pub broken_symlink_behaviour: BrokenSymlinkBehaviour,
744
745    /// Internal buffer size used for reading from source files.
746    ///
747    /// Defaults to 64 KiB.
748    pub read_buffer_size: usize,
749
750    /// Internal buffer size used for writing to destination files.
751    ///
752    /// Defaults to 64 KiB.
753    pub write_buffer_size: usize,
754
755    /// *Minimum* number of bytes written between two consecutive progress reports.
756    ///
757    /// Defaults to 512 KiB.
758    ///
759    /// *Note that the real reporting interval can be larger*
760    /// (see [`copy_directory_with_progress`] for more info).
761    ///
762    ///
763    /// [`copy_directory_with_progress`]: copy_directory_with_progress#progress-reporting
764    pub progress_update_byte_interval: u64,
765}
766
767impl Default for DirectoryCopyWithProgressOptions {
768    /// Constructs defaults for copying a directory, which are:
769    /// - [`DestinationDirectoryRule::AllowEmpty`]: if the destination directory already exists, it must be empty,
770    /// - [`DirectoryCopyDepthLimit::Unlimited`]: there is no copy depth limit,
771    /// - [`SymlinkBehaviour::Keep`]: symbolic links are not followed,
772    /// - [`BrokenSymlinkBehaviour::Keep`]: broken symbolic links are kept as-is, i.e. broken,
773    /// - the read and write buffers are 64 KiB large, and
774    /// - the progress reporting closure byte interval is set to 512 KiB.
775    fn default() -> Self {
776        Self {
777            destination_directory_rule: DestinationDirectoryRule::AllowEmpty,
778            copy_depth_limit: DirectoryCopyDepthLimit::Unlimited,
779            symlink_behaviour: SymlinkBehaviour::Keep,
780            broken_symlink_behaviour: BrokenSymlinkBehaviour::Abort,
781            read_buffer_size: DEFAULT_READ_BUFFER_SIZE,
782            write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
783            progress_update_byte_interval: DEFAULT_PROGRESS_UPDATE_BYTE_INTERVAL,
784        }
785    }
786}
787
788
789
790/// Given inner data of [`QueuedOperation::CopyFile`], this function
791/// copies the given file, with progress information.
792///
793/// The function respects given `options`.
794fn execute_copy_file_operation_with_progress<F>(
795    source_file_path: PathBuf,
796    source_size_bytes: u64,
797    destination_path: PathBuf,
798    options: &DirectoryCopyWithProgressOptions,
799    progress: &mut DirectoryCopyInternalProgress,
800    progress_handler: &mut F,
801) -> Result<(), CopyDirectoryExecutionError>
802where
803    F: FnMut(&DirectoryCopyProgressRef),
804{
805    let can_overwrite_destination_file = options
806        .destination_directory_rule
807        .allows_overwriting_existing_destination_files();
808
809
810
811    let destination_path_exists =
812        try_exists_without_follow(&destination_path).map_err(|error| {
813            CopyDirectoryExecutionError::UnableToAccessDestination {
814                path: destination_path.clone(),
815                error,
816            }
817        })?;
818
819    if destination_path_exists {
820        let destination_path_metadata =
821            fs::symlink_metadata(&destination_path).map_err(|error| {
822                CopyDirectoryExecutionError::UnableToAccessDestination {
823                    path: destination_path.clone(),
824                    error,
825                }
826            })?;
827
828
829        if !destination_path_metadata.is_file() {
830            return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
831                path: destination_path.clone(),
832            });
833        }
834
835        if !can_overwrite_destination_file {
836            return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
837                path: destination_path.clone(),
838            });
839        }
840    }
841
842
843    progress.set_next_operation_and_emit_progress(
844        DirectoryCopyOperation::CopyingFile {
845            destination_file_path: destination_path.clone(),
846            progress: FileProgress {
847                bytes_finished: 0,
848                bytes_total: source_size_bytes,
849            },
850        },
851        progress_handler,
852    );
853
854
855    // Set to `true` when we update our `bytes_total` to the
856    // freshly calculated total number of bytes in a file (after the copying starts).
857    let mut updated_bytes_total_with_fresh_value = false;
858
859    // Number of `bytes_copied` last emitted through the progress closure.
860    let bytes_copied_before = progress.bytes_finished;
861
862
863    copy_file_with_progress(
864        source_file_path,
865        &destination_path,
866        FileCopyWithProgressOptions {
867            colliding_file_behaviour: match options.destination_directory_rule {
868                DestinationDirectoryRule::DisallowExisting => CollidingFileBehaviour::Abort,
869                DestinationDirectoryRule::AllowEmpty => CollidingFileBehaviour::Abort,
870                DestinationDirectoryRule::AllowNonEmpty { colliding_file_behaviour, .. } => colliding_file_behaviour,
871            },
872            read_buffer_size: options.read_buffer_size,
873            write_buffer_size: options.write_buffer_size,
874            progress_update_byte_interval: options.progress_update_byte_interval,
875        },
876        |new_file_progress| progress.update_operation_and_emit_progress(
877                |progress| {
878                    let current_operation = progress.current_operation.as_mut()
879                        // PANIC SATEFY: The function calls `set_next_operation_and_emit_progress` above,
880                        // meaning the `current_operation` can never be None.
881                        .expect("the current_operation field to be Some");
882
883
884                    if let DirectoryCopyOperation::CopyingFile {
885                        progress: file_progress,
886                        ..
887                    } = current_operation
888                    {
889                        // It is somewhat possible that a file is written to between the scanning phase 
890                        // and copying. In that case, it is *possible* that the file size changes, 
891                        // which means we should listen to the size `copy_file_with_progress` 
892                        // is reporting. There is no point to doing this each update, so we do it only once.
893                        if !updated_bytes_total_with_fresh_value {
894                            file_progress.bytes_total = new_file_progress.bytes_total;
895                            updated_bytes_total_with_fresh_value = true;
896                        }
897
898                        file_progress.bytes_finished = new_file_progress.bytes_finished;
899                        progress.bytes_finished =
900                            bytes_copied_before + file_progress.bytes_finished;
901                    } else {
902                        // PANIC SAFETY: Since we set `progress` to a `CopyingFile` 
903                        // at the beginning of the function, and there is no possibility 
904                        // of changing that operation in between, this panic should never happen.
905                        panic!(
906                            "BUG: `progress.current_operation` doesn't match DirectoryCopyOperation::CopyingFile"
907                        );
908                    }
909                },
910                progress_handler,
911            )
912    )
913        .map_err(|file_error| CopyDirectoryExecutionError::FileCopyError { file_path: destination_path, error: file_error })?;
914
915
916    progress.files_copied += 1;
917
918    Ok(())
919}
920
921
922/// Given inner data of [`QueuedOperation::CreateDirectory`], this function
923/// creates the given directory with progress information.
924///
925/// If the directory already exists, no action
926/// is taken, unless the given options indicate that to be an error
927/// (`overwrite_existing_subdirectories`, see [`DestinationDirectoryRule`]).
928///
929/// If the given path exists, but is not a directory, an error is returned as well.
930fn execute_create_directory_operation_with_progress<F>(
931    destination_directory_path: PathBuf,
932    source_size_bytes: u64,
933    create_parent_directories: bool,
934    options: &DirectoryCopyWithProgressOptions,
935    progress: &mut DirectoryCopyInternalProgress,
936    progress_handler: &mut F,
937) -> Result<(), CopyDirectoryExecutionError>
938where
939    F: FnMut(&DirectoryCopyProgressRef),
940{
941    let destination_directory_exists = try_exists_without_follow(&destination_directory_path)
942        .map_err(|error| CopyDirectoryExecutionError::UnableToAccessDestination {
943            path: destination_directory_path.clone(),
944            error,
945        })?;
946
947    if destination_directory_exists {
948        let destination_directory_metadata = fs::symlink_metadata(&destination_directory_path)
949            .map_err(|error| CopyDirectoryExecutionError::UnableToAccessDestination {
950                path: destination_directory_path.clone(),
951                error,
952            })?;
953
954        if !destination_directory_metadata.is_dir() {
955            return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
956                path: destination_directory_path,
957            });
958        }
959
960        if options.destination_directory_rule == DestinationDirectoryRule::DisallowExisting {
961            return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
962                path: destination_directory_path,
963            });
964        }
965
966        // If the destination directory rule does not forbid an existing sub-directory,
967        // we have no directory to create, since it already exists.
968        return Ok(());
969    }
970
971
972    progress.set_next_operation_and_emit_progress(
973        DirectoryCopyOperation::CreatingDirectory {
974            destination_directory_path: destination_directory_path.clone(),
975        },
976        progress_handler,
977    );
978
979
980    if create_parent_directories {
981        fs::create_dir_all(&destination_directory_path).map_err(|error| {
982            CopyDirectoryExecutionError::UnableToCreateDirectory {
983                directory_path: destination_directory_path,
984                error,
985            }
986        })?;
987    } else {
988        fs::create_dir(&destination_directory_path).map_err(|error| {
989            CopyDirectoryExecutionError::UnableToCreateDirectory {
990                directory_path: destination_directory_path,
991                error,
992            }
993        })?;
994    }
995
996
997    progress.directories_created += 1;
998    progress.bytes_finished += source_size_bytes;
999
1000    Ok(())
1001}
1002
1003
1004
1005struct SymlinkCreationInfo {
1006    symlink_path: PathBuf,
1007
1008    symlink_destination_path: PathBuf,
1009
1010    #[cfg(windows)]
1011    symlink_type: crate::directory::prepared::SymlinkType,
1012
1013    unfollowed_symlink_file_size_bytes: u64,
1014}
1015
1016
1017fn execute_create_symlink_operation_with_progress<F>(
1018    symlink_info: SymlinkCreationInfo,
1019    options: &DirectoryCopyWithProgressOptions,
1020    progress: &mut DirectoryCopyInternalProgress,
1021    progress_handler: &mut F,
1022) -> Result<(), CopyDirectoryExecutionError>
1023where
1024    F: FnMut(&DirectoryCopyProgressRef),
1025{
1026    let can_overwrite_destination_file = options
1027        .destination_directory_rule
1028        .allows_overwriting_existing_destination_files();
1029
1030
1031    let symlink_path_exists =
1032        try_exists_without_follow(&symlink_info.symlink_path).map_err(|error| {
1033            CopyDirectoryExecutionError::UnableToAccessDestination {
1034                path: symlink_info.symlink_path.clone(),
1035                error,
1036            }
1037        })?;
1038
1039    if symlink_path_exists {
1040        let symlink_path_metadata =
1041            fs::symlink_metadata(&symlink_info.symlink_path).map_err(|error| {
1042                CopyDirectoryExecutionError::UnableToAccessDestination {
1043                    path: symlink_info.symlink_path.clone(),
1044                    error,
1045                }
1046            })?;
1047
1048        if !symlink_path_metadata.is_symlink() {
1049            return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
1050                path: symlink_info.symlink_path,
1051            });
1052        }
1053
1054        if !can_overwrite_destination_file {
1055            return Err(CopyDirectoryExecutionError::DestinationEntryUnexpected {
1056                path: symlink_info.symlink_path,
1057            });
1058        }
1059    }
1060
1061
1062    progress.set_next_operation_and_emit_progress(
1063        DirectoryCopyOperation::CreatingSymbolicLink {
1064            destination_symbolic_link_file_path: symlink_info.symlink_path.clone(),
1065        },
1066        progress_handler,
1067    );
1068
1069
1070    #[cfg(windows)]
1071    {
1072        use crate::directory::prepared::SymlinkType;
1073
1074        match symlink_info.symlink_type {
1075            SymlinkType::File => {
1076                std::os::windows::fs::symlink_file(
1077                    &symlink_info.symlink_destination_path,
1078                    &symlink_info.symlink_path,
1079                )
1080                .map_err(|error| {
1081                    CopyDirectoryExecutionError::SymlinkCreationError {
1082                        symlink_path: symlink_info.symlink_path.clone(),
1083                        error,
1084                    }
1085                })?;
1086            }
1087            SymlinkType::Directory => {
1088                std::os::windows::fs::symlink_dir(
1089                    &symlink_info.symlink_destination_path,
1090                    &symlink_info.symlink_path,
1091                )
1092                .map_err(|error| {
1093                    CopyDirectoryExecutionError::SymlinkCreationError {
1094                        symlink_path: symlink_info.symlink_path.clone(),
1095                        error,
1096                    }
1097                })?;
1098            }
1099        };
1100    }
1101
1102    #[cfg(unix)]
1103    {
1104        std::os::unix::fs::symlink(
1105            &symlink_info.symlink_destination_path,
1106            &symlink_info.symlink_path,
1107        )
1108        .map_err(|error| CopyDirectoryExecutionError::SymlinkCreationError {
1109            symlink_path: symlink_info.symlink_path.clone(),
1110            error,
1111        })?;
1112    }
1113
1114
1115    progress.symlinks_created += 1;
1116    progress.bytes_finished += symlink_info.unfollowed_symlink_file_size_bytes;
1117
1118    Ok(())
1119}
1120
1121
1122
1123
1124/// Execute a prepared copy with progress tracking.
1125///
1126/// For more details, see [`copy_directory_with_progress`].
1127pub(crate) fn execute_prepared_copy_directory_with_progress_unchecked<F>(
1128    prepared_copy: DirectoryCopyPrepared,
1129    options: DirectoryCopyWithProgressOptions,
1130    mut progress_handler: F,
1131) -> Result<DirectoryCopyFinished, CopyDirectoryExecutionError>
1132where
1133    F: FnMut(&DirectoryCopyProgressRef),
1134{
1135    let mut progress = DirectoryCopyInternalProgress {
1136        bytes_total: prepared_copy.total_bytes,
1137        bytes_finished: 0,
1138        files_copied: 0,
1139        symlinks_created: 0,
1140        directories_created: 0,
1141        // This is an invisible operation - we don't emit this progress struct at all,
1142        // but we do need something here before the next operation starts.
1143        current_operation: None,
1144        current_operation_index: None,
1145        total_operations: prepared_copy.operation_queue.len(),
1146    };
1147
1148
1149    // Execute queued directory copy operations.
1150    for operation in prepared_copy.operation_queue {
1151        match operation {
1152            QueuedOperation::CopyFile {
1153                source_file_path: source_path,
1154                source_size_bytes,
1155                destination_file_path,
1156            } => execute_copy_file_operation_with_progress(
1157                source_path,
1158                source_size_bytes,
1159                destination_file_path,
1160                &options,
1161                &mut progress,
1162                &mut progress_handler,
1163            )?,
1164
1165            QueuedOperation::CreateDirectory {
1166                source_size_bytes,
1167                destination_directory_path,
1168                create_parent_directories,
1169            } => execute_create_directory_operation_with_progress(
1170                destination_directory_path,
1171                source_size_bytes,
1172                create_parent_directories,
1173                &options,
1174                &mut progress,
1175                &mut progress_handler,
1176            )?,
1177
1178            #[cfg(windows)]
1179            QueuedOperation::CreateSymlink {
1180                symlink_path,
1181                symlink_destination_type: symlink_type,
1182                source_symlink_size_bytes,
1183                symlink_destination_path,
1184            } => execute_create_symlink_operation_with_progress(
1185                SymlinkCreationInfo {
1186                    symlink_path,
1187                    symlink_destination_path,
1188                    symlink_type,
1189                    unfollowed_symlink_file_size_bytes: source_symlink_size_bytes,
1190                },
1191                &options,
1192                &mut progress,
1193                &mut progress_handler,
1194            )?,
1195
1196            #[cfg(unix)]
1197            QueuedOperation::CreateSymlink {
1198                symlink_path,
1199                source_symlink_size_bytes,
1200                symlink_destination_path,
1201            } => execute_create_symlink_operation_with_progress(
1202                SymlinkCreationInfo {
1203                    symlink_path,
1204                    symlink_destination_path,
1205                    unfollowed_symlink_file_size_bytes: source_symlink_size_bytes,
1206                },
1207                &options,
1208                &mut progress,
1209                &mut progress_handler,
1210            )?,
1211        }
1212    }
1213
1214    // One last progress update - everything should be done at this point.
1215    progress_handler(&progress.to_user_facing_progress());
1216
1217    Ok(DirectoryCopyFinished {
1218        total_bytes_copied: progress.bytes_finished,
1219        files_copied: progress.files_copied,
1220        symlinks_created: progress.symlinks_created,
1221        directories_created: progress.directories_created,
1222    })
1223}
1224
1225
1226/// Copies a directory from the source to the destination directory, with progress reporting.
1227///
1228/// Contents of the source directory will be copied into the destination directory.
1229/// If needed, the destination directory will be created before copying begins.
1230///
1231///
1232/// # Symbolic links
1233/// Symbolic links inside the source directory are handled according to the [`symlink_behaviour`] option.
1234///
1235/// If a symbolic link is relative and symbolic link behaviour is set to [`SymlinkBehaviour::Keep`],
1236/// the relative path of the symlink will be preserved, even if this results in the symbolic link
1237/// now being broken on the destination side.
1238///
1239/// Additionally, if the provided `source_directory_path` is itself a symlink to a directory,
1240/// and the symbolic link behaviour is set to [`SymlinkBehaviour::Keep`], the link will be preserved
1241/// on the destination, meaning `destination_directory_path` will be a symbolic link as well.
1242///
1243/// This matches the behaviour of `cp` with `--recursive` (and optionally `--dereference`)
1244/// flags on Unix[^unix-cp-rd].
1245///
1246///
1247/// # Options
1248/// See [`DirectoryCopyWithProgressOptions`] for the full set of available directory copying options.
1249///
1250/// ### `destination_directory_rule` considerations
1251/// If you allow the destination directory to exist and be non-empty,
1252/// source directory contents will be merged (!) into the destination directory.
1253/// This is *not* the default, and you should probably consider the consequences
1254/// very carefully before setting the corresponding [`options.destination_directory_rule`]
1255/// option to anything other than [`DisallowExisting`] or [`AllowEmpty`].
1256///
1257///
1258/// # Return value
1259/// Upon success, the function returns information about the files and directories that were copied or created
1260/// as well as the total number of bytes copied; see [`DirectoryCopyFinished`].
1261///
1262///
1263/// ## Progress reporting
1264/// This function allows you to receive progress reports by passing
1265/// a `progress_handler` closure. It will be called with
1266/// a reference to [`DirectoryCopyProgress`] regularly.
1267///
1268/// You can control the progress reporting frequency by setting the
1269/// [`options.progress_update_byte_interval`] option to a sufficiently small or large value,
1270/// but note that smaller intervals are likely to have an additional impact on performance.
1271/// The value of this option is the minimum number of bytes written to a file between
1272/// two calls to the provided `progress_handler`.
1273///
1274/// This function does not guarantee a precise number of progress reports;
1275/// it does, however, guarantee at least one progress report per file copy, symlink and directory creation operation.
1276/// It also guarantees one final progress report, when the state indicates the copy has been completed.
1277///
1278/// For more details on reporting intervals for file copies, see progress reporting section
1279/// for [`copy_file`][crate::file::copy_file].
1280///
1281///
1282/// # Errors
1283/// If the directory cannot be copied to the destination, a [`CopyDirectoryError`] is returned;
1284/// see its documentation for more details.
1285///
1286/// Errors for this function are quite granular, and are split into two main groups:
1287/// - Preparation errors ([`CopyDirectoryError::PreparationError`]) are emitted during
1288///   the preparation phase of copying. Importantly, if an error from this group is returned,
1289///   the destination directory *hasn't been changed yet* in any way.
1290/// - Copy execution errors ([`CopyDirectoryError::ExecutionError`]) are emitted during
1291///   the actual copying phase. If an error from this group is returned,
1292///   it is very likely that the destination directory is in an unpredictable state, since
1293///   the error occurred while trying to copy a file or create a directory.
1294///
1295///
1296/// [`options.progress_update_byte_interval`]: DirectoryCopyWithProgressOptions::progress_update_byte_interval
1297/// [`options.destination_directory_rule`]: DirectoryCopyWithProgressOptions::destination_directory_rule
1298/// [`options.copy_depth_limit`]: DirectoryCopyWithProgressOptions::copy_depth_limit
1299/// [`symlink_behaviour`]: DirectoryCopyWithProgressOptions::symlink_behaviour
1300/// [`DisallowExisting`]: DestinationDirectoryRule::DisallowExisting
1301/// [`AllowEmpty`]: DestinationDirectoryRule::AllowEmpty
1302/// [`AllowNonEmpty`]: DestinationDirectoryRule::AllowNonEmpty
1303/// [`copy_file`]: crate::file::copy_file
1304/// [^unix-cp-rd]: Source for coreutils' `cp` is available
1305///     [here](https://github.com/coreutils/coreutils/blob/ccf47cad93bc0b85da0401b0a9d4b652e4c930e4/src/cp.c).
1306pub fn copy_directory_with_progress<S, T, F>(
1307    source_directory_path: S,
1308    destination_directory_path: T,
1309    options: DirectoryCopyWithProgressOptions,
1310    progress_handler: F,
1311) -> Result<DirectoryCopyFinished, CopyDirectoryError>
1312where
1313    S: AsRef<Path>,
1314    T: AsRef<Path>,
1315    F: FnMut(&DirectoryCopyProgressRef),
1316{
1317    let prepared_copy = DirectoryCopyPrepared::prepare(
1318        source_directory_path.as_ref(),
1319        destination_directory_path.as_ref(),
1320        options.destination_directory_rule,
1321        options.copy_depth_limit,
1322        options.symlink_behaviour,
1323        options.broken_symlink_behaviour,
1324    )?;
1325
1326
1327    let finished_copy = execute_prepared_copy_directory_with_progress_unchecked(
1328        prepared_copy,
1329        options,
1330        progress_handler,
1331    )?;
1332
1333    Ok(finished_copy)
1334}