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}