1use std::os::unix::fs::MetadataExt;
2
3use anyhow::{Context, anyhow};
4use async_recursion::async_recursion;
5use throttle::get_file_iops_tokens;
6use tracing::instrument;
7
8use crate::config::DryRunMode;
9use crate::filecmp;
10use crate::preserve;
11use crate::progress;
12use crate::rm;
13use crate::rm::{Settings as RmSettings, Summary as RmSummary};
14use crate::walk::{self, EntryKind};
15
16pub type Error = crate::error::OperationError<Summary>;
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
25pub enum OverwriteFilter {
26 #[value(name = "newer")]
28 Newer,
29}
30
31impl std::fmt::Display for OverwriteFilter {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 OverwriteFilter::Newer => write!(f, "newer"),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
40pub struct Settings {
41 pub dereference: bool,
42 pub fail_early: bool,
43 pub overwrite: bool,
44 pub overwrite_compare: filecmp::MetadataCmpSettings,
45 pub overwrite_filter: Option<OverwriteFilter>,
46 pub ignore_existing: bool,
47 pub chunk_size: u64,
48 pub skip_specials: bool,
50 pub remote_copy_buffer_size: usize,
56 pub filter: Option<crate::filter::FilterSettings>,
58 pub dry_run: Option<crate::config::DryRunMode>,
60}
61
62fn skipped_summary_for(kind: EntryKind) -> Summary {
66 match kind {
67 EntryKind::Dir => Summary {
68 directories_skipped: 1,
69 ..Default::default()
70 },
71 EntryKind::Symlink => Summary {
72 symlinks_skipped: 1,
73 ..Default::default()
74 },
75 EntryKind::File | EntryKind::Special => Summary {
76 files_skipped: 1,
77 ..Default::default()
78 },
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum EmptyDirAction {
86 Keep,
88 Remove,
90 DryRunSkip,
92}
93
94pub fn check_empty_dir_cleanup(
109 filter: Option<&crate::filter::FilterSettings>,
110 we_created_dir: bool,
111 anything_copied: bool,
112 relative_path: &std::path::Path,
113 is_root: bool,
114 is_dry_run: bool,
115) -> EmptyDirAction {
116 if filter.is_none() || anything_copied {
118 return EmptyDirAction::Keep;
119 }
120 if !we_created_dir {
122 return EmptyDirAction::Keep;
123 }
124 if is_root {
126 return EmptyDirAction::Keep;
127 }
128 let f = filter.unwrap();
130 if f.directly_matches_include(relative_path, true) {
132 return EmptyDirAction::Keep;
133 }
134 if is_dry_run {
136 EmptyDirAction::DryRunSkip
137 } else {
138 EmptyDirAction::Remove
139 }
140}
141
142#[instrument]
143pub fn is_file_type_same(md1: &std::fs::Metadata, md2: &std::fs::Metadata) -> bool {
144 let ft1 = md1.file_type();
145 let ft2 = md2.file_type();
146 ft1.is_dir() == ft2.is_dir()
147 && ft1.is_file() == ft2.is_file()
148 && ft1.is_symlink() == ft2.is_symlink()
149}
150
151#[instrument(skip(prog_track, src_metadata, settings, preserve))]
152pub async fn copy_file(
153 prog_track: &'static progress::Progress,
154 src: &std::path::Path,
155 dst: &std::path::Path,
156 src_metadata: &std::fs::Metadata,
157 settings: &Settings,
158 preserve: &preserve::Settings,
159 is_fresh: bool,
160) -> Result<Summary, Error> {
161 if !is_fresh
164 && settings.ignore_existing
165 && crate::walk::run_metadata_probed(
166 congestion::Side::Destination,
167 congestion::MetadataOp::Stat,
168 tokio::fs::symlink_metadata(dst),
169 )
170 .await
171 .is_ok()
172 {
173 if let Some(mode) = settings.dry_run {
174 match mode {
175 DryRunMode::Brief => {}
176 DryRunMode::All => println!("skip file {:?}", dst),
177 DryRunMode::Explain => println!("skip file {:?} (destination exists)", dst),
178 }
179 }
180 tracing::debug!("destination exists, skipping (--ignore-existing)");
181 prog_track.files_unchanged.inc();
182 return Ok(Summary {
183 files_unchanged: 1,
184 ..Default::default()
185 });
186 }
187 if settings.dry_run.is_some() {
189 crate::dry_run::report_action("copy", src, Some(dst), "file");
190 return Ok(Summary {
191 files_copied: 1,
192 bytes_copied: src_metadata.len(),
193 ..Default::default()
194 });
195 }
196 tracing::debug!("opening 'src' for reading and 'dst' for writing");
197 get_file_iops_tokens(settings.chunk_size, src_metadata.size()).await;
198 let mut rm_summary = RmSummary::default();
199 if !is_fresh && dst.exists() {
200 if settings.overwrite {
201 tracing::debug!("file exists, check if it's identical");
202 let dst_metadata = crate::walk::run_metadata_probed(
203 congestion::Side::Destination,
204 congestion::MetadataOp::Stat,
205 tokio::fs::symlink_metadata(dst),
206 )
207 .await
208 .with_context(|| format!("failed reading metadata from {:?}", &dst))
209 .map_err(|err| Error::new(err, Default::default()))?;
210 if is_file_type_same(src_metadata, &dst_metadata) {
211 if filecmp::metadata_equal(&settings.overwrite_compare, src_metadata, &dst_metadata)
212 {
213 tracing::debug!("file is identical, skipping");
214 prog_track.files_unchanged.inc();
215 return Ok(Summary {
216 files_unchanged: 1,
217 ..Default::default()
218 });
219 }
220 if let Some(OverwriteFilter::Newer) = settings.overwrite_filter
221 && filecmp::dest_is_newer(src_metadata, &dst_metadata)
222 {
223 tracing::debug!("dest is newer than source, skipping");
224 prog_track.files_unchanged.inc();
225 return Ok(Summary {
226 files_unchanged: 1,
227 ..Default::default()
228 });
229 }
230 }
231 tracing::info!("file is different, removing existing file");
232 rm_summary = rm::rm(
234 prog_track,
235 dst,
236 &RmSettings {
237 fail_early: settings.fail_early,
238 filter: None,
239 dry_run: None,
240 time_filter: None,
241 },
242 )
243 .await
244 .map_err(|err| {
245 let rm_summary = err.summary;
246 let copy_summary = Summary {
247 rm_summary,
248 ..Default::default()
249 };
250 Error::new(err.source, copy_summary)
251 })?;
252 } else {
253 return Err(Error::new(
254 anyhow!(
255 "destination {:?} already exists, did you intend to specify --overwrite?",
256 dst
257 ),
258 Default::default(),
259 ));
260 }
261 }
262 tracing::debug!("copying data");
263 let mut copy_summary = Summary {
264 rm_summary,
265 ..Default::default()
266 };
267 tokio::fs::copy(src, dst)
268 .await
269 .with_context(|| format!("failed copying {:?} to {:?}", &src, &dst))
270 .map_err(|err| Error::new(err, copy_summary))?;
271 prog_track.files_copied.inc();
272 prog_track.bytes_copied.add(src_metadata.len());
273 tracing::debug!("setting permissions");
274 preserve::set_file_metadata(preserve, src_metadata, dst)
275 .await
276 .map_err(|err| Error::new(err, copy_summary))?;
277 copy_summary.bytes_copied += src_metadata.len();
279 copy_summary.files_copied += 1;
280 Ok(copy_summary)
281}
282
283#[derive(Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
284pub struct Summary {
285 pub bytes_copied: u64,
286 pub files_copied: usize,
287 pub symlinks_created: usize,
288 pub directories_created: usize,
289 pub files_unchanged: usize,
290 pub symlinks_unchanged: usize,
291 pub directories_unchanged: usize,
292 pub files_skipped: usize,
293 pub symlinks_skipped: usize,
294 pub directories_skipped: usize,
295 pub specials_skipped: usize,
296 pub rm_summary: RmSummary,
297}
298
299impl std::ops::Add for Summary {
300 type Output = Self;
301 fn add(self, other: Self) -> Self {
302 Self {
303 bytes_copied: self.bytes_copied + other.bytes_copied,
304 files_copied: self.files_copied + other.files_copied,
305 symlinks_created: self.symlinks_created + other.symlinks_created,
306 directories_created: self.directories_created + other.directories_created,
307 files_unchanged: self.files_unchanged + other.files_unchanged,
308 symlinks_unchanged: self.symlinks_unchanged + other.symlinks_unchanged,
309 directories_unchanged: self.directories_unchanged + other.directories_unchanged,
310 files_skipped: self.files_skipped + other.files_skipped,
311 symlinks_skipped: self.symlinks_skipped + other.symlinks_skipped,
312 directories_skipped: self.directories_skipped + other.directories_skipped,
313 specials_skipped: self.specials_skipped + other.specials_skipped,
314 rm_summary: self.rm_summary + other.rm_summary,
315 }
316 }
317}
318
319impl std::fmt::Display for Summary {
320 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
321 write!(
322 f,
323 "copy:\n\
324 -----\n\
325 bytes copied: {}\n\
326 files copied: {}\n\
327 symlinks created: {}\n\
328 directories created: {}\n\
329 files unchanged: {}\n\
330 symlinks unchanged: {}\n\
331 directories unchanged: {}\n\
332 files skipped: {}\n\
333 symlinks skipped: {}\n\
334 directories skipped: {}\n\
335 specials skipped: {}\n\
336 \n\
337 delete:\n\
338 -------\n\
339 {}",
340 bytesize::ByteSize(self.bytes_copied),
341 self.files_copied,
342 self.symlinks_created,
343 self.directories_created,
344 self.files_unchanged,
345 self.symlinks_unchanged,
346 self.directories_unchanged,
347 self.files_skipped,
348 self.symlinks_skipped,
349 self.directories_skipped,
350 self.specials_skipped,
351 &self.rm_summary,
352 )
353 }
354}
355
356#[instrument(skip(prog_track, settings, preserve))]
359pub async fn copy(
360 prog_track: &'static progress::Progress,
361 src: &std::path::Path,
362 dst: &std::path::Path,
363 settings: &Settings,
364 preserve: &preserve::Settings,
365 is_fresh: bool,
366) -> Result<Summary, Error> {
367 if let Some(ref filter) = settings.filter {
369 let src_name = src.file_name().map(std::path::Path::new);
370 if let Some(name) = src_name {
371 let src_metadata = crate::walk::run_metadata_probed(
372 congestion::Side::Source,
373 congestion::MetadataOp::Stat,
374 tokio::fs::symlink_metadata(src),
375 )
376 .await
377 .with_context(|| format!("failed reading metadata from src: {:?}", &src))
378 .map_err(|err| Error::new(err, Default::default()))?;
379 let is_dir = src_metadata.is_dir();
380 let result = filter.should_include_root_item(name, is_dir);
381 match result {
382 crate::filter::FilterResult::Included => {}
383 result => {
384 let kind = EntryKind::from_metadata(&src_metadata);
385 if let Some(mode) = settings.dry_run {
386 crate::dry_run::report_skip(src, &result, mode, kind.label_long());
387 }
388 kind.inc_skipped(prog_track);
389 return Ok(skipped_summary_for(kind));
390 }
391 }
392 }
393 }
394 copy_internal(
395 prog_track, src, dst, src, settings, preserve, is_fresh, None,
396 )
397 .await
398}
399
400#[instrument(skip(prog_track, settings, preserve, open_file_guard))]
401#[async_recursion]
402#[allow(clippy::too_many_arguments)]
403async fn copy_internal(
404 prog_track: &'static progress::Progress,
405 src: &std::path::Path,
406 dst: &std::path::Path,
407 source_root: &std::path::Path,
408 settings: &Settings,
409 preserve: &preserve::Settings,
410 mut is_fresh: bool,
411 open_file_guard: Option<throttle::OpenFileGuard>,
412) -> Result<Summary, Error> {
413 let _ops_guard = prog_track.ops.guard();
414 tracing::debug!("reading source metadata");
415 let src_metadata = crate::walk::run_metadata_probed(
416 congestion::Side::Source,
417 congestion::MetadataOp::Stat,
418 tokio::fs::symlink_metadata(src),
419 )
420 .await
421 .with_context(|| format!("failed reading metadata from src: {:?}", &src))
422 .map_err(|err| Error::new(err, Default::default()))?;
423 if settings.dereference && src_metadata.is_symlink() {
424 debug_assert!(
425 open_file_guard.is_none(),
426 "open file guard should not be pre-acquired for symlinks"
427 );
428 let link = crate::walk::run_metadata_probed(
429 congestion::Side::Source,
430 congestion::MetadataOp::Stat,
431 tokio::fs::canonicalize(&src),
432 )
433 .await
434 .with_context(|| format!("failed reading src symlink {:?}", &src))
435 .map_err(|err| Error::new(err, Default::default()))?;
436 return copy(prog_track, &link, dst, settings, preserve, is_fresh).await;
437 }
438 if src_metadata.is_file() {
439 let _guard = match open_file_guard {
441 Some(g) => g,
442 None => throttle::open_file_permit().await,
443 };
444 return copy_file(
445 prog_track,
446 src,
447 dst,
448 &src_metadata,
449 settings,
450 preserve,
451 is_fresh,
452 )
453 .await;
454 }
455 debug_assert!(
456 open_file_guard.is_none(),
457 "open file guard should not be pre-acquired for directories or symlinks"
458 );
459 if src_metadata.is_symlink() {
460 if !is_fresh
463 && settings.ignore_existing
464 && crate::walk::run_metadata_probed(
465 congestion::Side::Destination,
466 congestion::MetadataOp::Stat,
467 tokio::fs::symlink_metadata(dst),
468 )
469 .await
470 .is_ok()
471 {
472 if let Some(mode) = settings.dry_run {
473 match mode {
474 DryRunMode::Brief => {}
475 DryRunMode::All => println!("skip symlink {:?}", dst),
476 DryRunMode::Explain => println!("skip symlink {:?} (destination exists)", dst),
477 }
478 }
479 tracing::debug!("destination exists, skipping symlink (--ignore-existing)");
480 prog_track.symlinks_unchanged.inc();
481 return Ok(Summary {
482 symlinks_unchanged: 1,
483 ..Default::default()
484 });
485 }
486 if settings.dry_run.is_some() {
488 crate::dry_run::report_action("copy", src, Some(dst), "symlink");
489 return Ok(Summary {
490 symlinks_created: 1,
491 ..Default::default()
492 });
493 }
494 let mut rm_summary = RmSummary::default();
495 let link = crate::walk::run_metadata_probed(
496 congestion::Side::Source,
497 congestion::MetadataOp::ReadLink,
498 tokio::fs::read_link(src),
499 )
500 .await
501 .with_context(|| format!("failed reading symlink {:?}", &src))
502 .map_err(|err| Error::new(err, Default::default()))?;
503 if let Err(error) = crate::walk::run_metadata_probed(
505 congestion::Side::Destination,
506 congestion::MetadataOp::Symlink,
507 tokio::fs::symlink(&link, dst),
508 )
509 .await
510 {
511 if settings.ignore_existing && error.kind() == std::io::ErrorKind::AlreadyExists {
512 tracing::debug!("destination exists, skipping symlink (--ignore-existing)");
513 prog_track.symlinks_unchanged.inc();
514 return Ok(Summary {
515 symlinks_unchanged: 1,
516 ..Default::default()
517 });
518 }
519 if settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
520 let dst_metadata = crate::walk::run_metadata_probed(
521 congestion::Side::Destination,
522 congestion::MetadataOp::Stat,
523 tokio::fs::symlink_metadata(dst),
524 )
525 .await
526 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
527 .map_err(|err| Error::new(err, Default::default()))?;
528 if is_file_type_same(&src_metadata, &dst_metadata) {
529 let dst_link = crate::walk::run_metadata_probed(
530 congestion::Side::Destination,
531 congestion::MetadataOp::ReadLink,
532 tokio::fs::read_link(dst),
533 )
534 .await
535 .with_context(|| format!("failed reading dst symlink: {:?}", &dst))
536 .map_err(|err| Error::new(err, Default::default()))?;
537 if link == dst_link {
538 tracing::debug!(
539 "'dst' is a symlink and points to the same location as 'src'"
540 );
541 if preserve.symlink.any() {
542 let dst_metadata = crate::walk::run_metadata_probed(
544 congestion::Side::Destination,
545 congestion::MetadataOp::Stat,
546 tokio::fs::symlink_metadata(dst),
547 )
548 .await
549 .with_context(|| {
550 format!("failed reading metadata from dst: {:?}", &dst)
551 })
552 .map_err(|err| Error::new(err, Default::default()))?;
553 if !filecmp::metadata_equal(
554 &settings.overwrite_compare,
555 &src_metadata,
556 &dst_metadata,
557 ) {
558 tracing::debug!("'dst' metadata is different, updating");
559 preserve::set_symlink_metadata(preserve, &src_metadata, dst)
560 .await
561 .map_err(|err| Error::new(err, Default::default()))?;
562 prog_track.symlinks_removed.inc();
563 prog_track.symlinks_created.inc();
564 return Ok(Summary {
565 rm_summary: RmSummary {
566 symlinks_removed: 1,
567 ..Default::default()
568 },
569 symlinks_created: 1,
570 ..Default::default()
571 });
572 }
573 }
574 tracing::debug!("symlink already exists, skipping");
575 prog_track.symlinks_unchanged.inc();
576 return Ok(Summary {
577 symlinks_unchanged: 1,
578 ..Default::default()
579 });
580 }
581 tracing::debug!("'dst' is a symlink but points to a different path, updating");
582 } else {
583 tracing::info!("'dst' is not a symlink, updating");
584 }
585 rm_summary = rm::rm(
586 prog_track,
587 dst,
588 &RmSettings {
589 fail_early: settings.fail_early,
590 filter: None,
591 dry_run: None,
592 time_filter: None,
593 },
594 )
595 .await
596 .map_err(|err| {
597 let rm_summary = err.summary;
598 let copy_summary = Summary {
599 rm_summary,
600 ..Default::default()
601 };
602 Error::new(err.source, copy_summary)
603 })?;
604 crate::walk::run_metadata_probed(
605 congestion::Side::Destination,
606 congestion::MetadataOp::Symlink,
607 tokio::fs::symlink(&link, dst),
608 )
609 .await
610 .with_context(|| format!("failed creating symlink {:?}", &dst))
611 .map_err(|err| {
612 let copy_summary = Summary {
613 rm_summary,
614 ..Default::default()
615 };
616 Error::new(err, copy_summary)
617 })?;
618 } else {
619 return Err(Error::new(
620 anyhow!("failed creating symlink {:?}", &dst),
621 Default::default(),
622 ));
623 }
624 }
625 preserve::set_symlink_metadata(preserve, &src_metadata, dst)
626 .await
627 .map_err(|err| {
628 let copy_summary = Summary {
629 rm_summary,
630 ..Default::default()
631 };
632 Error::new(err, copy_summary)
633 })?;
634 prog_track.symlinks_created.inc();
635 return Ok(Summary {
636 rm_summary,
637 symlinks_created: 1,
638 ..Default::default()
639 });
640 }
641 if !src_metadata.is_dir() {
642 if settings.skip_specials {
643 tracing::debug!(
644 "skipping special file {:?} (type: {:?})",
645 src,
646 src_metadata.file_type()
647 );
648 if let Some(mode) = settings.dry_run {
649 match mode {
650 DryRunMode::Brief => {}
651 DryRunMode::All => println!("skip special {:?}", src),
652 DryRunMode::Explain => {
653 println!(
654 "skip special {:?} (unsupported file type: {:?})",
655 src,
656 src_metadata.file_type()
657 );
658 }
659 }
660 }
661 prog_track.specials_skipped.inc();
662 return Ok(Summary {
663 specials_skipped: 1,
664 ..Default::default()
665 });
666 }
667 return Err(Error::new(
668 anyhow!(
669 "copy: {:?} -> {:?} failed, unsupported src file type: {:?}",
670 src,
671 dst,
672 src_metadata.file_type()
673 ),
674 Default::default(),
675 ));
676 }
677 if settings.dry_run.is_some() {
679 if settings.ignore_existing
680 && !is_fresh
681 && crate::walk::run_metadata_probed(
682 congestion::Side::Destination,
683 congestion::MetadataOp::Stat,
684 tokio::fs::symlink_metadata(dst),
685 )
686 .await
687 .is_ok()
688 && !dst.is_dir()
689 {
690 if let Some(mode) = settings.dry_run {
692 match mode {
693 DryRunMode::Brief => {}
694 DryRunMode::All => println!("skip dir {:?}", dst),
695 DryRunMode::Explain => {
696 println!("skip dir {:?} (destination exists, not a directory)", dst);
697 }
698 }
699 }
700 return Ok(Summary {
701 directories_unchanged: 1,
702 ..Default::default()
703 });
704 }
705 crate::dry_run::report_action("copy", src, Some(dst), "dir");
706 }
708 tracing::debug!("process contents of 'src' directory");
709 let mut entries = tokio::fs::read_dir(src)
710 .await
711 .with_context(|| format!("cannot open directory {src:?} for reading"))
712 .map_err(|err| Error::new(err, Default::default()))?;
713 let mut copy_summary = if settings.dry_run.is_some() {
715 Summary {
716 directories_created: 1, ..Default::default()
718 }
719 } else if let Err(error) = crate::walk::run_metadata_probed(
720 congestion::Side::Destination,
721 congestion::MetadataOp::MkDir,
722 tokio::fs::create_dir(dst),
723 )
724 .await
725 {
726 assert!(
727 !is_fresh,
728 "unexpected error creating directory: {dst:?}: {error}"
729 );
730 if (settings.overwrite || settings.ignore_existing)
731 && error.kind() == std::io::ErrorKind::AlreadyExists
732 {
733 let dst_metadata = crate::walk::run_metadata_probed(
738 congestion::Side::Destination,
739 congestion::MetadataOp::Stat,
740 tokio::fs::symlink_metadata(dst),
741 )
742 .await
743 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
744 .map_err(|err| Error::new(err, Default::default()))?;
745 if dst_metadata.is_dir() {
746 tracing::debug!("'dst' is a directory, leaving it as is");
747 prog_track.directories_unchanged.inc();
748 Summary {
749 directories_unchanged: 1,
750 ..Default::default()
751 }
752 } else if settings.ignore_existing {
753 tracing::debug!(
756 "destination exists but is not a directory, skipping subtree (--ignore-existing)"
757 );
758 prog_track.directories_unchanged.inc();
759 return Ok(Summary {
760 directories_unchanged: 1,
761 ..Default::default()
762 });
763 } else {
764 tracing::info!("'dst' is not a directory, removing and creating a new one");
765 let rm_summary = rm::rm(
766 prog_track,
767 dst,
768 &RmSettings {
769 fail_early: settings.fail_early,
770 filter: None,
771 dry_run: None,
772 time_filter: None,
773 },
774 )
775 .await
776 .map_err(|err| {
777 let rm_summary = err.summary;
778 let copy_summary = Summary {
779 rm_summary,
780 ..Default::default()
781 };
782 Error::new(err.source, copy_summary)
783 })?;
784 crate::walk::run_metadata_probed(
785 congestion::Side::Destination,
786 congestion::MetadataOp::MkDir,
787 tokio::fs::create_dir(dst),
788 )
789 .await
790 .with_context(|| format!("cannot create directory {dst:?}"))
791 .map_err(|err| {
792 let copy_summary = Summary {
793 rm_summary,
794 ..Default::default()
795 };
796 Error::new(err, copy_summary)
797 })?;
798 is_fresh = true;
800 prog_track.directories_created.inc();
801 Summary {
802 rm_summary,
803 directories_created: 1,
804 ..Default::default()
805 }
806 }
807 } else {
808 let error = Err::<(), std::io::Error>(error)
809 .with_context(|| format!("cannot create directory {:?}", dst))
810 .unwrap_err();
811 tracing::error!("{:#}", &error);
812 return Err(Error::new(error, Default::default()));
813 }
814 } else {
815 is_fresh = true;
817 prog_track.directories_created.inc();
818 Summary {
819 directories_created: 1,
820 ..Default::default()
821 }
822 };
823 let we_created_this_dir = copy_summary.directories_created == 1;
826 let mut join_set = tokio::task::JoinSet::new();
827 let errors = crate::error_collector::ErrorCollector::default();
828 loop {
829 let Some((entry, entry_file_type)) =
830 crate::walk::next_entry_probed(&mut entries, congestion::Side::Source, || {
831 format!("failed traversing src directory {:?}", &src)
832 })
833 .await
834 .map_err(|err| Error::new(err, copy_summary))?
835 else {
836 break;
837 };
838 let entry_path = entry.path();
839 let entry_name = entry_path.file_name().unwrap();
840 let dst_path = dst.join(entry_name);
841 let entry_kind = EntryKind::from_file_type(entry_file_type.as_ref());
842 let entry_is_dir = entry_kind == EntryKind::Dir;
843 let relative_path = entry_path.strip_prefix(source_root).unwrap_or(&entry_path);
845 if let Some(skip_result) =
847 walk::should_skip_entry(&settings.filter, relative_path, entry_is_dir)
848 {
849 if let Some(mode) = settings.dry_run {
850 crate::dry_run::report_skip(&entry_path, &skip_result, mode, entry_kind.label());
851 }
852 tracing::debug!("skipping {:?} due to filter", &entry_path);
853 copy_summary = copy_summary + skipped_summary_for(entry_kind);
854 entry_kind.inc_skipped(prog_track);
855 continue;
856 }
857 if settings.skip_specials && entry_kind == EntryKind::Special {
859 tracing::debug!("skipping special file {:?}", &entry_path);
860 if let Some(mode) = settings.dry_run {
861 match mode {
862 DryRunMode::Brief => {}
863 DryRunMode::All => println!("skip special {:?}", &entry_path),
864 DryRunMode::Explain => {
865 println!(
866 "skip special {:?} (unsupported file type: {:?})",
867 &entry_path,
868 entry_file_type.unwrap()
869 );
870 }
871 }
872 }
873 copy_summary.specials_skipped += 1;
874 prog_track.specials_skipped.inc();
875 continue;
876 }
877 let entry_is_regular_file = entry_file_type.as_ref().is_some_and(|ft| ft.is_file());
885 let open_file_guard = if entry_is_regular_file {
886 Some(throttle::open_file_permit().await)
887 } else {
888 None
889 };
890 let settings = settings.clone();
893 let preserve = *preserve;
894 let source_root = source_root.to_owned();
895 let do_copy = || async move {
896 copy_internal(
897 prog_track,
898 &entry_path,
899 &dst_path,
900 &source_root,
901 &settings,
902 &preserve,
903 is_fresh,
904 open_file_guard,
905 )
906 .await
907 };
908 join_set.spawn(do_copy());
909 }
910 drop(entries);
913 while let Some(res) = join_set.join_next().await {
914 match res {
915 Ok(result) => match result {
916 Ok(summary) => copy_summary = copy_summary + summary,
917 Err(error) => {
918 tracing::error!("copy: {:?} -> {:?} failed with: {:#}", src, dst, &error);
919 copy_summary = copy_summary + error.summary;
920 if settings.fail_early {
921 return Err(Error::new(error.source, copy_summary));
922 }
923 errors.push(error.source);
924 }
925 },
926 Err(error) => {
927 if settings.fail_early {
928 return Err(Error::new(error.into(), copy_summary));
929 }
930 errors.push(error.into());
931 }
932 }
933 }
934 let this_dir_count = usize::from(we_created_this_dir);
937 let child_dirs_created = copy_summary
938 .directories_created
939 .saturating_sub(this_dir_count);
940 let anything_copied = copy_summary.files_copied > 0
941 || copy_summary.symlinks_created > 0
942 || child_dirs_created > 0;
943 let relative_path = src.strip_prefix(source_root).unwrap_or(src);
944 let is_root = src == source_root;
945 match check_empty_dir_cleanup(
946 settings.filter.as_ref(),
947 we_created_this_dir,
948 anything_copied,
949 relative_path,
950 is_root,
951 settings.dry_run.is_some(),
952 ) {
953 EmptyDirAction::Keep => { }
954 EmptyDirAction::DryRunSkip => {
955 tracing::debug!(
956 "dry-run: directory {:?} would not be created (nothing to copy inside)",
957 &dst
958 );
959 copy_summary.directories_created = 0;
960 return Ok(copy_summary);
961 }
962 EmptyDirAction::Remove => {
963 tracing::debug!(
964 "directory {:?} has nothing to copy inside, removing empty directory",
965 &dst
966 );
967 match crate::walk::run_metadata_probed(
968 congestion::Side::Destination,
969 congestion::MetadataOp::RmDir,
970 tokio::fs::remove_dir(dst),
971 )
972 .await
973 {
974 Ok(()) => {
975 copy_summary.directories_created = 0;
976 return Ok(copy_summary);
977 }
978 Err(err) => {
979 tracing::debug!(
981 "failed to remove empty directory {:?}: {:#}, keeping",
982 &dst,
983 &err
984 );
985 }
987 }
988 }
989 }
990 tracing::debug!("set 'dst' directory metadata");
995 let metadata_result = if settings.dry_run.is_some() {
996 Ok(()) } else {
998 preserve::set_dir_metadata(preserve, &src_metadata, dst).await
999 };
1000 if errors.has_errors() {
1001 if let Err(metadata_err) = metadata_result {
1003 tracing::error!(
1004 "copy: {:?} -> {:?} failed to set directory metadata: {:#}",
1005 src,
1006 dst,
1007 &metadata_err
1008 );
1009 }
1010 return Err(Error::new(errors.into_error().unwrap(), copy_summary));
1012 }
1013 metadata_result.map_err(|err| Error::new(err, copy_summary))?;
1015 Ok(copy_summary)
1016}
1017
1018#[cfg(test)]
1019mod copy_tests {
1020 use crate::testutils;
1021 use anyhow::Context;
1022 use std::os::unix::fs::MetadataExt;
1023 use std::os::unix::fs::PermissionsExt;
1024 use tracing_test::traced_test;
1025
1026 use super::*;
1027
1028 static PROGRESS: std::sync::LazyLock<progress::Progress> =
1029 std::sync::LazyLock::new(progress::Progress::new);
1030 static NO_PRESERVE_SETTINGS: std::sync::LazyLock<preserve::Settings> =
1031 std::sync::LazyLock::new(preserve::preserve_none);
1032 static DO_PRESERVE_SETTINGS: std::sync::LazyLock<preserve::Settings> =
1033 std::sync::LazyLock::new(preserve::preserve_all);
1034
1035 #[tokio::test]
1036 #[traced_test]
1037 async fn check_basic_copy() -> Result<(), anyhow::Error> {
1038 let tmp_dir = testutils::setup_test_dir().await?;
1039 let test_path = tmp_dir.as_path();
1040 let summary = copy(
1041 &PROGRESS,
1042 &test_path.join("foo"),
1043 &test_path.join("bar"),
1044 &Settings {
1045 dereference: false,
1046 fail_early: false,
1047 overwrite: false,
1048 overwrite_compare: filecmp::MetadataCmpSettings {
1049 size: true,
1050 mtime: true,
1051 ..Default::default()
1052 },
1053 overwrite_filter: None,
1054 ignore_existing: false,
1055 chunk_size: 0,
1056 skip_specials: false,
1057 remote_copy_buffer_size: 0,
1058 filter: None,
1059 dry_run: None,
1060 },
1061 &NO_PRESERVE_SETTINGS,
1062 false,
1063 )
1064 .await?;
1065 assert_eq!(summary.files_copied, 5);
1066 assert_eq!(summary.symlinks_created, 2);
1067 assert_eq!(summary.directories_created, 3);
1068 testutils::check_dirs_identical(
1069 &test_path.join("foo"),
1070 &test_path.join("bar"),
1071 testutils::FileEqualityCheck::Basic,
1072 )
1073 .await?;
1074 Ok(())
1075 }
1076
1077 #[tokio::test]
1078 #[traced_test]
1079 async fn no_read_permission() -> Result<(), anyhow::Error> {
1080 let tmp_dir = testutils::setup_test_dir().await?;
1081 let test_path = tmp_dir.as_path();
1082 let filepaths = vec![
1083 test_path.join("foo").join("0.txt"),
1084 test_path.join("foo").join("baz"),
1085 ];
1086 for fpath in &filepaths {
1087 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o000)).await?;
1089 }
1090 match copy(
1091 &PROGRESS,
1092 &test_path.join("foo"),
1093 &test_path.join("bar"),
1094 &Settings {
1095 dereference: false,
1096 fail_early: false,
1097 overwrite: false,
1098 overwrite_compare: filecmp::MetadataCmpSettings {
1099 size: true,
1100 mtime: true,
1101 ..Default::default()
1102 },
1103 overwrite_filter: None,
1104 ignore_existing: false,
1105 chunk_size: 0,
1106 skip_specials: false,
1107 remote_copy_buffer_size: 0,
1108 filter: None,
1109 dry_run: None,
1110 },
1111 &NO_PRESERVE_SETTINGS,
1112 false,
1113 )
1114 .await
1115 {
1116 Ok(_) => panic!("Expected the copy to error!"),
1117 Err(error) => {
1118 tracing::info!("{}", &error);
1119 assert_eq!(error.summary.files_copied, 3);
1130 assert_eq!(error.summary.symlinks_created, 0);
1131 assert_eq!(error.summary.directories_created, 2);
1132 }
1133 }
1134 for fpath in &filepaths {
1136 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o700)).await?;
1137 if tokio::fs::symlink_metadata(fpath).await?.is_file() {
1138 tokio::fs::remove_file(fpath).await?;
1139 } else {
1140 tokio::fs::remove_dir_all(fpath).await?;
1141 }
1142 }
1143 testutils::check_dirs_identical(
1144 &test_path.join("foo"),
1145 &test_path.join("bar"),
1146 testutils::FileEqualityCheck::Basic,
1147 )
1148 .await?;
1149 Ok(())
1150 }
1151
1152 #[tokio::test]
1153 #[traced_test]
1154 async fn check_default_mode() -> Result<(), anyhow::Error> {
1155 let tmp_dir = testutils::setup_test_dir().await?;
1156 tokio::fs::set_permissions(
1158 tmp_dir.join("foo").join("0.txt"),
1159 std::fs::Permissions::from_mode(0o700),
1160 )
1161 .await?;
1162 let exec_sticky_file = tmp_dir.join("foo").join("bar").join("1.txt");
1164 tokio::fs::set_permissions(&exec_sticky_file, std::fs::Permissions::from_mode(0o3770))
1165 .await?;
1166 let test_path = tmp_dir.as_path();
1167 let summary = copy(
1168 &PROGRESS,
1169 &test_path.join("foo"),
1170 &test_path.join("bar"),
1171 &Settings {
1172 dereference: false,
1173 fail_early: false,
1174 overwrite: false,
1175 overwrite_compare: filecmp::MetadataCmpSettings {
1176 size: true,
1177 mtime: true,
1178 ..Default::default()
1179 },
1180 overwrite_filter: None,
1181 ignore_existing: false,
1182 chunk_size: 0,
1183 skip_specials: false,
1184 remote_copy_buffer_size: 0,
1185 filter: None,
1186 dry_run: None,
1187 },
1188 &NO_PRESERVE_SETTINGS,
1189 false,
1190 )
1191 .await?;
1192 assert_eq!(summary.files_copied, 5);
1193 assert_eq!(summary.symlinks_created, 2);
1194 assert_eq!(summary.directories_created, 3);
1195 tokio::fs::set_permissions(
1197 &exec_sticky_file,
1198 std::fs::Permissions::from_mode(
1199 std::fs::symlink_metadata(&exec_sticky_file)?
1200 .permissions()
1201 .mode()
1202 & 0o0777,
1203 ),
1204 )
1205 .await?;
1206 testutils::check_dirs_identical(
1207 &test_path.join("foo"),
1208 &test_path.join("bar"),
1209 testutils::FileEqualityCheck::Basic,
1210 )
1211 .await?;
1212 Ok(())
1213 }
1214
1215 #[tokio::test]
1216 #[traced_test]
1217 async fn no_write_permission() -> Result<(), anyhow::Error> {
1218 let tmp_dir = testutils::setup_test_dir().await?;
1219 let test_path = tmp_dir.as_path();
1220 let non_exec_dir = test_path.join("foo").join("bogey");
1222 tokio::fs::create_dir(&non_exec_dir).await?;
1223 tokio::fs::set_permissions(&non_exec_dir, std::fs::Permissions::from_mode(0o400)).await?;
1224 tokio::fs::set_permissions(
1226 &test_path.join("foo").join("baz"),
1227 std::fs::Permissions::from_mode(0o500),
1228 )
1229 .await?;
1230 tokio::fs::set_permissions(
1232 &test_path.join("foo").join("baz").join("4.txt"),
1233 std::fs::Permissions::from_mode(0o440),
1234 )
1235 .await?;
1236 let summary = copy(
1237 &PROGRESS,
1238 &test_path.join("foo"),
1239 &test_path.join("bar"),
1240 &Settings {
1241 dereference: false,
1242 fail_early: false,
1243 overwrite: false,
1244 overwrite_compare: filecmp::MetadataCmpSettings {
1245 size: true,
1246 mtime: true,
1247 ..Default::default()
1248 },
1249 overwrite_filter: None,
1250 ignore_existing: false,
1251 chunk_size: 0,
1252 skip_specials: false,
1253 remote_copy_buffer_size: 0,
1254 filter: None,
1255 dry_run: None,
1256 },
1257 &NO_PRESERVE_SETTINGS,
1258 false,
1259 )
1260 .await?;
1261 assert_eq!(summary.files_copied, 5);
1262 assert_eq!(summary.symlinks_created, 2);
1263 assert_eq!(summary.directories_created, 4);
1264 testutils::check_dirs_identical(
1265 &test_path.join("foo"),
1266 &test_path.join("bar"),
1267 testutils::FileEqualityCheck::Basic,
1268 )
1269 .await?;
1270 Ok(())
1271 }
1272
1273 #[tokio::test]
1274 #[traced_test]
1275 async fn dereference() -> Result<(), anyhow::Error> {
1276 let tmp_dir = testutils::setup_test_dir().await?;
1277 let test_path = tmp_dir.as_path();
1278 let src1 = &test_path.join("foo").join("bar").join("2.txt");
1280 let src2 = &test_path.join("foo").join("bar").join("3.txt");
1281 let test_mode = 0o440;
1282 for f in [src1, src2] {
1283 tokio::fs::set_permissions(f, std::fs::Permissions::from_mode(test_mode)).await?;
1284 }
1285 let summary = copy(
1286 &PROGRESS,
1287 &test_path.join("foo"),
1288 &test_path.join("bar"),
1289 &Settings {
1290 dereference: true, fail_early: false,
1292 overwrite: false,
1293 overwrite_compare: filecmp::MetadataCmpSettings {
1294 size: true,
1295 mtime: true,
1296 ..Default::default()
1297 },
1298 overwrite_filter: None,
1299 ignore_existing: false,
1300 chunk_size: 0,
1301 skip_specials: false,
1302 remote_copy_buffer_size: 0,
1303 filter: None,
1304 dry_run: None,
1305 },
1306 &NO_PRESERVE_SETTINGS,
1307 false,
1308 )
1309 .await?;
1310 assert_eq!(summary.files_copied, 7);
1311 assert_eq!(summary.symlinks_created, 0);
1312 assert_eq!(summary.directories_created, 3);
1313 let dst1 = &test_path.join("bar").join("baz").join("5.txt");
1319 let dst2 = &test_path.join("bar").join("baz").join("6.txt");
1320 for f in [dst1, dst2] {
1321 let metadata = tokio::fs::symlink_metadata(f)
1322 .await
1323 .with_context(|| format!("failed reading metadata from {:?}", &f))?;
1324 assert!(metadata.is_file());
1325 assert_eq!(metadata.permissions().mode() & 0o777, test_mode);
1327 }
1328 Ok(())
1329 }
1330
1331 async fn cp_compare(
1332 cp_args: &[&str],
1333 rcp_settings: &Settings,
1334 preserve: bool,
1335 ) -> Result<(), anyhow::Error> {
1336 let tmp_dir = testutils::setup_test_dir().await?;
1337 let test_path = tmp_dir.as_path();
1338 let cp_output = tokio::process::Command::new("cp")
1340 .args(cp_args)
1341 .arg(test_path.join("foo"))
1342 .arg(test_path.join("bar"))
1343 .output()
1344 .await?;
1345 assert!(cp_output.status.success());
1346 let summary = copy(
1348 &PROGRESS,
1349 &test_path.join("foo"),
1350 &test_path.join("baz"),
1351 rcp_settings,
1352 if preserve {
1353 &DO_PRESERVE_SETTINGS
1354 } else {
1355 &NO_PRESERVE_SETTINGS
1356 },
1357 false,
1358 )
1359 .await?;
1360 if rcp_settings.dereference {
1361 assert_eq!(summary.files_copied, 7);
1362 assert_eq!(summary.symlinks_created, 0);
1363 } else {
1364 assert_eq!(summary.files_copied, 5);
1365 assert_eq!(summary.symlinks_created, 2);
1366 }
1367 assert_eq!(summary.directories_created, 3);
1368 testutils::check_dirs_identical(
1369 &test_path.join("bar"),
1370 &test_path.join("baz"),
1371 if preserve {
1372 testutils::FileEqualityCheck::Timestamp
1373 } else {
1374 testutils::FileEqualityCheck::Basic
1375 },
1376 )
1377 .await?;
1378 Ok(())
1379 }
1380
1381 #[tokio::test]
1382 #[traced_test]
1383 async fn test_cp_compat() -> Result<(), anyhow::Error> {
1384 cp_compare(
1385 &["-r"],
1386 &Settings {
1387 dereference: false,
1388 fail_early: false,
1389 overwrite: false,
1390 overwrite_compare: filecmp::MetadataCmpSettings {
1391 size: true,
1392 mtime: true,
1393 ..Default::default()
1394 },
1395 overwrite_filter: None,
1396 ignore_existing: false,
1397 chunk_size: 0,
1398 skip_specials: false,
1399 remote_copy_buffer_size: 0,
1400 filter: None,
1401 dry_run: None,
1402 },
1403 false,
1404 )
1405 .await?;
1406 Ok(())
1407 }
1408
1409 #[tokio::test]
1410 #[traced_test]
1411 async fn test_cp_compat_preserve() -> Result<(), anyhow::Error> {
1412 cp_compare(
1413 &["-r", "-p"],
1414 &Settings {
1415 dereference: false,
1416 fail_early: false,
1417 overwrite: false,
1418 overwrite_compare: filecmp::MetadataCmpSettings {
1419 size: true,
1420 mtime: true,
1421 ..Default::default()
1422 },
1423 overwrite_filter: None,
1424 ignore_existing: false,
1425 chunk_size: 0,
1426 skip_specials: false,
1427 remote_copy_buffer_size: 0,
1428 filter: None,
1429 dry_run: None,
1430 },
1431 true,
1432 )
1433 .await?;
1434 Ok(())
1435 }
1436
1437 #[tokio::test]
1438 #[traced_test]
1439 async fn test_cp_compat_dereference() -> Result<(), anyhow::Error> {
1440 cp_compare(
1441 &["-r", "-L"],
1442 &Settings {
1443 dereference: true,
1444 fail_early: false,
1445 overwrite: false,
1446 overwrite_compare: filecmp::MetadataCmpSettings {
1447 size: true,
1448 mtime: true,
1449 ..Default::default()
1450 },
1451 overwrite_filter: None,
1452 ignore_existing: false,
1453 chunk_size: 0,
1454 skip_specials: false,
1455 remote_copy_buffer_size: 0,
1456 filter: None,
1457 dry_run: None,
1458 },
1459 false,
1460 )
1461 .await?;
1462 Ok(())
1463 }
1464
1465 #[tokio::test]
1466 #[traced_test]
1467 async fn test_cp_compat_preserve_and_dereference() -> Result<(), anyhow::Error> {
1468 cp_compare(
1469 &["-r", "-p", "-L"],
1470 &Settings {
1471 dereference: true,
1472 fail_early: false,
1473 overwrite: false,
1474 overwrite_compare: filecmp::MetadataCmpSettings {
1475 size: true,
1476 mtime: true,
1477 ..Default::default()
1478 },
1479 overwrite_filter: None,
1480 ignore_existing: false,
1481 chunk_size: 0,
1482 skip_specials: false,
1483 remote_copy_buffer_size: 0,
1484 filter: None,
1485 dry_run: None,
1486 },
1487 true,
1488 )
1489 .await?;
1490 Ok(())
1491 }
1492
1493 async fn setup_test_dir_and_copy() -> Result<std::path::PathBuf, anyhow::Error> {
1494 let tmp_dir = testutils::setup_test_dir().await?;
1495 let test_path = tmp_dir.as_path();
1496 let summary = copy(
1497 &PROGRESS,
1498 &test_path.join("foo"),
1499 &test_path.join("bar"),
1500 &Settings {
1501 dereference: false,
1502 fail_early: false,
1503 overwrite: false,
1504 overwrite_compare: filecmp::MetadataCmpSettings {
1505 size: true,
1506 mtime: true,
1507 ..Default::default()
1508 },
1509 overwrite_filter: None,
1510 ignore_existing: false,
1511 chunk_size: 0,
1512 skip_specials: false,
1513 remote_copy_buffer_size: 0,
1514 filter: None,
1515 dry_run: None,
1516 },
1517 &DO_PRESERVE_SETTINGS,
1518 false,
1519 )
1520 .await?;
1521 assert_eq!(summary.files_copied, 5);
1522 assert_eq!(summary.symlinks_created, 2);
1523 assert_eq!(summary.directories_created, 3);
1524 Ok(tmp_dir)
1525 }
1526
1527 #[tokio::test]
1528 #[traced_test]
1529 async fn test_cp_overwrite_basic() -> Result<(), anyhow::Error> {
1530 let tmp_dir = setup_test_dir_and_copy().await?;
1531 let output_path = &tmp_dir.join("bar");
1532 {
1533 let summary = rm::rm(
1544 &PROGRESS,
1545 &output_path.join("bar"),
1546 &RmSettings {
1547 fail_early: false,
1548 filter: None,
1549 dry_run: None,
1550 time_filter: None,
1551 },
1552 )
1553 .await?
1554 + rm::rm(
1555 &PROGRESS,
1556 &output_path.join("baz").join("5.txt"),
1557 &RmSettings {
1558 fail_early: false,
1559 filter: None,
1560 dry_run: None,
1561 time_filter: None,
1562 },
1563 )
1564 .await?;
1565 assert_eq!(summary.files_removed, 3);
1566 assert_eq!(summary.symlinks_removed, 1);
1567 assert_eq!(summary.directories_removed, 1);
1568 }
1569 let summary = copy(
1570 &PROGRESS,
1571 &tmp_dir.join("foo"),
1572 output_path,
1573 &Settings {
1574 dereference: false,
1575 fail_early: false,
1576 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1578 size: true,
1579 mtime: true,
1580 ..Default::default()
1581 },
1582 overwrite_filter: None,
1583 ignore_existing: false,
1584 chunk_size: 0,
1585 skip_specials: false,
1586 remote_copy_buffer_size: 0,
1587 filter: None,
1588 dry_run: None,
1589 },
1590 &DO_PRESERVE_SETTINGS,
1591 false,
1592 )
1593 .await?;
1594 assert_eq!(summary.files_copied, 3);
1595 assert_eq!(summary.symlinks_created, 1);
1596 assert_eq!(summary.directories_created, 1);
1597 testutils::check_dirs_identical(
1598 &tmp_dir.join("foo"),
1599 output_path,
1600 testutils::FileEqualityCheck::Timestamp,
1601 )
1602 .await?;
1603 Ok(())
1604 }
1605
1606 #[tokio::test]
1607 #[traced_test]
1608 async fn test_cp_overwrite_dir_file() -> Result<(), anyhow::Error> {
1609 let tmp_dir = setup_test_dir_and_copy().await?;
1610 let output_path = &tmp_dir.join("bar");
1611 {
1612 let summary = rm::rm(
1623 &PROGRESS,
1624 &output_path.join("bar").join("1.txt"),
1625 &RmSettings {
1626 fail_early: false,
1627 filter: None,
1628 dry_run: None,
1629 time_filter: None,
1630 },
1631 )
1632 .await?
1633 + rm::rm(
1634 &PROGRESS,
1635 &output_path.join("baz"),
1636 &RmSettings {
1637 fail_early: false,
1638 filter: None,
1639 dry_run: None,
1640 time_filter: None,
1641 },
1642 )
1643 .await?;
1644 assert_eq!(summary.files_removed, 2);
1645 assert_eq!(summary.symlinks_removed, 2);
1646 assert_eq!(summary.directories_removed, 1);
1647 }
1648 {
1649 tokio::fs::create_dir(&output_path.join("bar").join("1.txt")).await?;
1651 tokio::fs::write(&output_path.join("baz"), "baz").await?;
1653 }
1654 let summary = copy(
1655 &PROGRESS,
1656 &tmp_dir.join("foo"),
1657 output_path,
1658 &Settings {
1659 dereference: false,
1660 fail_early: false,
1661 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1663 size: true,
1664 mtime: true,
1665 ..Default::default()
1666 },
1667 overwrite_filter: None,
1668 ignore_existing: false,
1669 chunk_size: 0,
1670 skip_specials: false,
1671 remote_copy_buffer_size: 0,
1672 filter: None,
1673 dry_run: None,
1674 },
1675 &DO_PRESERVE_SETTINGS,
1676 false,
1677 )
1678 .await?;
1679 assert_eq!(summary.rm_summary.files_removed, 1);
1680 assert_eq!(summary.rm_summary.symlinks_removed, 0);
1681 assert_eq!(summary.rm_summary.directories_removed, 1);
1682 assert_eq!(summary.files_copied, 2);
1683 assert_eq!(summary.symlinks_created, 2);
1684 assert_eq!(summary.directories_created, 1);
1685 testutils::check_dirs_identical(
1686 &tmp_dir.join("foo"),
1687 output_path,
1688 testutils::FileEqualityCheck::Timestamp,
1689 )
1690 .await?;
1691 Ok(())
1692 }
1693
1694 #[tokio::test]
1695 #[traced_test]
1696 async fn test_cp_overwrite_symlink_file() -> Result<(), anyhow::Error> {
1697 let tmp_dir = setup_test_dir_and_copy().await?;
1698 let output_path = &tmp_dir.join("bar");
1699 {
1700 let summary = rm::rm(
1707 &PROGRESS,
1708 &output_path.join("baz").join("4.txt"),
1709 &RmSettings {
1710 fail_early: false,
1711 filter: None,
1712 dry_run: None,
1713 time_filter: None,
1714 },
1715 )
1716 .await?
1717 + rm::rm(
1718 &PROGRESS,
1719 &output_path.join("baz").join("5.txt"),
1720 &RmSettings {
1721 fail_early: false,
1722 filter: None,
1723 dry_run: None,
1724 time_filter: None,
1725 },
1726 )
1727 .await?;
1728 assert_eq!(summary.files_removed, 1);
1729 assert_eq!(summary.symlinks_removed, 1);
1730 assert_eq!(summary.directories_removed, 0);
1731 }
1732 {
1733 tokio::fs::symlink("../0.txt", &output_path.join("baz").join("4.txt")).await?;
1735 tokio::fs::write(&output_path.join("baz").join("5.txt"), "baz").await?;
1737 }
1738 let summary = copy(
1739 &PROGRESS,
1740 &tmp_dir.join("foo"),
1741 output_path,
1742 &Settings {
1743 dereference: false,
1744 fail_early: false,
1745 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1747 size: true,
1748 mtime: true,
1749 ..Default::default()
1750 },
1751 overwrite_filter: None,
1752 ignore_existing: false,
1753 chunk_size: 0,
1754 skip_specials: false,
1755 remote_copy_buffer_size: 0,
1756 filter: None,
1757 dry_run: None,
1758 },
1759 &DO_PRESERVE_SETTINGS,
1760 false,
1761 )
1762 .await?;
1763 assert_eq!(summary.rm_summary.files_removed, 1);
1764 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1765 assert_eq!(summary.rm_summary.directories_removed, 0);
1766 assert_eq!(summary.files_copied, 1);
1767 assert_eq!(summary.symlinks_created, 1);
1768 assert_eq!(summary.directories_created, 0);
1769 testutils::check_dirs_identical(
1770 &tmp_dir.join("foo"),
1771 output_path,
1772 testutils::FileEqualityCheck::Timestamp,
1773 )
1774 .await?;
1775 Ok(())
1776 }
1777
1778 #[tokio::test]
1779 #[traced_test]
1780 async fn test_cp_overwrite_symlink_dir() -> Result<(), anyhow::Error> {
1781 let tmp_dir = setup_test_dir_and_copy().await?;
1782 let output_path = &tmp_dir.join("bar");
1783 {
1784 let summary = rm::rm(
1794 &PROGRESS,
1795 &output_path.join("bar"),
1796 &RmSettings {
1797 fail_early: false,
1798 filter: None,
1799 dry_run: None,
1800 time_filter: None,
1801 },
1802 )
1803 .await?
1804 + rm::rm(
1805 &PROGRESS,
1806 &output_path.join("baz").join("5.txt"),
1807 &RmSettings {
1808 fail_early: false,
1809 filter: None,
1810 dry_run: None,
1811 time_filter: None,
1812 },
1813 )
1814 .await?;
1815 assert_eq!(summary.files_removed, 3);
1816 assert_eq!(summary.symlinks_removed, 1);
1817 assert_eq!(summary.directories_removed, 1);
1818 }
1819 {
1820 tokio::fs::symlink("0.txt", &output_path.join("bar")).await?;
1822 tokio::fs::create_dir(&output_path.join("baz").join("5.txt")).await?;
1824 }
1825 let summary = copy(
1826 &PROGRESS,
1827 &tmp_dir.join("foo"),
1828 output_path,
1829 &Settings {
1830 dereference: false,
1831 fail_early: false,
1832 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1834 size: true,
1835 mtime: true,
1836 ..Default::default()
1837 },
1838 overwrite_filter: None,
1839 ignore_existing: false,
1840 chunk_size: 0,
1841 skip_specials: false,
1842 remote_copy_buffer_size: 0,
1843 filter: None,
1844 dry_run: None,
1845 },
1846 &DO_PRESERVE_SETTINGS,
1847 false,
1848 )
1849 .await?;
1850 assert_eq!(summary.rm_summary.files_removed, 0);
1851 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1852 assert_eq!(summary.rm_summary.directories_removed, 1);
1853 assert_eq!(summary.files_copied, 3);
1854 assert_eq!(summary.symlinks_created, 1);
1855 assert_eq!(summary.directories_created, 1);
1856 assert_eq!(summary.files_unchanged, 2);
1857 assert_eq!(summary.symlinks_unchanged, 1);
1858 assert_eq!(summary.directories_unchanged, 2);
1859 testutils::check_dirs_identical(
1860 &tmp_dir.join("foo"),
1861 output_path,
1862 testutils::FileEqualityCheck::Timestamp,
1863 )
1864 .await?;
1865 Ok(())
1866 }
1867
1868 #[tokio::test]
1869 #[traced_test]
1870 async fn test_cp_overwrite_error() -> Result<(), anyhow::Error> {
1871 let tmp_dir = testutils::setup_test_dir().await?;
1872 let test_path = tmp_dir.as_path();
1873 let summary = copy(
1874 &PROGRESS,
1875 &test_path.join("foo"),
1876 &test_path.join("bar"),
1877 &Settings {
1878 dereference: false,
1879 fail_early: false,
1880 overwrite: false,
1881 overwrite_compare: filecmp::MetadataCmpSettings {
1882 size: true,
1883 mtime: true,
1884 ..Default::default()
1885 },
1886 overwrite_filter: None,
1887 ignore_existing: false,
1888 chunk_size: 0,
1889 skip_specials: false,
1890 remote_copy_buffer_size: 0,
1891 filter: None,
1892 dry_run: None,
1893 },
1894 &NO_PRESERVE_SETTINGS, false,
1896 )
1897 .await?;
1898 assert_eq!(summary.files_copied, 5);
1899 assert_eq!(summary.symlinks_created, 2);
1900 assert_eq!(summary.directories_created, 3);
1901 let source_path = &test_path.join("foo");
1902 let output_path = &tmp_dir.join("bar");
1903 tokio::fs::set_permissions(
1905 &source_path.join("bar"),
1906 std::fs::Permissions::from_mode(0o000),
1907 )
1908 .await?;
1909 tokio::fs::set_permissions(
1910 &source_path.join("baz").join("4.txt"),
1911 std::fs::Permissions::from_mode(0o000),
1912 )
1913 .await?;
1914 match copy(
1922 &PROGRESS,
1923 &tmp_dir.join("foo"),
1924 output_path,
1925 &Settings {
1926 dereference: false,
1927 fail_early: false,
1928 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1930 size: true,
1931 mtime: true,
1932 ..Default::default()
1933 },
1934 overwrite_filter: None,
1935 ignore_existing: false,
1936 chunk_size: 0,
1937 skip_specials: false,
1938 remote_copy_buffer_size: 0,
1939 filter: None,
1940 dry_run: None,
1941 },
1942 &DO_PRESERVE_SETTINGS,
1943 false,
1944 )
1945 .await
1946 {
1947 Ok(_) => panic!("Expected the copy to error!"),
1948 Err(error) => {
1949 tracing::info!("{}", &error);
1950 assert_eq!(error.summary.files_copied, 1);
1951 assert_eq!(error.summary.symlinks_created, 2);
1952 assert_eq!(error.summary.directories_created, 0);
1953 assert_eq!(error.summary.rm_summary.files_removed, 2);
1954 assert_eq!(error.summary.rm_summary.symlinks_removed, 2);
1955 assert_eq!(error.summary.rm_summary.directories_removed, 0);
1956 }
1957 }
1958 Ok(())
1959 }
1960
1961 #[tokio::test]
1962 #[traced_test]
1963 async fn overwrite_filter_newer_skips_when_dest_is_newer() -> Result<(), anyhow::Error> {
1964 let tmp_dir = testutils::create_temp_dir().await?;
1965 let test_path = tmp_dir.as_path();
1966 let src_file = test_path.join("src.txt");
1967 let dst_file = test_path.join("dst.txt");
1968 tokio::fs::write(&dst_file, "newer content").await?;
1970 let future_time = filetime::FileTime::from_unix_time(2_000_000_000, 0);
1972 filetime::set_file_mtime(&dst_file, future_time)?;
1973 tokio::fs::write(&src_file, "older content").await?;
1974 let past_time = filetime::FileTime::from_unix_time(1_000_000_000, 0);
1975 filetime::set_file_mtime(&src_file, past_time)?;
1976 let summary = copy_file(
1977 &PROGRESS,
1978 &src_file,
1979 &dst_file,
1980 &tokio::fs::metadata(&src_file).await?,
1981 &Settings {
1982 dereference: false,
1983 fail_early: false,
1984 overwrite: true,
1985 overwrite_compare: filecmp::MetadataCmpSettings {
1986 size: true,
1987 mtime: true,
1988 ..Default::default()
1989 },
1990 overwrite_filter: Some(OverwriteFilter::Newer),
1991 ignore_existing: false,
1992 chunk_size: 0,
1993 skip_specials: false,
1994 remote_copy_buffer_size: 0,
1995 filter: None,
1996 dry_run: None,
1997 },
1998 &NO_PRESERVE_SETTINGS,
1999 false,
2000 )
2001 .await?;
2002 assert_eq!(summary.files_unchanged, 1);
2003 assert_eq!(summary.files_copied, 0);
2004 let content = tokio::fs::read_to_string(&dst_file).await?;
2006 assert_eq!(content, "newer content");
2007 Ok(())
2008 }
2009
2010 #[tokio::test]
2011 #[traced_test]
2012 async fn overwrite_filter_newer_copies_when_dest_is_older() -> Result<(), anyhow::Error> {
2013 let tmp_dir = testutils::create_temp_dir().await?;
2014 let test_path = tmp_dir.as_path();
2015 let src_file = test_path.join("src.txt");
2016 let dst_file = test_path.join("dst.txt");
2017 tokio::fs::write(&dst_file, "old content").await?;
2019 let past_time = filetime::FileTime::from_unix_time(1_000_000_000, 0);
2020 filetime::set_file_mtime(&dst_file, past_time)?;
2021 tokio::fs::write(&src_file, "new content").await?;
2022 let future_time = filetime::FileTime::from_unix_time(2_000_000_000, 0);
2023 filetime::set_file_mtime(&src_file, future_time)?;
2024 let summary = copy_file(
2025 &PROGRESS,
2026 &src_file,
2027 &dst_file,
2028 &tokio::fs::metadata(&src_file).await?,
2029 &Settings {
2030 dereference: false,
2031 fail_early: false,
2032 overwrite: true,
2033 overwrite_compare: filecmp::MetadataCmpSettings {
2034 size: true,
2035 mtime: true,
2036 ..Default::default()
2037 },
2038 overwrite_filter: Some(OverwriteFilter::Newer),
2039 ignore_existing: false,
2040 chunk_size: 0,
2041 skip_specials: false,
2042 remote_copy_buffer_size: 0,
2043 filter: None,
2044 dry_run: None,
2045 },
2046 &NO_PRESERVE_SETTINGS,
2047 false,
2048 )
2049 .await?;
2050 assert_eq!(summary.files_copied, 1);
2051 assert_eq!(summary.files_unchanged, 0);
2052 let content = tokio::fs::read_to_string(&dst_file).await?;
2054 assert_eq!(content, "new content");
2055 Ok(())
2056 }
2057
2058 #[tokio::test]
2059 #[traced_test]
2060 async fn overwrite_filter_newer_copies_when_same_mtime() -> Result<(), anyhow::Error> {
2061 let tmp_dir = testutils::create_temp_dir().await?;
2062 let test_path = tmp_dir.as_path();
2063 let src_file = test_path.join("src.txt");
2064 let dst_file = test_path.join("dst.txt");
2065 tokio::fs::write(&dst_file, "old").await?;
2067 tokio::fs::write(&src_file, "new content").await?;
2068 let same_time = filetime::FileTime::from_unix_time(1_500_000_000, 0);
2069 filetime::set_file_mtime(&dst_file, same_time)?;
2070 filetime::set_file_mtime(&src_file, same_time)?;
2071 let summary = copy_file(
2072 &PROGRESS,
2073 &src_file,
2074 &dst_file,
2075 &tokio::fs::metadata(&src_file).await?,
2076 &Settings {
2077 dereference: false,
2078 fail_early: false,
2079 overwrite: true,
2080 overwrite_compare: filecmp::MetadataCmpSettings {
2081 size: true,
2082 mtime: true,
2083 ..Default::default()
2084 },
2085 overwrite_filter: Some(OverwriteFilter::Newer),
2086 ignore_existing: false,
2087 chunk_size: 0,
2088 skip_specials: false,
2089 remote_copy_buffer_size: 0,
2090 filter: None,
2091 dry_run: None,
2092 },
2093 &NO_PRESERVE_SETTINGS,
2094 false,
2095 )
2096 .await?;
2097 assert_eq!(summary.files_copied, 1);
2099 assert_eq!(summary.files_unchanged, 0);
2100 let content = tokio::fs::read_to_string(&dst_file).await?;
2101 assert_eq!(content, "new content");
2102 Ok(())
2103 }
2104
2105 #[tokio::test]
2106 #[traced_test]
2107 async fn overwrite_without_filter_copies_when_dest_is_newer() -> Result<(), anyhow::Error> {
2108 let tmp_dir = testutils::create_temp_dir().await?;
2109 let test_path = tmp_dir.as_path();
2110 let src_file = test_path.join("src.txt");
2111 let dst_file = test_path.join("dst.txt");
2112 tokio::fs::write(&dst_file, "newer content").await?;
2114 let future_time = filetime::FileTime::from_unix_time(2_000_000_000, 0);
2115 filetime::set_file_mtime(&dst_file, future_time)?;
2116 tokio::fs::write(&src_file, "older content").await?;
2117 let past_time = filetime::FileTime::from_unix_time(1_000_000_000, 0);
2118 filetime::set_file_mtime(&src_file, past_time)?;
2119 let summary = copy_file(
2120 &PROGRESS,
2121 &src_file,
2122 &dst_file,
2123 &tokio::fs::metadata(&src_file).await?,
2124 &Settings {
2125 dereference: false,
2126 fail_early: false,
2127 overwrite: true,
2128 overwrite_compare: filecmp::MetadataCmpSettings {
2129 size: true,
2130 mtime: true,
2131 ..Default::default()
2132 },
2133 overwrite_filter: None,
2134 ignore_existing: false,
2135 chunk_size: 0,
2136 skip_specials: false,
2137 remote_copy_buffer_size: 0,
2138 filter: None,
2139 dry_run: None,
2140 },
2141 &NO_PRESERVE_SETTINGS,
2142 false,
2143 )
2144 .await?;
2145 assert_eq!(summary.files_copied, 1);
2147 let content = tokio::fs::read_to_string(&dst_file).await?;
2148 assert_eq!(content, "older content");
2149 Ok(())
2150 }
2151
2152 #[tokio::test]
2153 #[traced_test]
2154 async fn ignore_existing_skips_when_dest_exists() -> Result<(), anyhow::Error> {
2155 let tmp_dir = testutils::create_temp_dir().await?;
2156 let test_path = tmp_dir.as_path();
2157 let src_file = test_path.join("src.txt");
2158 let dst_file = test_path.join("dst.txt");
2159 tokio::fs::write(&src_file, "source content").await?;
2160 tokio::fs::write(&dst_file, "dest content").await?;
2161 let summary = copy_file(
2162 &PROGRESS,
2163 &src_file,
2164 &dst_file,
2165 &tokio::fs::metadata(&src_file).await?,
2166 &Settings {
2167 dereference: false,
2168 fail_early: false,
2169 overwrite: false,
2170 overwrite_compare: Default::default(),
2171 overwrite_filter: None,
2172 ignore_existing: true,
2173 chunk_size: 0,
2174 skip_specials: false,
2175 remote_copy_buffer_size: 0,
2176 filter: None,
2177 dry_run: None,
2178 },
2179 &NO_PRESERVE_SETTINGS,
2180 false,
2181 )
2182 .await?;
2183 assert_eq!(summary.files_unchanged, 1);
2184 assert_eq!(summary.files_copied, 0);
2185 let content = tokio::fs::read_to_string(&dst_file).await?;
2187 assert_eq!(content, "dest content");
2188 Ok(())
2189 }
2190
2191 #[tokio::test]
2192 #[traced_test]
2193 async fn ignore_existing_skips_when_dest_is_different_type() -> Result<(), anyhow::Error> {
2194 let tmp_dir = testutils::create_temp_dir().await?;
2195 let test_path = tmp_dir.as_path();
2196 let src_file = test_path.join("src.txt");
2197 let dst_dir = test_path.join("dst.txt");
2198 tokio::fs::write(&src_file, "source content").await?;
2199 tokio::fs::create_dir(&dst_dir).await?;
2201 let summary = copy_file(
2202 &PROGRESS,
2203 &src_file,
2204 &dst_dir,
2205 &tokio::fs::metadata(&src_file).await?,
2206 &Settings {
2207 dereference: false,
2208 fail_early: false,
2209 overwrite: false,
2210 overwrite_compare: Default::default(),
2211 overwrite_filter: None,
2212 ignore_existing: true,
2213 chunk_size: 0,
2214 skip_specials: false,
2215 remote_copy_buffer_size: 0,
2216 filter: None,
2217 dry_run: None,
2218 },
2219 &NO_PRESERVE_SETTINGS,
2220 false,
2221 )
2222 .await?;
2223 assert_eq!(summary.files_unchanged, 1);
2224 assert_eq!(summary.files_copied, 0);
2225 assert!(dst_dir.is_dir());
2227 Ok(())
2228 }
2229
2230 #[tokio::test]
2231 #[traced_test]
2232 async fn ignore_existing_copies_when_dest_missing() -> Result<(), anyhow::Error> {
2233 let tmp_dir = testutils::create_temp_dir().await?;
2234 let test_path = tmp_dir.as_path();
2235 let src_file = test_path.join("src.txt");
2236 let dst_file = test_path.join("dst.txt");
2237 tokio::fs::write(&src_file, "source content").await?;
2238 let summary = copy_file(
2239 &PROGRESS,
2240 &src_file,
2241 &dst_file,
2242 &tokio::fs::metadata(&src_file).await?,
2243 &Settings {
2244 dereference: false,
2245 fail_early: false,
2246 overwrite: false,
2247 overwrite_compare: Default::default(),
2248 overwrite_filter: None,
2249 ignore_existing: true,
2250 chunk_size: 0,
2251 skip_specials: false,
2252 remote_copy_buffer_size: 0,
2253 filter: None,
2254 dry_run: None,
2255 },
2256 &NO_PRESERVE_SETTINGS,
2257 false,
2258 )
2259 .await?;
2260 assert_eq!(summary.files_copied, 1);
2261 let content = tokio::fs::read_to_string(&dst_file).await?;
2262 assert_eq!(content, "source content");
2263 Ok(())
2264 }
2265
2266 #[tokio::test]
2267 #[traced_test]
2268 async fn test_cp_dereference_symlink_chain() -> Result<(), anyhow::Error> {
2269 let tmp_dir = testutils::create_temp_dir().await?;
2271 let test_path = tmp_dir.as_path();
2272 let baz_file = test_path.join("baz_file.txt");
2274 tokio::fs::write(&baz_file, "final content").await?;
2275 let bar_link = test_path.join("bar_link");
2276 let foo_link = test_path.join("foo_link");
2277 tokio::fs::symlink(&baz_file, &bar_link).await?;
2279 tokio::fs::symlink(&bar_link, &foo_link).await?;
2280 let src_dir = test_path.join("src_chain");
2282 tokio::fs::create_dir(&src_dir).await?;
2283 tokio::fs::symlink("../foo_link", &src_dir.join("foo")).await?;
2285 tokio::fs::symlink("../bar_link", &src_dir.join("bar")).await?;
2286 tokio::fs::symlink("../baz_file.txt", &src_dir.join("baz")).await?;
2287 let summary = copy(
2289 &PROGRESS,
2290 &src_dir,
2291 &test_path.join("dst_with_deref"),
2292 &Settings {
2293 dereference: true, fail_early: false,
2295 overwrite: false,
2296 overwrite_compare: filecmp::MetadataCmpSettings {
2297 size: true,
2298 mtime: true,
2299 ..Default::default()
2300 },
2301 overwrite_filter: None,
2302 ignore_existing: false,
2303 chunk_size: 0,
2304 skip_specials: false,
2305 remote_copy_buffer_size: 0,
2306 filter: None,
2307 dry_run: None,
2308 },
2309 &NO_PRESERVE_SETTINGS,
2310 false,
2311 )
2312 .await?;
2313 assert_eq!(summary.files_copied, 3); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 1);
2316 let dst_dir = test_path.join("dst_with_deref");
2317 let foo_content = tokio::fs::read_to_string(dst_dir.join("foo")).await?;
2319 let bar_content = tokio::fs::read_to_string(dst_dir.join("bar")).await?;
2320 let baz_content = tokio::fs::read_to_string(dst_dir.join("baz")).await?;
2321 assert_eq!(foo_content, "final content");
2322 assert_eq!(bar_content, "final content");
2323 assert_eq!(baz_content, "final content");
2324 assert!(dst_dir.join("foo").is_file());
2326 assert!(dst_dir.join("bar").is_file());
2327 assert!(dst_dir.join("baz").is_file());
2328 assert!(!dst_dir.join("foo").is_symlink());
2329 assert!(!dst_dir.join("bar").is_symlink());
2330 assert!(!dst_dir.join("baz").is_symlink());
2331 Ok(())
2332 }
2333
2334 #[tokio::test]
2335 #[traced_test]
2336 async fn test_cp_dereference_symlink_to_directory() -> Result<(), anyhow::Error> {
2337 let tmp_dir = testutils::create_temp_dir().await?;
2338 let test_path = tmp_dir.as_path();
2339 let target_dir = test_path.join("target_dir");
2341 tokio::fs::create_dir(&target_dir).await?;
2342 tokio::fs::set_permissions(&target_dir, std::fs::Permissions::from_mode(0o755)).await?;
2343 tokio::fs::write(target_dir.join("file1.txt"), "content1").await?;
2345 tokio::fs::write(target_dir.join("file2.txt"), "content2").await?;
2346 tokio::fs::set_permissions(
2347 &target_dir.join("file1.txt"),
2348 std::fs::Permissions::from_mode(0o644),
2349 )
2350 .await?;
2351 tokio::fs::set_permissions(
2352 &target_dir.join("file2.txt"),
2353 std::fs::Permissions::from_mode(0o600),
2354 )
2355 .await?;
2356 let dir_symlink = test_path.join("dir_symlink");
2358 tokio::fs::symlink(&target_dir, &dir_symlink).await?;
2359 let summary = copy(
2361 &PROGRESS,
2362 &dir_symlink,
2363 &test_path.join("copied_dir"),
2364 &Settings {
2365 dereference: true, fail_early: false,
2367 overwrite: false,
2368 overwrite_compare: filecmp::MetadataCmpSettings {
2369 size: true,
2370 mtime: true,
2371 ..Default::default()
2372 },
2373 overwrite_filter: None,
2374 ignore_existing: false,
2375 chunk_size: 0,
2376 skip_specials: false,
2377 remote_copy_buffer_size: 0,
2378 filter: None,
2379 dry_run: None,
2380 },
2381 &DO_PRESERVE_SETTINGS,
2382 false,
2383 )
2384 .await?;
2385 assert_eq!(summary.files_copied, 2); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 1); let copied_dir = test_path.join("copied_dir");
2389 assert!(copied_dir.is_dir());
2391 assert!(!copied_dir.is_symlink()); let file1_content = tokio::fs::read_to_string(copied_dir.join("file1.txt")).await?;
2394 let file2_content = tokio::fs::read_to_string(copied_dir.join("file2.txt")).await?;
2395 assert_eq!(file1_content, "content1");
2396 assert_eq!(file2_content, "content2");
2397 let copied_dir_metadata = tokio::fs::metadata(&copied_dir).await?;
2399 let file1_metadata = tokio::fs::metadata(copied_dir.join("file1.txt")).await?;
2400 let file2_metadata = tokio::fs::metadata(copied_dir.join("file2.txt")).await?;
2401 assert_eq!(copied_dir_metadata.permissions().mode() & 0o777, 0o755);
2402 assert_eq!(file1_metadata.permissions().mode() & 0o777, 0o644);
2403 assert_eq!(file2_metadata.permissions().mode() & 0o777, 0o600);
2404 Ok(())
2405 }
2406
2407 #[tokio::test]
2408 #[traced_test]
2409 async fn test_cp_dereference_permissions_preserved() -> Result<(), anyhow::Error> {
2410 let tmp_dir = testutils::create_temp_dir().await?;
2411 let test_path = tmp_dir.as_path();
2412 let file1 = test_path.join("file1.txt");
2414 let file2 = test_path.join("file2.txt");
2415 tokio::fs::write(&file1, "content1").await?;
2416 tokio::fs::write(&file2, "content2").await?;
2417 tokio::fs::set_permissions(&file1, std::fs::Permissions::from_mode(0o755)).await?;
2418 tokio::fs::set_permissions(&file2, std::fs::Permissions::from_mode(0o640)).await?;
2419 let symlink1 = test_path.join("symlink1");
2421 let symlink2 = test_path.join("symlink2");
2422 tokio::fs::symlink(&file1, &symlink1).await?;
2423 tokio::fs::symlink(&file2, &symlink2).await?;
2424 let summary1 = copy(
2426 &PROGRESS,
2427 &symlink1,
2428 &test_path.join("copied_file1.txt"),
2429 &Settings {
2430 dereference: true, fail_early: false,
2432 overwrite: false,
2433 overwrite_compare: filecmp::MetadataCmpSettings::default(),
2434 overwrite_filter: None,
2435 ignore_existing: false,
2436 chunk_size: 0,
2437 skip_specials: false,
2438 remote_copy_buffer_size: 0,
2439 filter: None,
2440 dry_run: None,
2441 },
2442 &DO_PRESERVE_SETTINGS, false,
2444 )
2445 .await?;
2446 let summary2 = copy(
2447 &PROGRESS,
2448 &symlink2,
2449 &test_path.join("copied_file2.txt"),
2450 &Settings {
2451 dereference: true,
2452 fail_early: false,
2453 overwrite: false,
2454 overwrite_compare: filecmp::MetadataCmpSettings::default(),
2455 overwrite_filter: None,
2456 ignore_existing: false,
2457 chunk_size: 0,
2458 skip_specials: false,
2459 remote_copy_buffer_size: 0,
2460 filter: None,
2461 dry_run: None,
2462 },
2463 &DO_PRESERVE_SETTINGS,
2464 false,
2465 )
2466 .await?;
2467 assert_eq!(summary1.files_copied, 1);
2468 assert_eq!(summary1.symlinks_created, 0);
2469 assert_eq!(summary2.files_copied, 1);
2470 assert_eq!(summary2.symlinks_created, 0);
2471 let copied1 = test_path.join("copied_file1.txt");
2472 let copied2 = test_path.join("copied_file2.txt");
2473 assert!(copied1.is_file());
2475 assert!(!copied1.is_symlink());
2476 assert!(copied2.is_file());
2477 assert!(!copied2.is_symlink());
2478 let content1 = tokio::fs::read_to_string(&copied1).await?;
2480 let content2 = tokio::fs::read_to_string(&copied2).await?;
2481 assert_eq!(content1, "content1");
2482 assert_eq!(content2, "content2");
2483 let copied1_metadata = tokio::fs::metadata(&copied1).await?;
2485 let copied2_metadata = tokio::fs::metadata(&copied2).await?;
2486 assert_eq!(copied1_metadata.permissions().mode() & 0o777, 0o755);
2487 assert_eq!(copied2_metadata.permissions().mode() & 0o777, 0o640);
2488 Ok(())
2489 }
2490
2491 #[tokio::test]
2492 #[traced_test]
2493 async fn test_cp_dereference_dir() -> Result<(), anyhow::Error> {
2494 let tmp_dir = testutils::setup_test_dir().await?;
2495 tokio::fs::symlink("bar", &tmp_dir.join("foo").join("bar-link")).await?;
2497 tokio::fs::symlink("bar-link", &tmp_dir.join("foo").join("bar-link-link")).await?;
2499 let summary = copy(
2500 &PROGRESS,
2501 &tmp_dir.join("foo"),
2502 &tmp_dir.join("bar"),
2503 &Settings {
2504 dereference: true, fail_early: false,
2506 overwrite: false,
2507 overwrite_compare: filecmp::MetadataCmpSettings {
2508 size: true,
2509 mtime: true,
2510 ..Default::default()
2511 },
2512 overwrite_filter: None,
2513 ignore_existing: false,
2514 chunk_size: 0,
2515 skip_specials: false,
2516 remote_copy_buffer_size: 0,
2517 filter: None,
2518 dry_run: None,
2519 },
2520 &DO_PRESERVE_SETTINGS,
2521 false,
2522 )
2523 .await?;
2524 assert_eq!(summary.files_copied, 13); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 5);
2527 tokio::process::Command::new("cp")
2529 .args(["-r", "-L"])
2530 .arg(tmp_dir.join("foo"))
2531 .arg(tmp_dir.join("bar-cp"))
2532 .output()
2533 .await?;
2534 testutils::check_dirs_identical(
2535 &tmp_dir.join("bar"),
2536 &tmp_dir.join("bar-cp"),
2537 testutils::FileEqualityCheck::Basic,
2538 )
2539 .await?;
2540 Ok(())
2541 }
2542
2543 mod error_message_tests {
2545 use super::*;
2546
2547 fn get_full_error_message(error: &Error) -> String {
2549 format!("{:#}", error.source)
2550 }
2551
2552 #[tokio::test]
2553 #[traced_test]
2554 async fn test_permission_error_includes_root_cause() -> Result<(), anyhow::Error> {
2555 let tmp_dir = testutils::create_temp_dir().await?;
2556 let unreadable = tmp_dir.join("unreadable.txt");
2557 tokio::fs::write(&unreadable, "test").await?;
2558 tokio::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o000)).await?;
2559
2560 let src_metadata = tokio::fs::symlink_metadata(&unreadable).await?;
2562 let result = copy_file(
2563 &PROGRESS,
2564 &unreadable,
2565 &tmp_dir.join("dest.txt"),
2566 &src_metadata,
2567 &Settings {
2568 dereference: false,
2569 fail_early: false,
2570 overwrite: false,
2571 overwrite_compare: Default::default(),
2572 overwrite_filter: None,
2573 ignore_existing: false,
2574 chunk_size: 0,
2575 skip_specials: false,
2576 remote_copy_buffer_size: 0,
2577 filter: None,
2578 dry_run: None,
2579 },
2580 &NO_PRESERVE_SETTINGS,
2581 false,
2582 )
2583 .await;
2584
2585 assert!(result.is_err(), "Should fail with permission error");
2586 let err_msg = get_full_error_message(&result.unwrap_err());
2587
2588 assert!(
2590 err_msg.to_lowercase().contains("permission")
2591 || err_msg.contains("EACCES")
2592 || err_msg.contains("denied"),
2593 "Error message must include permission-related text. Got: {}",
2594 err_msg
2595 );
2596 Ok(())
2597 }
2598
2599 #[tokio::test]
2600 #[traced_test]
2601 async fn test_nonexistent_source_includes_root_cause() -> Result<(), anyhow::Error> {
2602 let tmp_dir = testutils::create_temp_dir().await?;
2603
2604 let result = copy(
2605 &PROGRESS,
2606 &tmp_dir.join("does_not_exist.txt"),
2607 &tmp_dir.join("dest.txt"),
2608 &Settings {
2609 dereference: false,
2610 fail_early: false,
2611 overwrite: false,
2612 overwrite_compare: Default::default(),
2613 overwrite_filter: None,
2614 ignore_existing: false,
2615 chunk_size: 0,
2616 skip_specials: false,
2617 remote_copy_buffer_size: 0,
2618 filter: None,
2619 dry_run: None,
2620 },
2621 &NO_PRESERVE_SETTINGS,
2622 false,
2623 )
2624 .await;
2625
2626 assert!(result.is_err());
2627 let err_msg = get_full_error_message(&result.unwrap_err());
2628
2629 assert!(
2630 err_msg.to_lowercase().contains("no such file")
2631 || err_msg.to_lowercase().contains("not found")
2632 || err_msg.contains("ENOENT"),
2633 "Error message must include file not found text. Got: {}",
2634 err_msg
2635 );
2636 Ok(())
2637 }
2638
2639 #[tokio::test]
2640 #[traced_test]
2641 async fn test_unreadable_directory_includes_root_cause() -> Result<(), anyhow::Error> {
2642 let tmp_dir = testutils::create_temp_dir().await?;
2643 let unreadable_dir = tmp_dir.join("unreadable_dir");
2644 tokio::fs::create_dir(&unreadable_dir).await?;
2645 tokio::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o000))
2646 .await?;
2647
2648 let result = copy(
2649 &PROGRESS,
2650 &unreadable_dir,
2651 &tmp_dir.join("dest"),
2652 &Settings {
2653 dereference: false,
2654 fail_early: true,
2655 overwrite: false,
2656 overwrite_compare: Default::default(),
2657 overwrite_filter: None,
2658 ignore_existing: false,
2659 chunk_size: 0,
2660 skip_specials: false,
2661 remote_copy_buffer_size: 0,
2662 filter: None,
2663 dry_run: None,
2664 },
2665 &NO_PRESERVE_SETTINGS,
2666 false,
2667 )
2668 .await;
2669
2670 assert!(result.is_err());
2671 let err_msg = get_full_error_message(&result.unwrap_err());
2672
2673 assert!(
2674 err_msg.to_lowercase().contains("permission")
2675 || err_msg.contains("EACCES")
2676 || err_msg.contains("denied"),
2677 "Error message must include permission-related text. Got: {}",
2678 err_msg
2679 );
2680
2681 tokio::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o700))
2683 .await?;
2684 Ok(())
2685 }
2686
2687 #[tokio::test]
2688 #[traced_test]
2689 async fn test_destination_permission_error_includes_root_cause() -> Result<(), anyhow::Error>
2690 {
2691 let tmp_dir = testutils::setup_test_dir().await?;
2692 let test_path = tmp_dir.as_path();
2693 let readonly_parent = test_path.join("readonly_dest");
2694 tokio::fs::create_dir(&readonly_parent).await?;
2695 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o555))
2696 .await?;
2697
2698 let result = copy(
2699 &PROGRESS,
2700 &test_path.join("foo"),
2701 &readonly_parent.join("copy"),
2702 &Settings {
2703 dereference: false,
2704 fail_early: true,
2705 overwrite: false,
2706 overwrite_compare: Default::default(),
2707 overwrite_filter: None,
2708 ignore_existing: false,
2709 chunk_size: 0,
2710 skip_specials: false,
2711 remote_copy_buffer_size: 0,
2712 filter: None,
2713 dry_run: None,
2714 },
2715 &NO_PRESERVE_SETTINGS,
2716 false,
2717 )
2718 .await;
2719
2720 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o755))
2722 .await?;
2723
2724 assert!(result.is_err(), "copy into read-only parent should fail");
2725 let err_msg = get_full_error_message(&result.unwrap_err());
2726
2727 assert!(
2728 err_msg.to_lowercase().contains("permission denied") || err_msg.contains("EACCES"),
2729 "Error message must include permission denied text. Got: {}",
2730 err_msg
2731 );
2732 Ok(())
2733 }
2734 }
2735
2736 mod empty_dir_cleanup_tests {
2737 use super::*;
2738 use crate::filter::FilterSettings;
2739 use std::path::Path;
2740 #[test]
2741 fn test_check_empty_dir_cleanup_no_filter() {
2742 assert_eq!(
2744 check_empty_dir_cleanup(None, true, false, Path::new("any"), false, false),
2745 EmptyDirAction::Keep
2746 );
2747 }
2748 #[test]
2749 fn test_check_empty_dir_cleanup_something_copied() {
2750 let mut filter = FilterSettings::new();
2752 filter.add_include("*.txt").unwrap();
2753 assert_eq!(
2754 check_empty_dir_cleanup(Some(&filter), true, true, Path::new("any"), false, false),
2755 EmptyDirAction::Keep
2756 );
2757 }
2758 #[test]
2759 fn test_check_empty_dir_cleanup_not_created() {
2760 let mut filter = FilterSettings::new();
2762 filter.add_include("*.txt").unwrap();
2763 assert_eq!(
2764 check_empty_dir_cleanup(
2765 Some(&filter),
2766 false,
2767 false,
2768 Path::new("any"),
2769 false,
2770 false
2771 ),
2772 EmptyDirAction::Keep
2773 );
2774 }
2775 #[test]
2776 fn test_check_empty_dir_cleanup_directly_matched() {
2777 let mut filter = FilterSettings::new();
2779 filter.add_include("target/").unwrap();
2780 assert_eq!(
2781 check_empty_dir_cleanup(
2782 Some(&filter),
2783 true,
2784 false,
2785 Path::new("target"),
2786 false,
2787 false
2788 ),
2789 EmptyDirAction::Keep
2790 );
2791 }
2792 #[test]
2793 fn test_check_empty_dir_cleanup_traversed_only() {
2794 let mut filter = FilterSettings::new();
2796 filter.add_include("*.txt").unwrap();
2797 assert_eq!(
2798 check_empty_dir_cleanup(Some(&filter), true, false, Path::new("src"), false, false),
2799 EmptyDirAction::Remove
2800 );
2801 }
2802 #[test]
2803 fn test_check_empty_dir_cleanup_dry_run() {
2804 let mut filter = FilterSettings::new();
2806 filter.add_include("*.txt").unwrap();
2807 assert_eq!(
2808 check_empty_dir_cleanup(Some(&filter), true, false, Path::new("src"), false, true),
2809 EmptyDirAction::DryRunSkip
2810 );
2811 }
2812 #[test]
2813 fn test_check_empty_dir_cleanup_root_always_kept() {
2814 let mut filter = FilterSettings::new();
2816 filter.add_include("*.txt").unwrap();
2817 assert_eq!(
2818 check_empty_dir_cleanup(Some(&filter), true, false, Path::new(""), true, false),
2819 EmptyDirAction::Keep
2820 );
2821 }
2822 #[test]
2823 fn test_check_empty_dir_cleanup_root_kept_in_dry_run() {
2824 let mut filter = FilterSettings::new();
2826 filter.add_include("*.txt").unwrap();
2827 assert_eq!(
2828 check_empty_dir_cleanup(Some(&filter), true, false, Path::new(""), true, true),
2829 EmptyDirAction::Keep
2830 );
2831 }
2832 }
2833
2834 #[tokio::test]
2838 #[traced_test]
2839 async fn test_directory_metadata_applied_on_child_error() -> Result<(), anyhow::Error> {
2840 let tmp_dir = testutils::create_temp_dir().await?;
2841 let test_path = tmp_dir.as_path();
2842 let src_dir = test_path.join("src");
2844 tokio::fs::create_dir(&src_dir).await?;
2845 tokio::fs::set_permissions(&src_dir, std::fs::Permissions::from_mode(0o750)).await?;
2846 let readable_file = src_dir.join("readable.txt");
2848 tokio::fs::write(&readable_file, "content").await?;
2849 let unreadable_file = src_dir.join("unreadable.txt");
2850 tokio::fs::write(&unreadable_file, "secret").await?;
2851 tokio::fs::set_permissions(&unreadable_file, std::fs::Permissions::from_mode(0o000))
2852 .await?;
2853 let dst_dir = test_path.join("dst");
2854 let result = copy(
2856 &PROGRESS,
2857 &src_dir,
2858 &dst_dir,
2859 &Settings {
2860 dereference: false,
2861 fail_early: false,
2862 overwrite: false,
2863 overwrite_compare: Default::default(),
2864 overwrite_filter: None,
2865 ignore_existing: false,
2866 chunk_size: 0,
2867 skip_specials: false,
2868 remote_copy_buffer_size: 0,
2869 filter: None,
2870 dry_run: None,
2871 },
2872 &DO_PRESERVE_SETTINGS,
2873 false,
2874 )
2875 .await;
2876 tokio::fs::set_permissions(&unreadable_file, std::fs::Permissions::from_mode(0o644))
2878 .await?;
2879 assert!(result.is_err(), "copy should fail due to unreadable file");
2881 let error = result.unwrap_err();
2882 assert_eq!(error.summary.files_copied, 1);
2884 assert_eq!(error.summary.directories_created, 1);
2885 let dst_metadata = tokio::fs::metadata(&dst_dir).await?;
2887 assert!(dst_metadata.is_dir());
2888 let actual_mode = dst_metadata.permissions().mode() & 0o7777;
2889 assert_eq!(
2890 actual_mode, 0o750,
2891 "directory should have preserved source permissions (0o750), got {:o}",
2892 actual_mode
2893 );
2894 Ok(())
2895 }
2896
2897 #[tokio::test]
2899 #[traced_test]
2900 async fn test_fail_early_does_not_apply_parent_directory_metadata_after_child_error()
2901 -> Result<(), anyhow::Error> {
2902 let tmp_dir = testutils::create_temp_dir().await?;
2903 let test_path = tmp_dir.as_path();
2904 let src_dir = test_path.join("src");
2905 tokio::fs::create_dir(&src_dir).await?;
2906 tokio::fs::write(src_dir.join("readable.txt"), "content").await?;
2907 let unreadable_file = src_dir.join("unreadable.txt");
2908 tokio::fs::write(&unreadable_file, "secret").await?;
2909 tokio::fs::set_permissions(&unreadable_file, std::fs::Permissions::from_mode(0o000))
2910 .await?;
2911 let fixed_secs = 946684800;
2912 let fixed_nsec = 123_456_789;
2913 let fixed_time = nix::sys::time::TimeSpec::new(fixed_secs, fixed_nsec);
2914 nix::sys::stat::utimensat(
2915 nix::fcntl::AT_FDCWD,
2916 &src_dir,
2917 &fixed_time,
2918 &fixed_time,
2919 nix::sys::stat::UtimensatFlags::NoFollowSymlink,
2920 )?;
2921 let src_metadata = tokio::fs::metadata(&src_dir).await?;
2922 let dst_dir = test_path.join("dst");
2923 let result = copy(
2924 &PROGRESS,
2925 &src_dir,
2926 &dst_dir,
2927 &Settings {
2928 dereference: false,
2929 fail_early: true,
2930 overwrite: false,
2931 overwrite_compare: Default::default(),
2932 overwrite_filter: None,
2933 ignore_existing: false,
2934 chunk_size: 0,
2935 skip_specials: false,
2936 remote_copy_buffer_size: 0,
2937 filter: None,
2938 dry_run: None,
2939 },
2940 &DO_PRESERVE_SETTINGS,
2941 false,
2942 )
2943 .await;
2944 tokio::fs::set_permissions(&unreadable_file, std::fs::Permissions::from_mode(0o644))
2945 .await?;
2946 assert!(result.is_err(), "copy should fail due to unreadable file");
2947 let dst_metadata = tokio::fs::metadata(&dst_dir).await?;
2948 assert!(dst_metadata.is_dir());
2949 assert_ne!(
2950 (dst_metadata.mtime(), dst_metadata.mtime_nsec()),
2951 (src_metadata.mtime(), src_metadata.mtime_nsec()),
2952 "fail-early should return before applying preserved directory timestamps"
2953 );
2954 Ok(())
2955 }
2956 mod filter_tests {
2957 use super::*;
2958 use crate::filter::FilterSettings;
2959 #[tokio::test]
2963 #[traced_test]
2964 async fn test_path_pattern_matches_nested_files() -> Result<(), anyhow::Error> {
2965 let tmp_dir = testutils::setup_test_dir().await?;
2966 let test_path = tmp_dir.as_path();
2967 let mut filter = FilterSettings::new();
2979 filter.add_include("bar/*.txt").unwrap();
2980 let summary = copy(
2981 &PROGRESS,
2982 &test_path.join("foo"),
2983 &test_path.join("dst"),
2984 &Settings {
2985 dereference: false,
2986 fail_early: false,
2987 overwrite: false,
2988 overwrite_compare: Default::default(),
2989 overwrite_filter: None,
2990 ignore_existing: false,
2991 chunk_size: 0,
2992 skip_specials: false,
2993 remote_copy_buffer_size: 0,
2994 filter: Some(filter),
2995 dry_run: None,
2996 },
2997 &NO_PRESERVE_SETTINGS,
2998 false,
2999 )
3000 .await?;
3001 assert_eq!(
3004 summary.files_copied, 3,
3005 "should copy 3 files matching bar/*.txt"
3006 );
3007 assert!(
3009 test_path.join("dst/bar/1.txt").exists(),
3010 "bar/1.txt should be copied"
3011 );
3012 assert!(
3013 test_path.join("dst/bar/2.txt").exists(),
3014 "bar/2.txt should be copied"
3015 );
3016 assert!(
3017 test_path.join("dst/bar/3.txt").exists(),
3018 "bar/3.txt should be copied"
3019 );
3020 assert!(
3022 !test_path.join("dst/0.txt").exists(),
3023 "0.txt should not be copied"
3024 );
3025 Ok(())
3026 }
3027 #[tokio::test]
3029 #[traced_test]
3030 async fn test_anchored_pattern_matches_only_at_root() -> Result<(), anyhow::Error> {
3031 let tmp_dir = testutils::setup_test_dir().await?;
3032 let test_path = tmp_dir.as_path();
3033 let mut filter = FilterSettings::new();
3035 filter.add_include("/bar/**").unwrap();
3036 let summary = copy(
3037 &PROGRESS,
3038 &test_path.join("foo"),
3039 &test_path.join("dst"),
3040 &Settings {
3041 dereference: false,
3042 fail_early: false,
3043 overwrite: false,
3044 overwrite_compare: Default::default(),
3045 overwrite_filter: None,
3046 ignore_existing: false,
3047 chunk_size: 0,
3048 skip_specials: false,
3049 remote_copy_buffer_size: 0,
3050 filter: Some(filter),
3051 dry_run: None,
3052 },
3053 &NO_PRESERVE_SETTINGS,
3054 false,
3055 )
3056 .await?;
3057 assert!(
3059 test_path.join("dst/bar").exists(),
3060 "bar directory should be copied"
3061 );
3062 assert!(
3063 !test_path.join("dst/baz").exists(),
3064 "baz directory should not be copied"
3065 );
3066 assert!(
3067 !test_path.join("dst/0.txt").exists(),
3068 "0.txt should not be copied"
3069 );
3070 assert_eq!(
3072 summary.files_copied, 3,
3073 "should copy 3 files in bar (1.txt, 2.txt, 3.txt)"
3074 );
3075 assert_eq!(
3076 summary.directories_created, 2,
3077 "should create 2 directories (root dst + bar)"
3078 );
3079 assert_eq!(summary.files_skipped, 1, "should skip 1 file (0.txt)");
3081 assert_eq!(
3082 summary.directories_skipped, 1,
3083 "should skip 1 directory (baz)"
3084 );
3085 Ok(())
3086 }
3087 #[tokio::test]
3089 #[traced_test]
3090 async fn test_double_star_pattern_matches_nested() -> Result<(), anyhow::Error> {
3091 let tmp_dir = testutils::setup_test_dir().await?;
3092 let test_path = tmp_dir.as_path();
3093 let mut filter = FilterSettings::new();
3095 filter.add_include("**/*.txt").unwrap();
3096 let summary = copy(
3097 &PROGRESS,
3098 &test_path.join("foo"),
3099 &test_path.join("dst"),
3100 &Settings {
3101 dereference: false,
3102 fail_early: false,
3103 overwrite: false,
3104 overwrite_compare: Default::default(),
3105 overwrite_filter: None,
3106 ignore_existing: false,
3107 chunk_size: 0,
3108 skip_specials: false,
3109 remote_copy_buffer_size: 0,
3110 filter: Some(filter),
3111 dry_run: None,
3112 },
3113 &NO_PRESERVE_SETTINGS,
3114 false,
3115 )
3116 .await?;
3117 assert_eq!(
3119 summary.files_copied, 5,
3120 "should copy all 5 .txt files with **/*.txt pattern"
3121 );
3122 Ok(())
3123 }
3124 #[tokio::test]
3126 #[traced_test]
3127 async fn test_filter_applies_to_single_file_source() -> Result<(), anyhow::Error> {
3128 let tmp_dir = testutils::setup_test_dir().await?;
3129 let test_path = tmp_dir.as_path();
3130 let mut filter = FilterSettings::new();
3132 filter.add_exclude("*.txt").unwrap();
3133 let result = copy(
3134 &PROGRESS,
3135 &test_path.join("foo/0.txt"), &test_path.join("dst.txt"),
3137 &Settings {
3138 dereference: false,
3139 fail_early: false,
3140 overwrite: false,
3141 overwrite_compare: Default::default(),
3142 overwrite_filter: None,
3143 ignore_existing: false,
3144 chunk_size: 0,
3145 skip_specials: false,
3146 remote_copy_buffer_size: 0,
3147 filter: Some(filter),
3148 dry_run: None,
3149 },
3150 &NO_PRESERVE_SETTINGS,
3151 false,
3152 )
3153 .await?;
3154 assert_eq!(
3156 result.files_copied, 0,
3157 "file matching exclude pattern should not be copied"
3158 );
3159 assert!(
3160 !test_path.join("dst.txt").exists(),
3161 "excluded file should not exist at destination"
3162 );
3163 Ok(())
3164 }
3165 #[tokio::test]
3167 #[traced_test]
3168 async fn test_filter_applies_to_root_directory() -> Result<(), anyhow::Error> {
3169 let test_path = testutils::create_temp_dir().await?;
3170 tokio::fs::create_dir_all(test_path.join("excluded_dir")).await?;
3172 tokio::fs::write(test_path.join("excluded_dir/file.txt"), "content").await?;
3173 let mut filter = FilterSettings::new();
3175 filter.add_exclude("*_dir/").unwrap();
3176 let result = copy(
3177 &PROGRESS,
3178 &test_path.join("excluded_dir"),
3179 &test_path.join("dst"),
3180 &Settings {
3181 dereference: false,
3182 fail_early: false,
3183 overwrite: false,
3184 overwrite_compare: Default::default(),
3185 overwrite_filter: None,
3186 ignore_existing: false,
3187 chunk_size: 0,
3188 skip_specials: false,
3189 remote_copy_buffer_size: 0,
3190 filter: Some(filter),
3191 dry_run: None,
3192 },
3193 &NO_PRESERVE_SETTINGS,
3194 false,
3195 )
3196 .await?;
3197 assert_eq!(
3199 result.directories_created, 0,
3200 "root directory matching exclude should not be created"
3201 );
3202 assert!(
3203 !test_path.join("dst").exists(),
3204 "excluded root directory should not exist at destination"
3205 );
3206 Ok(())
3207 }
3208 #[tokio::test]
3210 #[traced_test]
3211 async fn test_filter_applies_to_root_symlink() -> Result<(), anyhow::Error> {
3212 let test_path = testutils::create_temp_dir().await?;
3213 tokio::fs::write(test_path.join("target.txt"), "content").await?;
3215 tokio::fs::symlink(
3216 test_path.join("target.txt"),
3217 test_path.join("excluded_link"),
3218 )
3219 .await?;
3220 let mut filter = FilterSettings::new();
3222 filter.add_exclude("*_link").unwrap();
3223 let result = copy(
3224 &PROGRESS,
3225 &test_path.join("excluded_link"),
3226 &test_path.join("dst"),
3227 &Settings {
3228 dereference: false,
3229 fail_early: false,
3230 overwrite: false,
3231 overwrite_compare: Default::default(),
3232 overwrite_filter: None,
3233 ignore_existing: false,
3234 chunk_size: 0,
3235 skip_specials: false,
3236 remote_copy_buffer_size: 0,
3237 filter: Some(filter),
3238 dry_run: None,
3239 },
3240 &NO_PRESERVE_SETTINGS,
3241 false,
3242 )
3243 .await?;
3244 assert_eq!(
3246 result.symlinks_created, 0,
3247 "root symlink matching exclude should not be created"
3248 );
3249 assert!(
3250 !test_path.join("dst").exists(),
3251 "excluded root symlink should not exist at destination"
3252 );
3253 Ok(())
3254 }
3255 #[tokio::test]
3257 #[traced_test]
3258 async fn test_combined_include_exclude_patterns() -> Result<(), anyhow::Error> {
3259 let tmp_dir = testutils::setup_test_dir().await?;
3260 let test_path = tmp_dir.as_path();
3261 let mut filter = FilterSettings::new();
3268 filter.add_include("**/*.txt").unwrap();
3269 filter.add_exclude("bar/2.txt").unwrap();
3270 let summary = copy(
3271 &PROGRESS,
3272 &test_path.join("foo"),
3273 &test_path.join("dst"),
3274 &Settings {
3275 dereference: false,
3276 fail_early: false,
3277 overwrite: false,
3278 overwrite_compare: Default::default(),
3279 overwrite_filter: None,
3280 ignore_existing: false,
3281 chunk_size: 0,
3282 skip_specials: false,
3283 remote_copy_buffer_size: 0,
3284 filter: Some(filter),
3285 dry_run: None,
3286 },
3287 &NO_PRESERVE_SETTINGS,
3288 false,
3289 )
3290 .await?;
3291 assert_eq!(summary.files_copied, 4, "should copy 4 .txt files");
3295 assert_eq!(
3296 summary.files_skipped, 1,
3297 "should skip 1 file (bar/2.txt excluded)"
3298 );
3299 assert!(
3301 test_path.join("dst/bar/1.txt").exists(),
3302 "bar/1.txt should be copied"
3303 );
3304 assert!(
3305 !test_path.join("dst/bar/2.txt").exists(),
3306 "bar/2.txt should be excluded"
3307 );
3308 assert!(
3309 test_path.join("dst/bar/3.txt").exists(),
3310 "bar/3.txt should be copied"
3311 );
3312 Ok(())
3313 }
3314 #[tokio::test]
3316 #[traced_test]
3317 async fn test_skipped_counts_comprehensive() -> Result<(), anyhow::Error> {
3318 let tmp_dir = testutils::setup_test_dir().await?;
3319 let test_path = tmp_dir.as_path();
3320 let mut filter = FilterSettings::new();
3327 filter.add_exclude("bar/").unwrap();
3328 let summary = copy(
3329 &PROGRESS,
3330 &test_path.join("foo"),
3331 &test_path.join("dst"),
3332 &Settings {
3333 dereference: false,
3334 fail_early: false,
3335 overwrite: false,
3336 overwrite_compare: Default::default(),
3337 overwrite_filter: None,
3338 ignore_existing: false,
3339 chunk_size: 0,
3340 skip_specials: false,
3341 remote_copy_buffer_size: 0,
3342 filter: Some(filter),
3343 dry_run: None,
3344 },
3345 &NO_PRESERVE_SETTINGS,
3346 false,
3347 )
3348 .await?;
3349 assert_eq!(summary.files_copied, 2, "should copy 2 files");
3353 assert_eq!(summary.symlinks_created, 2, "should copy 2 symlinks");
3354 assert_eq!(
3355 summary.directories_created, 2,
3356 "should create 2 directories"
3357 );
3358 assert_eq!(
3359 summary.directories_skipped, 1,
3360 "should skip 1 directory (bar)"
3361 );
3362 assert_eq!(
3363 summary.files_skipped, 0,
3364 "no files skipped (bar contents not counted)"
3365 );
3366 Ok(())
3367 }
3368 #[tokio::test]
3371 #[traced_test]
3372 async fn test_empty_dir_not_created_when_only_traversed() -> Result<(), anyhow::Error> {
3373 let test_path = testutils::create_temp_dir().await?;
3374 let src_path = test_path.join("src");
3380 tokio::fs::create_dir(&src_path).await?;
3381 tokio::fs::write(src_path.join("foo"), "content").await?;
3382 tokio::fs::write(src_path.join("bar"), "content").await?;
3383 tokio::fs::create_dir(src_path.join("baz")).await?;
3384 let mut filter = FilterSettings::new();
3386 filter.add_include("foo").unwrap();
3387 let summary = copy(
3388 &PROGRESS,
3389 &src_path,
3390 &test_path.join("dst"),
3391 &Settings {
3392 dereference: false,
3393 fail_early: false,
3394 overwrite: false,
3395 overwrite_compare: Default::default(),
3396 overwrite_filter: None,
3397 ignore_existing: false,
3398 chunk_size: 0,
3399 skip_specials: false,
3400 remote_copy_buffer_size: 0,
3401 filter: Some(filter),
3402 dry_run: None,
3403 },
3404 &NO_PRESERVE_SETTINGS,
3405 false,
3406 )
3407 .await?;
3408 assert_eq!(summary.files_copied, 1, "should copy only 'foo' file");
3410 assert_eq!(
3411 summary.directories_created, 1,
3412 "should create only root directory (not empty 'baz')"
3413 );
3414 assert!(
3416 test_path.join("dst").join("foo").exists(),
3417 "foo should be copied"
3418 );
3419 assert!(
3421 !test_path.join("dst").join("bar").exists(),
3422 "bar should not be copied"
3423 );
3424 assert!(
3426 !test_path.join("dst").join("baz").exists(),
3427 "empty baz directory should NOT be created"
3428 );
3429 Ok(())
3430 }
3431 #[tokio::test]
3434 #[traced_test]
3435 async fn test_dir_with_nonmatching_content_not_created() -> Result<(), anyhow::Error> {
3436 let test_path = testutils::create_temp_dir().await?;
3437 let src_path = test_path.join("src");
3444 tokio::fs::create_dir(&src_path).await?;
3445 tokio::fs::write(src_path.join("foo"), "content").await?;
3446 tokio::fs::create_dir(src_path.join("baz")).await?;
3447 tokio::fs::write(src_path.join("baz").join("qux"), "content").await?;
3448 tokio::fs::write(src_path.join("baz").join("quux"), "content").await?;
3449 let mut filter = FilterSettings::new();
3451 filter.add_include("foo").unwrap();
3452 let summary = copy(
3453 &PROGRESS,
3454 &src_path,
3455 &test_path.join("dst"),
3456 &Settings {
3457 dereference: false,
3458 fail_early: false,
3459 overwrite: false,
3460 overwrite_compare: Default::default(),
3461 overwrite_filter: None,
3462 ignore_existing: false,
3463 chunk_size: 0,
3464 skip_specials: false,
3465 remote_copy_buffer_size: 0,
3466 filter: Some(filter),
3467 dry_run: None,
3468 },
3469 &NO_PRESERVE_SETTINGS,
3470 false,
3471 )
3472 .await?;
3473 assert_eq!(summary.files_copied, 1, "should copy only 'foo' file");
3475 assert_eq!(
3476 summary.files_skipped, 2,
3477 "should skip 2 files (qux and quux)"
3478 );
3479 assert_eq!(
3480 summary.directories_created, 1,
3481 "should create only root directory (not 'baz' with non-matching content)"
3482 );
3483 assert!(
3485 test_path.join("dst").join("foo").exists(),
3486 "foo should be copied"
3487 );
3488 assert!(
3490 !test_path.join("dst").join("baz").exists(),
3491 "baz directory should NOT be created (no matching content inside)"
3492 );
3493 Ok(())
3494 }
3495 #[tokio::test]
3498 #[traced_test]
3499 async fn test_dry_run_empty_dir_not_reported_as_created() -> Result<(), anyhow::Error> {
3500 let test_path = testutils::create_temp_dir().await?;
3501 let src_path = test_path.join("src");
3507 tokio::fs::create_dir(&src_path).await?;
3508 tokio::fs::write(src_path.join("foo"), "content").await?;
3509 tokio::fs::write(src_path.join("bar"), "content").await?;
3510 tokio::fs::create_dir(src_path.join("baz")).await?;
3511 let mut filter = FilterSettings::new();
3513 filter.add_include("foo").unwrap();
3514 let summary = copy(
3515 &PROGRESS,
3516 &src_path,
3517 &test_path.join("dst"),
3518 &Settings {
3519 dereference: false,
3520 fail_early: false,
3521 overwrite: false,
3522 overwrite_compare: Default::default(),
3523 overwrite_filter: None,
3524 ignore_existing: false,
3525 chunk_size: 0,
3526 skip_specials: false,
3527 remote_copy_buffer_size: 0,
3528 filter: Some(filter),
3529 dry_run: Some(crate::config::DryRunMode::Explain),
3530 },
3531 &NO_PRESERVE_SETTINGS,
3532 false,
3533 )
3534 .await?;
3535 assert_eq!(
3537 summary.files_copied, 1,
3538 "should report only 'foo' would be copied"
3539 );
3540 assert_eq!(
3541 summary.directories_created, 1,
3542 "should report only root directory would be created (not empty 'baz')"
3543 );
3544 assert!(
3546 !test_path.join("dst").exists(),
3547 "dst should not exist in dry-run"
3548 );
3549 Ok(())
3550 }
3551 #[tokio::test]
3554 #[traced_test]
3555 async fn test_existing_dir_not_removed_with_overwrite() -> Result<(), anyhow::Error> {
3556 let test_path = testutils::create_temp_dir().await?;
3557 let src_path = test_path.join("src");
3563 tokio::fs::create_dir(&src_path).await?;
3564 tokio::fs::write(src_path.join("foo"), "content").await?;
3565 tokio::fs::write(src_path.join("bar"), "content").await?;
3566 tokio::fs::create_dir(src_path.join("baz")).await?;
3567 let dst_path = test_path.join("dst");
3569 tokio::fs::create_dir(&dst_path).await?;
3570 tokio::fs::create_dir(dst_path.join("baz")).await?;
3571 tokio::fs::write(dst_path.join("baz").join("marker.txt"), "existing").await?;
3573 let mut filter = FilterSettings::new();
3575 filter.add_include("foo").unwrap();
3576 let summary = copy(
3577 &PROGRESS,
3578 &src_path,
3579 &dst_path,
3580 &Settings {
3581 dereference: false,
3582 fail_early: false,
3583 overwrite: true, overwrite_compare: Default::default(),
3585 overwrite_filter: None,
3586 ignore_existing: false,
3587 chunk_size: 0,
3588 skip_specials: false,
3589 remote_copy_buffer_size: 0,
3590 filter: Some(filter),
3591 dry_run: None,
3592 },
3593 &NO_PRESERVE_SETTINGS,
3594 false,
3595 )
3596 .await?;
3597 assert_eq!(summary.files_copied, 1, "should copy only 'foo' file");
3599 assert_eq!(
3601 summary.directories_unchanged, 2,
3602 "root dst and baz directories should be unchanged"
3603 );
3604 assert_eq!(
3605 summary.directories_created, 0,
3606 "should not create any directories"
3607 );
3608 assert!(dst_path.join("foo").exists(), "foo should be copied");
3610 assert!(!dst_path.join("bar").exists(), "bar should not be copied");
3612 assert!(
3614 dst_path.join("baz").exists(),
3615 "existing baz directory should still exist"
3616 );
3617 assert!(
3618 dst_path.join("baz").join("marker.txt").exists(),
3619 "existing content in baz should still exist"
3620 );
3621 Ok(())
3622 }
3623 }
3624 mod dry_run_tests {
3625 use super::*;
3626 use crate::filter::FilterSettings;
3627 #[tokio::test]
3630 #[traced_test]
3631 async fn test_dry_run_directory_does_not_create_destination() -> Result<(), anyhow::Error> {
3632 let tmp_dir = testutils::setup_test_dir().await?;
3633 let test_path = tmp_dir.as_path();
3634 let dst_path = test_path.join("nonexistent_dst");
3635 assert!(
3637 !dst_path.exists(),
3638 "destination should not exist before dry-run"
3639 );
3640 let summary = copy(
3641 &PROGRESS,
3642 &test_path.join("foo"),
3643 &dst_path,
3644 &Settings {
3645 dereference: false,
3646 fail_early: false,
3647 overwrite: false,
3648 overwrite_compare: Default::default(),
3649 overwrite_filter: None,
3650 ignore_existing: false,
3651 chunk_size: 0,
3652 skip_specials: false,
3653 remote_copy_buffer_size: 0,
3654 filter: None,
3655 dry_run: Some(crate::config::DryRunMode::Brief),
3656 },
3657 &NO_PRESERVE_SETTINGS,
3658 false,
3659 )
3660 .await?;
3661 assert!(
3663 !dst_path.exists(),
3664 "dry-run should not create destination directory"
3665 );
3666 assert!(
3668 summary.directories_created > 0,
3669 "dry-run should report directories that would be created"
3670 );
3671 assert!(
3672 summary.files_copied > 0,
3673 "dry-run should report files that would be copied"
3674 );
3675 Ok(())
3676 }
3677 #[tokio::test]
3681 #[traced_test]
3682 async fn test_root_dir_preserved_when_nothing_matches() -> Result<(), anyhow::Error> {
3683 let test_path = testutils::create_temp_dir().await?;
3684 let src_path = test_path.join("src");
3689 tokio::fs::create_dir(&src_path).await?;
3690 tokio::fs::write(src_path.join("bar.log"), "content").await?;
3691 tokio::fs::create_dir(src_path.join("baz")).await?;
3692 let mut filter = FilterSettings::new();
3694 filter.add_include("*.txt").unwrap();
3695 let dst_path = test_path.join("dst");
3696 let summary = copy(
3697 &PROGRESS,
3698 &src_path,
3699 &dst_path,
3700 &Settings {
3701 dereference: false,
3702 fail_early: false,
3703 overwrite: false,
3704 overwrite_compare: Default::default(),
3705 overwrite_filter: None,
3706 ignore_existing: false,
3707 chunk_size: 0,
3708 skip_specials: false,
3709 remote_copy_buffer_size: 0,
3710 filter: Some(filter),
3711 dry_run: None,
3712 },
3713 &NO_PRESERVE_SETTINGS,
3714 false,
3715 )
3716 .await?;
3717 assert_eq!(summary.files_copied, 0, "no files match *.txt");
3719 assert_eq!(
3721 summary.directories_created, 1,
3722 "root directory should always be created"
3723 );
3724 assert!(dst_path.exists(), "root destination directory should exist");
3725 assert!(
3727 !dst_path.join("baz").exists(),
3728 "empty baz should not be created"
3729 );
3730 Ok(())
3731 }
3732 #[tokio::test]
3734 #[traced_test]
3735 async fn test_root_dir_counted_in_dry_run_when_nothing_matches() -> Result<(), anyhow::Error>
3736 {
3737 let test_path = testutils::create_temp_dir().await?;
3738 let src_path = test_path.join("src");
3739 tokio::fs::create_dir(&src_path).await?;
3740 tokio::fs::write(src_path.join("bar.log"), "content").await?;
3741 let mut filter = FilterSettings::new();
3743 filter.add_include("*.txt").unwrap();
3744 let dst_path = test_path.join("dst");
3745 let summary = copy(
3746 &PROGRESS,
3747 &src_path,
3748 &dst_path,
3749 &Settings {
3750 dereference: false,
3751 fail_early: false,
3752 overwrite: false,
3753 overwrite_compare: Default::default(),
3754 overwrite_filter: None,
3755 ignore_existing: false,
3756 chunk_size: 0,
3757 skip_specials: false,
3758 remote_copy_buffer_size: 0,
3759 filter: Some(filter),
3760 dry_run: Some(crate::config::DryRunMode::Explain),
3761 },
3762 &NO_PRESERVE_SETTINGS,
3763 false,
3764 )
3765 .await?;
3766 assert_eq!(summary.files_copied, 0, "no files match *.txt");
3767 assert_eq!(
3768 summary.directories_created, 1,
3769 "root directory should be counted in dry-run"
3770 );
3771 assert!(
3772 !dst_path.exists(),
3773 "nothing should be created in dry-run mode"
3774 );
3775 Ok(())
3776 }
3777 }
3778
3779 mod max_open_files_tests {
3781 use super::*;
3782
3783 #[tokio::test]
3786 #[traced_test]
3787 async fn wide_copy_under_open_files_saturation() -> Result<(), anyhow::Error> {
3788 let tmp_dir = testutils::create_temp_dir().await?;
3789 let src = tmp_dir.join("src");
3790 let dst = tmp_dir.join("dst");
3791 tokio::fs::create_dir(&src).await?;
3792 let file_count = 200;
3793 for i in 0..file_count {
3794 tokio::fs::write(src.join(format!("{}.txt", i)), format!("content-{}", i)).await?;
3795 }
3796 throttle::set_max_open_files(4);
3798 let summary = copy(
3799 &PROGRESS,
3800 &src,
3801 &dst,
3802 &Settings {
3803 dereference: false,
3804 fail_early: true,
3805 overwrite: false,
3806 overwrite_compare: Default::default(),
3807 overwrite_filter: None,
3808 ignore_existing: false,
3809 chunk_size: 0,
3810 skip_specials: false,
3811 remote_copy_buffer_size: 0,
3812 filter: None,
3813 dry_run: None,
3814 },
3815 &NO_PRESERVE_SETTINGS,
3816 false,
3817 )
3818 .await?;
3819 assert_eq!(summary.files_copied, file_count);
3820 assert_eq!(summary.directories_created, 1);
3821 for i in 0..file_count {
3822 let content = tokio::fs::read_to_string(dst.join(format!("{}.txt", i))).await?;
3823 assert_eq!(content, format!("content-{}", i));
3824 }
3825 Ok(())
3826 }
3827
3828 #[tokio::test]
3831 #[traced_test]
3832 async fn deep_tree_no_deadlock_under_open_files_saturation() -> Result<(), anyhow::Error> {
3833 let tmp_dir = testutils::create_temp_dir().await?;
3834 let src = tmp_dir.join("src");
3835 let dst = tmp_dir.join("dst");
3836 let depth = 20;
3837 let files_per_level = 5;
3838 let limit = 4;
3839 let mut dir = src.clone();
3841 for level in 0..depth {
3842 tokio::fs::create_dir_all(&dir).await?;
3843 for f in 0..files_per_level {
3844 tokio::fs::write(
3845 dir.join(format!("f{}_{}.txt", level, f)),
3846 format!("L{}F{}", level, f),
3847 )
3848 .await?;
3849 }
3850 dir = dir.join(format!("d{}", level));
3851 }
3852 throttle::set_max_open_files(limit);
3853 let summary = tokio::time::timeout(
3854 std::time::Duration::from_secs(30),
3855 copy(
3856 &PROGRESS,
3857 &src,
3858 &dst,
3859 &Settings {
3860 dereference: false,
3861 fail_early: true,
3862 overwrite: false,
3863 overwrite_compare: Default::default(),
3864 overwrite_filter: None,
3865 ignore_existing: false,
3866 chunk_size: 0,
3867 skip_specials: false,
3868 remote_copy_buffer_size: 0,
3869 filter: None,
3870 dry_run: None,
3871 },
3872 &NO_PRESERVE_SETTINGS,
3873 false,
3874 ),
3875 )
3876 .await
3877 .context("copy timed out — possible deadlock")?
3878 .context("copy failed")?;
3879 assert_eq!(summary.files_copied, depth * files_per_level);
3880 assert_eq!(summary.directories_created, depth);
3881 let mut check_dir = dst.clone();
3883 for level in 0..depth {
3884 let content =
3885 tokio::fs::read_to_string(check_dir.join(format!("f{}_0.txt", level))).await?;
3886 assert_eq!(content, format!("L{}F0", level));
3887 check_dir = check_dir.join(format!("d{}", level));
3888 }
3889 Ok(())
3890 }
3891
3892 #[tokio::test]
3902 #[traced_test]
3903 async fn parallel_overwrite_dir_with_file_no_deadlock() -> Result<(), anyhow::Error> {
3904 let tmp_dir = testutils::create_temp_dir().await?;
3905 let src = tmp_dir.join("src");
3906 let dst = tmp_dir.join("dst");
3907 tokio::fs::create_dir(&src).await?;
3908 tokio::fs::create_dir(&dst).await?;
3909 let n = 8;
3913 for i in 0..n {
3914 tokio::fs::write(src.join(format!("e{}", i)), format!("file-{}", i)).await?;
3915 let dst_subdir = dst.join(format!("e{}", i));
3916 tokio::fs::create_dir(&dst_subdir).await?;
3917 for j in 0..3 {
3918 tokio::fs::write(
3919 dst_subdir.join(format!("inner_{}.txt", j)),
3920 format!("inner-{}-{}", i, j),
3921 )
3922 .await?;
3923 }
3924 }
3925 throttle::set_max_open_files(2);
3929 let summary = tokio::time::timeout(
3930 std::time::Duration::from_secs(30),
3931 copy(
3932 &PROGRESS,
3933 &src,
3934 &dst,
3935 &Settings {
3936 dereference: false,
3937 fail_early: true,
3938 overwrite: true,
3939 overwrite_compare: Default::default(),
3940 overwrite_filter: None,
3941 ignore_existing: false,
3942 chunk_size: 0,
3943 skip_specials: false,
3944 remote_copy_buffer_size: 0,
3945 filter: None,
3946 dry_run: None,
3947 },
3948 &NO_PRESERVE_SETTINGS,
3949 false,
3950 ),
3951 )
3952 .await
3953 .context(
3954 "copy timed out — deadlock between copy_file's open-files permit and inner rm",
3955 )?
3956 .context("copy failed")?;
3957 assert_eq!(summary.files_copied, n);
3958 assert_eq!(summary.rm_summary.files_removed, n * 3);
3959 assert_eq!(summary.rm_summary.directories_removed, n);
3960 for i in 0..n {
3961 let path = dst.join(format!("e{}", i));
3962 let content = tokio::fs::read_to_string(&path).await?;
3963 assert_eq!(content, format!("file-{}", i));
3964 }
3965 Ok(())
3966 }
3967 }
3968
3969 mod skip_specials_tests {
3970 use super::*;
3971
3972 #[tokio::test]
3973 #[traced_test]
3974 async fn skip_specials_skips_socket_in_directory() -> Result<(), anyhow::Error> {
3975 let tmp_dir = testutils::setup_test_dir().await?;
3976 let test_path = tmp_dir.as_path();
3977 let src = test_path.join("src_dir");
3978 let dst = test_path.join("dst_dir");
3979 tokio::fs::create_dir(&src).await?;
3980 tokio::fs::write(src.join("file.txt"), "hello").await?;
3981 let _listener = std::os::unix::net::UnixListener::bind(src.join("test.sock"))?;
3983 let summary = copy(
3984 &PROGRESS,
3985 &src,
3986 &dst,
3987 &Settings {
3988 dereference: false,
3989 fail_early: false,
3990 overwrite: false,
3991 overwrite_compare: Default::default(),
3992 overwrite_filter: None,
3993 ignore_existing: false,
3994 chunk_size: 0,
3995 skip_specials: true,
3996 remote_copy_buffer_size: 0,
3997 filter: None,
3998 dry_run: None,
3999 },
4000 &NO_PRESERVE_SETTINGS,
4001 false,
4002 )
4003 .await?;
4004 assert_eq!(summary.files_copied, 1);
4005 assert_eq!(summary.specials_skipped, 1);
4006 assert!(dst.join("file.txt").exists());
4007 assert!(!dst.join("test.sock").exists());
4008 Ok(())
4009 }
4010
4011 #[tokio::test]
4012 #[traced_test]
4013 async fn skip_specials_skips_fifo_in_directory() -> Result<(), anyhow::Error> {
4014 let tmp_dir = testutils::setup_test_dir().await?;
4015 let test_path = tmp_dir.as_path();
4016 let src = test_path.join("src_dir");
4017 let dst = test_path.join("dst_dir");
4018 tokio::fs::create_dir(&src).await?;
4019 tokio::fs::write(src.join("file.txt"), "hello").await?;
4020 nix::unistd::mkfifo(
4022 &src.join("test.fifo"),
4023 nix::sys::stat::Mode::S_IRUSR | nix::sys::stat::Mode::S_IWUSR,
4024 )?;
4025 let summary = copy(
4026 &PROGRESS,
4027 &src,
4028 &dst,
4029 &Settings {
4030 dereference: false,
4031 fail_early: false,
4032 overwrite: false,
4033 overwrite_compare: Default::default(),
4034 overwrite_filter: None,
4035 ignore_existing: false,
4036 chunk_size: 0,
4037 skip_specials: true,
4038 remote_copy_buffer_size: 0,
4039 filter: None,
4040 dry_run: None,
4041 },
4042 &NO_PRESERVE_SETTINGS,
4043 false,
4044 )
4045 .await?;
4046 assert_eq!(summary.files_copied, 1);
4047 assert_eq!(summary.specials_skipped, 1);
4048 assert!(dst.join("file.txt").exists());
4049 assert!(!dst.join("test.fifo").exists());
4050 Ok(())
4051 }
4052
4053 #[tokio::test]
4054 #[traced_test]
4055 async fn special_file_errors_without_skip_specials() -> Result<(), anyhow::Error> {
4056 let tmp_dir = testutils::setup_test_dir().await?;
4057 let test_path = tmp_dir.as_path();
4058 let src = test_path.join("src_dir");
4059 let dst = test_path.join("dst_dir");
4060 tokio::fs::create_dir(&src).await?;
4061 tokio::fs::write(src.join("file.txt"), "hello").await?;
4062 let _listener = std::os::unix::net::UnixListener::bind(src.join("test.sock"))?;
4063 let result = copy(
4064 &PROGRESS,
4065 &src,
4066 &dst,
4067 &Settings {
4068 dereference: false,
4069 fail_early: false,
4070 overwrite: false,
4071 overwrite_compare: Default::default(),
4072 overwrite_filter: None,
4073 ignore_existing: false,
4074 chunk_size: 0,
4075 skip_specials: false,
4076 remote_copy_buffer_size: 0,
4077 filter: None,
4078 dry_run: None,
4079 },
4080 &NO_PRESERVE_SETTINGS,
4081 false,
4082 )
4083 .await;
4084 assert!(result.is_err());
4085 let err = result.unwrap_err();
4086 assert!(
4087 format!("{:#}", err).contains("unsupported src file type"),
4088 "error should mention unsupported file type, got: {:#}",
4089 err
4090 );
4091 Ok(())
4092 }
4093
4094 #[tokio::test]
4095 #[traced_test]
4096 async fn skip_specials_top_level_socket() -> Result<(), anyhow::Error> {
4097 let tmp_dir = testutils::setup_test_dir().await?;
4098 let test_path = tmp_dir.as_path();
4099 let src_socket = test_path.join("test.sock");
4100 let dst = test_path.join("dst.sock");
4101 let _listener = std::os::unix::net::UnixListener::bind(&src_socket)?;
4102 let summary = copy(
4103 &PROGRESS,
4104 &src_socket,
4105 &dst,
4106 &Settings {
4107 dereference: false,
4108 fail_early: false,
4109 overwrite: false,
4110 overwrite_compare: Default::default(),
4111 overwrite_filter: None,
4112 ignore_existing: false,
4113 chunk_size: 0,
4114 skip_specials: true,
4115 remote_copy_buffer_size: 0,
4116 filter: None,
4117 dry_run: None,
4118 },
4119 &NO_PRESERVE_SETTINGS,
4120 false,
4121 )
4122 .await?;
4123 assert_eq!(summary.specials_skipped, 1);
4124 assert_eq!(summary.files_copied, 0);
4125 assert!(!dst.exists());
4126 Ok(())
4127 }
4128 }
4129}