1use std::os::unix::fs::MetadataExt;
2
3use anyhow::{anyhow, Context};
4use async_recursion::async_recursion;
5use throttle::get_file_iops_tokens;
6use tracing::instrument;
7
8use crate::filecmp;
9use crate::preserve;
10use crate::progress;
11use crate::rm;
12use crate::rm::{Settings as RmSettings, Summary as RmSummary};
13
14#[derive(Debug, thiserror::Error)]
25#[error("{source:#}")]
26pub struct Error {
27 #[source]
28 pub source: anyhow::Error,
29 pub summary: Summary,
30}
31
32impl Error {
33 #[must_use]
34 pub fn new(source: anyhow::Error, summary: Summary) -> Self {
35 Error { source, summary }
36 }
37}
38
39#[derive(Debug, Copy, 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 chunk_size: u64,
46 pub remote_copy_buffer_size: usize,
52}
53
54#[instrument]
55pub fn is_file_type_same(md1: &std::fs::Metadata, md2: &std::fs::Metadata) -> bool {
56 let ft1 = md1.file_type();
57 let ft2 = md2.file_type();
58 ft1.is_dir() == ft2.is_dir()
59 && ft1.is_file() == ft2.is_file()
60 && ft1.is_symlink() == ft2.is_symlink()
61}
62
63#[instrument(skip(prog_track))]
64pub async fn copy_file(
65 prog_track: &'static progress::Progress,
66 src: &std::path::Path,
67 dst: &std::path::Path,
68 settings: &Settings,
69 preserve: &preserve::Settings,
70 is_fresh: bool,
71) -> Result<Summary, Error> {
72 let _open_file_guard = throttle::open_file_permit().await;
73 tracing::debug!("opening 'src' for reading and 'dst' for writing");
74 let src_metadata = tokio::fs::symlink_metadata(src)
75 .await
76 .with_context(|| format!("failed reading metadata from {:?}", &src))
77 .map_err(|err| Error::new(err, Default::default()))?;
78 get_file_iops_tokens(settings.chunk_size, src_metadata.size()).await;
79 let mut rm_summary = RmSummary::default();
80 if !is_fresh && dst.exists() {
81 if settings.overwrite {
82 tracing::debug!("file exists, check if it's identical");
83 let dst_metadata = tokio::fs::symlink_metadata(dst)
84 .await
85 .with_context(|| format!("failed reading metadata from {:?}", &dst))
86 .map_err(|err| Error::new(err, Default::default()))?;
87 if is_file_type_same(&src_metadata, &dst_metadata)
88 && filecmp::metadata_equal(
89 &settings.overwrite_compare,
90 &src_metadata,
91 &dst_metadata,
92 )
93 {
94 tracing::debug!("file is identical, skipping");
95 prog_track.files_unchanged.inc();
96 return Ok(Summary {
97 files_unchanged: 1,
98 ..Default::default()
99 });
100 }
101 tracing::info!("file is different, removing existing file");
102 rm_summary = rm::rm(
104 prog_track,
105 dst,
106 &RmSettings {
107 fail_early: settings.fail_early,
108 },
109 )
110 .await
111 .map_err(|err| {
112 let rm_summary = err.summary;
113 let copy_summary = Summary {
114 rm_summary,
115 ..Default::default()
116 };
117 Error::new(err.source, copy_summary)
118 })?;
119 } else {
120 return Err(Error::new(
121 anyhow!(
122 "destination {:?} already exists, did you intend to specify --overwrite?",
123 dst
124 ),
125 Default::default(),
126 ));
127 }
128 }
129 tracing::debug!("copying data");
130 let mut copy_summary = Summary {
131 rm_summary,
132 ..Default::default()
133 };
134 tokio::fs::copy(src, dst)
135 .await
136 .with_context(|| format!("failed copying {:?} to {:?}", &src, &dst))
137 .map_err(|err| Error::new(err, copy_summary))?;
138 prog_track.files_copied.inc();
139 prog_track.bytes_copied.add(src_metadata.len());
140 tracing::debug!("setting permissions");
141 preserve::set_file_metadata(preserve, &src_metadata, dst)
142 .await
143 .map_err(|err| Error::new(err, copy_summary))?;
144 copy_summary.bytes_copied += src_metadata.len();
146 copy_summary.files_copied += 1;
147 Ok(copy_summary)
148}
149
150#[derive(Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
151pub struct Summary {
152 pub bytes_copied: u64,
153 pub files_copied: usize,
154 pub symlinks_created: usize,
155 pub directories_created: usize,
156 pub files_unchanged: usize,
157 pub symlinks_unchanged: usize,
158 pub directories_unchanged: usize,
159 pub rm_summary: RmSummary,
160}
161
162impl std::ops::Add for Summary {
163 type Output = Self;
164 fn add(self, other: Self) -> Self {
165 Self {
166 bytes_copied: self.bytes_copied + other.bytes_copied,
167 files_copied: self.files_copied + other.files_copied,
168 symlinks_created: self.symlinks_created + other.symlinks_created,
169 directories_created: self.directories_created + other.directories_created,
170 files_unchanged: self.files_unchanged + other.files_unchanged,
171 symlinks_unchanged: self.symlinks_unchanged + other.symlinks_unchanged,
172 directories_unchanged: self.directories_unchanged + other.directories_unchanged,
173 rm_summary: self.rm_summary + other.rm_summary,
174 }
175 }
176}
177
178impl std::fmt::Display for Summary {
179 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
180 write!(
181 f,
182 "bytes copied: {}\n\
183 files copied: {}\n\
184 symlinks created: {}\n\
185 directories created: {}\n\
186 files unchanged: {}\n\
187 symlinks unchanged: {}\n\
188 directories unchanged: {}\n\
189 {}",
190 bytesize::ByteSize(self.bytes_copied),
191 self.files_copied,
192 self.symlinks_created,
193 self.directories_created,
194 self.files_unchanged,
195 self.symlinks_unchanged,
196 self.directories_unchanged,
197 &self.rm_summary,
198 )
199 }
200}
201
202#[instrument(skip(prog_track))]
203#[async_recursion]
204pub async fn copy(
205 prog_track: &'static progress::Progress,
206 src: &std::path::Path,
207 dst: &std::path::Path,
208 settings: &Settings,
209 preserve: &preserve::Settings,
210 mut is_fresh: bool,
211) -> Result<Summary, Error> {
212 let _ops_guard = prog_track.ops.guard();
213 tracing::debug!("reading source metadata");
214 let src_metadata = tokio::fs::symlink_metadata(src)
215 .await
216 .with_context(|| format!("failed reading metadata from src: {:?}", &src))
217 .map_err(|err| Error::new(err, Default::default()))?;
218 if settings.dereference && src_metadata.is_symlink() {
219 let link = tokio::fs::canonicalize(&src)
220 .await
221 .with_context(|| format!("failed reading src symlink {:?}", &src))
222 .map_err(|err| Error::new(err, Default::default()))?;
223 return copy(prog_track, &link, dst, settings, preserve, is_fresh).await;
224 }
225 if src_metadata.is_file() {
226 return copy_file(prog_track, src, dst, settings, preserve, is_fresh).await;
227 }
228 if src_metadata.is_symlink() {
229 let mut rm_summary = RmSummary::default();
230 let link = tokio::fs::read_link(src)
231 .await
232 .with_context(|| format!("failed reading symlink {:?}", &src))
233 .map_err(|err| Error::new(err, Default::default()))?;
234 if let Err(error) = tokio::fs::symlink(&link, dst).await {
236 if settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
237 let dst_metadata = tokio::fs::symlink_metadata(dst)
238 .await
239 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
240 .map_err(|err| Error::new(err, Default::default()))?;
241 if is_file_type_same(&src_metadata, &dst_metadata) {
242 let dst_link = tokio::fs::read_link(dst)
243 .await
244 .with_context(|| format!("failed reading dst symlink: {:?}", &dst))
245 .map_err(|err| Error::new(err, Default::default()))?;
246 if link == dst_link {
247 tracing::debug!(
248 "'dst' is a symlink and points to the same location as 'src'"
249 );
250 if preserve.symlink.any() {
251 let dst_metadata = tokio::fs::symlink_metadata(dst)
253 .await
254 .with_context(|| {
255 format!("failed reading metadata from dst: {:?}", &dst)
256 })
257 .map_err(|err| Error::new(err, Default::default()))?;
258 if !filecmp::metadata_equal(
259 &settings.overwrite_compare,
260 &src_metadata,
261 &dst_metadata,
262 ) {
263 tracing::debug!("'dst' metadata is different, updating");
264 preserve::set_symlink_metadata(preserve, &src_metadata, dst)
265 .await
266 .map_err(|err| Error::new(err, Default::default()))?;
267 prog_track.symlinks_removed.inc();
268 prog_track.symlinks_created.inc();
269 return Ok(Summary {
270 rm_summary: RmSummary {
271 symlinks_removed: 1,
272 ..Default::default()
273 },
274 symlinks_created: 1,
275 ..Default::default()
276 });
277 }
278 }
279 tracing::debug!("symlink already exists, skipping");
280 prog_track.symlinks_unchanged.inc();
281 return Ok(Summary {
282 symlinks_unchanged: 1,
283 ..Default::default()
284 });
285 }
286 tracing::debug!("'dst' is a symlink but points to a different path, updating");
287 } else {
288 tracing::info!("'dst' is not a symlink, updating");
289 }
290 rm_summary = rm::rm(
291 prog_track,
292 dst,
293 &RmSettings {
294 fail_early: settings.fail_early,
295 },
296 )
297 .await
298 .map_err(|err| {
299 let rm_summary = err.summary;
300 let copy_summary = Summary {
301 rm_summary,
302 ..Default::default()
303 };
304 Error::new(err.source, copy_summary)
305 })?;
306 tokio::fs::symlink(&link, dst)
307 .await
308 .with_context(|| format!("failed creating symlink {:?}", &dst))
309 .map_err(|err| {
310 let copy_summary = Summary {
311 rm_summary,
312 ..Default::default()
313 };
314 Error::new(err, copy_summary)
315 })?;
316 } else {
317 return Err(Error::new(
318 anyhow!("failed creating symlink {:?}", &dst),
319 Default::default(),
320 ));
321 }
322 }
323 preserve::set_symlink_metadata(preserve, &src_metadata, dst)
324 .await
325 .map_err(|err| {
326 let copy_summary = Summary {
327 rm_summary,
328 ..Default::default()
329 };
330 Error::new(err, copy_summary)
331 })?;
332 prog_track.symlinks_created.inc();
333 return Ok(Summary {
334 rm_summary,
335 symlinks_created: 1,
336 ..Default::default()
337 });
338 }
339 if !src_metadata.is_dir() {
340 return Err(Error::new(
341 anyhow!(
342 "copy: {:?} -> {:?} failed, unsupported src file type: {:?}",
343 src,
344 dst,
345 src_metadata.file_type()
346 ),
347 Default::default(),
348 ));
349 }
350 tracing::debug!("process contents of 'src' directory");
351 let mut entries = tokio::fs::read_dir(src)
352 .await
353 .with_context(|| format!("cannot open directory {src:?} for reading"))
354 .map_err(|err| Error::new(err, Default::default()))?;
355 let mut copy_summary = {
356 if let Err(error) = tokio::fs::create_dir(dst).await {
357 assert!(
358 !is_fresh,
359 "unexpected error creating directory: {dst:?}: {error}"
360 );
361 if settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
362 let dst_metadata = tokio::fs::metadata(dst)
367 .await
368 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
369 .map_err(|err| Error::new(err, Default::default()))?;
370 if dst_metadata.is_dir() {
371 tracing::debug!("'dst' is a directory, leaving it as is");
372 prog_track.directories_unchanged.inc();
373 Summary {
374 directories_unchanged: 1,
375 ..Default::default()
376 }
377 } else {
378 tracing::info!("'dst' is not a directory, removing and creating a new one");
379 let rm_summary = rm::rm(
380 prog_track,
381 dst,
382 &RmSettings {
383 fail_early: settings.fail_early,
384 },
385 )
386 .await
387 .map_err(|err| {
388 let rm_summary = err.summary;
389 let copy_summary = Summary {
390 rm_summary,
391 ..Default::default()
392 };
393 Error::new(err.source, copy_summary)
394 })?;
395 tokio::fs::create_dir(dst)
396 .await
397 .with_context(|| format!("cannot create directory {dst:?}"))
398 .map_err(|err| {
399 let copy_summary = Summary {
400 rm_summary,
401 ..Default::default()
402 };
403 Error::new(err, copy_summary)
404 })?;
405 is_fresh = true;
407 prog_track.directories_created.inc();
408 Summary {
409 rm_summary,
410 directories_created: 1,
411 ..Default::default()
412 }
413 }
414 } else {
415 let error = Err::<(), std::io::Error>(error)
416 .with_context(|| format!("cannot create directory {:?}", dst))
417 .unwrap_err();
418 tracing::error!("{:#}", &error);
419 return Err(Error::new(error, Default::default()));
420 }
421 } else {
422 is_fresh = true;
424 prog_track.directories_created.inc();
425 Summary {
426 directories_created: 1,
427 ..Default::default()
428 }
429 }
430 };
431 let mut join_set = tokio::task::JoinSet::new();
432 let mut success = true;
433 while let Some(entry) = entries
434 .next_entry()
435 .await
436 .with_context(|| format!("failed traversing src directory {:?}", &src))
437 .map_err(|err| Error::new(err, copy_summary))?
438 {
439 throttle::get_ops_token().await;
443 let entry_path = entry.path();
444 let entry_name = entry_path.file_name().unwrap();
445 let dst_path = dst.join(entry_name);
446 let settings = *settings;
447 let preserve = *preserve;
448 let do_copy = || async move {
449 copy(
450 prog_track,
451 &entry_path,
452 &dst_path,
453 &settings,
454 &preserve,
455 is_fresh,
456 )
457 .await
458 };
459 join_set.spawn(do_copy());
460 }
461 drop(entries);
464 while let Some(res) = join_set.join_next().await {
465 match res {
466 Ok(result) => match result {
467 Ok(summary) => copy_summary = copy_summary + summary,
468 Err(error) => {
469 tracing::error!("copy: {:?} -> {:?} failed with: {:#}", src, dst, &error);
470 copy_summary = copy_summary + error.summary;
471 if settings.fail_early {
472 return Err(Error::new(error.source, copy_summary));
473 }
474 success = false;
475 }
476 },
477 Err(error) => {
478 if settings.fail_early {
479 return Err(Error::new(error.into(), copy_summary));
480 }
481 }
482 }
483 }
484 if !success {
485 return Err(Error::new(
486 anyhow!("copy: {:?} -> {:?} failed!", src, dst),
487 copy_summary,
488 ))?;
489 }
490 tracing::debug!("set 'dst' directory metadata");
491 preserve::set_dir_metadata(preserve, &src_metadata, dst)
492 .await
493 .map_err(|err| Error::new(err, copy_summary))?;
494 Ok(copy_summary)
495}
496
497#[cfg(test)]
498mod copy_tests {
499 use crate::testutils;
500 use anyhow::Context;
501 use std::os::unix::fs::PermissionsExt;
502 use tracing_test::traced_test;
503
504 use super::*;
505
506 lazy_static! {
507 static ref PROGRESS: progress::Progress = progress::Progress::new();
508 static ref NO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_default();
509 static ref DO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_all();
510 }
511
512 #[tokio::test]
513 #[traced_test]
514 async fn check_basic_copy() -> Result<(), anyhow::Error> {
515 let tmp_dir = testutils::setup_test_dir().await?;
516 let test_path = tmp_dir.as_path();
517 let summary = copy(
518 &PROGRESS,
519 &test_path.join("foo"),
520 &test_path.join("bar"),
521 &Settings {
522 dereference: false,
523 fail_early: false,
524 overwrite: false,
525 overwrite_compare: filecmp::MetadataCmpSettings {
526 size: true,
527 mtime: true,
528 ..Default::default()
529 },
530 chunk_size: 0,
531 remote_copy_buffer_size: 0,
532 },
533 &NO_PRESERVE_SETTINGS,
534 false,
535 )
536 .await?;
537 assert_eq!(summary.files_copied, 5);
538 assert_eq!(summary.symlinks_created, 2);
539 assert_eq!(summary.directories_created, 3);
540 testutils::check_dirs_identical(
541 &test_path.join("foo"),
542 &test_path.join("bar"),
543 testutils::FileEqualityCheck::Basic,
544 )
545 .await?;
546 Ok(())
547 }
548
549 #[tokio::test]
550 #[traced_test]
551 async fn no_read_permission() -> Result<(), anyhow::Error> {
552 let tmp_dir = testutils::setup_test_dir().await?;
553 let test_path = tmp_dir.as_path();
554 let filepaths = vec![
555 test_path.join("foo").join("0.txt"),
556 test_path.join("foo").join("baz"),
557 ];
558 for fpath in &filepaths {
559 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o000)).await?;
561 }
562 match copy(
563 &PROGRESS,
564 &test_path.join("foo"),
565 &test_path.join("bar"),
566 &Settings {
567 dereference: false,
568 fail_early: false,
569 overwrite: false,
570 overwrite_compare: filecmp::MetadataCmpSettings {
571 size: true,
572 mtime: true,
573 ..Default::default()
574 },
575 chunk_size: 0,
576 remote_copy_buffer_size: 0,
577 },
578 &NO_PRESERVE_SETTINGS,
579 false,
580 )
581 .await
582 {
583 Ok(_) => panic!("Expected the copy to error!"),
584 Err(error) => {
585 tracing::info!("{}", &error);
586 assert_eq!(error.summary.files_copied, 3);
597 assert_eq!(error.summary.symlinks_created, 0);
598 assert_eq!(error.summary.directories_created, 2);
599 }
600 }
601 for fpath in &filepaths {
603 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o700)).await?;
604 if tokio::fs::symlink_metadata(fpath).await?.is_file() {
605 tokio::fs::remove_file(fpath).await?;
606 } else {
607 tokio::fs::remove_dir_all(fpath).await?;
608 }
609 }
610 testutils::check_dirs_identical(
611 &test_path.join("foo"),
612 &test_path.join("bar"),
613 testutils::FileEqualityCheck::Basic,
614 )
615 .await?;
616 Ok(())
617 }
618
619 #[tokio::test]
620 #[traced_test]
621 async fn check_default_mode() -> Result<(), anyhow::Error> {
622 let tmp_dir = testutils::setup_test_dir().await?;
623 tokio::fs::set_permissions(
625 tmp_dir.join("foo").join("0.txt"),
626 std::fs::Permissions::from_mode(0o700),
627 )
628 .await?;
629 let exec_sticky_file = tmp_dir.join("foo").join("bar").join("1.txt");
631 tokio::fs::set_permissions(&exec_sticky_file, std::fs::Permissions::from_mode(0o3770))
632 .await?;
633 let test_path = tmp_dir.as_path();
634 let summary = copy(
635 &PROGRESS,
636 &test_path.join("foo"),
637 &test_path.join("bar"),
638 &Settings {
639 dereference: false,
640 fail_early: false,
641 overwrite: false,
642 overwrite_compare: filecmp::MetadataCmpSettings {
643 size: true,
644 mtime: true,
645 ..Default::default()
646 },
647 chunk_size: 0,
648 remote_copy_buffer_size: 0,
649 },
650 &NO_PRESERVE_SETTINGS,
651 false,
652 )
653 .await?;
654 assert_eq!(summary.files_copied, 5);
655 assert_eq!(summary.symlinks_created, 2);
656 assert_eq!(summary.directories_created, 3);
657 tokio::fs::set_permissions(
659 &exec_sticky_file,
660 std::fs::Permissions::from_mode(
661 std::fs::symlink_metadata(&exec_sticky_file)?
662 .permissions()
663 .mode()
664 & 0o0777,
665 ),
666 )
667 .await?;
668 testutils::check_dirs_identical(
669 &test_path.join("foo"),
670 &test_path.join("bar"),
671 testutils::FileEqualityCheck::Basic,
672 )
673 .await?;
674 Ok(())
675 }
676
677 #[tokio::test]
678 #[traced_test]
679 async fn no_write_permission() -> Result<(), anyhow::Error> {
680 let tmp_dir = testutils::setup_test_dir().await?;
681 let test_path = tmp_dir.as_path();
682 let non_exec_dir = test_path.join("foo").join("bogey");
684 tokio::fs::create_dir(&non_exec_dir).await?;
685 tokio::fs::set_permissions(&non_exec_dir, std::fs::Permissions::from_mode(0o400)).await?;
686 tokio::fs::set_permissions(
688 &test_path.join("foo").join("baz"),
689 std::fs::Permissions::from_mode(0o500),
690 )
691 .await?;
692 tokio::fs::set_permissions(
694 &test_path.join("foo").join("baz").join("4.txt"),
695 std::fs::Permissions::from_mode(0o440),
696 )
697 .await?;
698 let summary = copy(
699 &PROGRESS,
700 &test_path.join("foo"),
701 &test_path.join("bar"),
702 &Settings {
703 dereference: false,
704 fail_early: false,
705 overwrite: false,
706 overwrite_compare: filecmp::MetadataCmpSettings {
707 size: true,
708 mtime: true,
709 ..Default::default()
710 },
711 chunk_size: 0,
712 remote_copy_buffer_size: 0,
713 },
714 &NO_PRESERVE_SETTINGS,
715 false,
716 )
717 .await?;
718 assert_eq!(summary.files_copied, 5);
719 assert_eq!(summary.symlinks_created, 2);
720 assert_eq!(summary.directories_created, 4);
721 testutils::check_dirs_identical(
722 &test_path.join("foo"),
723 &test_path.join("bar"),
724 testutils::FileEqualityCheck::Basic,
725 )
726 .await?;
727 Ok(())
728 }
729
730 #[tokio::test]
731 #[traced_test]
732 async fn dereference() -> Result<(), anyhow::Error> {
733 let tmp_dir = testutils::setup_test_dir().await?;
734 let test_path = tmp_dir.as_path();
735 let src1 = &test_path.join("foo").join("bar").join("2.txt");
737 let src2 = &test_path.join("foo").join("bar").join("3.txt");
738 let test_mode = 0o440;
739 for f in [src1, src2] {
740 tokio::fs::set_permissions(f, std::fs::Permissions::from_mode(test_mode)).await?;
741 }
742 let summary = copy(
743 &PROGRESS,
744 &test_path.join("foo"),
745 &test_path.join("bar"),
746 &Settings {
747 dereference: true, fail_early: false,
749 overwrite: false,
750 overwrite_compare: filecmp::MetadataCmpSettings {
751 size: true,
752 mtime: true,
753 ..Default::default()
754 },
755 chunk_size: 0,
756 remote_copy_buffer_size: 0,
757 },
758 &NO_PRESERVE_SETTINGS,
759 false,
760 )
761 .await?;
762 assert_eq!(summary.files_copied, 7);
763 assert_eq!(summary.symlinks_created, 0);
764 assert_eq!(summary.directories_created, 3);
765 let dst1 = &test_path.join("bar").join("baz").join("5.txt");
771 let dst2 = &test_path.join("bar").join("baz").join("6.txt");
772 for f in [dst1, dst2] {
773 let metadata = tokio::fs::symlink_metadata(f)
774 .await
775 .with_context(|| format!("failed reading metadata from {:?}", &f))?;
776 assert!(metadata.is_file());
777 assert_eq!(metadata.permissions().mode() & 0o777, test_mode);
779 }
780 Ok(())
781 }
782
783 async fn cp_compare(
784 cp_args: &[&str],
785 rcp_settings: &Settings,
786 preserve: bool,
787 ) -> Result<(), anyhow::Error> {
788 let tmp_dir = testutils::setup_test_dir().await?;
789 let test_path = tmp_dir.as_path();
790 let cp_output = tokio::process::Command::new("cp")
792 .args(cp_args)
793 .arg(test_path.join("foo"))
794 .arg(test_path.join("bar"))
795 .output()
796 .await?;
797 assert!(cp_output.status.success());
798 let summary = copy(
800 &PROGRESS,
801 &test_path.join("foo"),
802 &test_path.join("baz"),
803 rcp_settings,
804 if preserve {
805 &DO_PRESERVE_SETTINGS
806 } else {
807 &NO_PRESERVE_SETTINGS
808 },
809 false,
810 )
811 .await?;
812 if rcp_settings.dereference {
813 assert_eq!(summary.files_copied, 7);
814 assert_eq!(summary.symlinks_created, 0);
815 } else {
816 assert_eq!(summary.files_copied, 5);
817 assert_eq!(summary.symlinks_created, 2);
818 }
819 assert_eq!(summary.directories_created, 3);
820 testutils::check_dirs_identical(
821 &test_path.join("bar"),
822 &test_path.join("baz"),
823 if preserve {
824 testutils::FileEqualityCheck::Timestamp
825 } else {
826 testutils::FileEqualityCheck::Basic
827 },
828 )
829 .await?;
830 Ok(())
831 }
832
833 #[tokio::test]
834 #[traced_test]
835 async fn test_cp_compat() -> Result<(), anyhow::Error> {
836 cp_compare(
837 &["-r"],
838 &Settings {
839 dereference: false,
840 fail_early: false,
841 overwrite: false,
842 overwrite_compare: filecmp::MetadataCmpSettings {
843 size: true,
844 mtime: true,
845 ..Default::default()
846 },
847 chunk_size: 0,
848 remote_copy_buffer_size: 0,
849 },
850 false,
851 )
852 .await?;
853 Ok(())
854 }
855
856 #[tokio::test]
857 #[traced_test]
858 async fn test_cp_compat_preserve() -> Result<(), anyhow::Error> {
859 cp_compare(
860 &["-r", "-p"],
861 &Settings {
862 dereference: false,
863 fail_early: false,
864 overwrite: false,
865 overwrite_compare: filecmp::MetadataCmpSettings {
866 size: true,
867 mtime: true,
868 ..Default::default()
869 },
870 chunk_size: 0,
871 remote_copy_buffer_size: 0,
872 },
873 true,
874 )
875 .await?;
876 Ok(())
877 }
878
879 #[tokio::test]
880 #[traced_test]
881 async fn test_cp_compat_dereference() -> Result<(), anyhow::Error> {
882 cp_compare(
883 &["-r", "-L"],
884 &Settings {
885 dereference: true,
886 fail_early: false,
887 overwrite: false,
888 overwrite_compare: filecmp::MetadataCmpSettings {
889 size: true,
890 mtime: true,
891 ..Default::default()
892 },
893 chunk_size: 0,
894 remote_copy_buffer_size: 0,
895 },
896 false,
897 )
898 .await?;
899 Ok(())
900 }
901
902 #[tokio::test]
903 #[traced_test]
904 async fn test_cp_compat_preserve_and_dereference() -> Result<(), anyhow::Error> {
905 cp_compare(
906 &["-r", "-p", "-L"],
907 &Settings {
908 dereference: true,
909 fail_early: false,
910 overwrite: false,
911 overwrite_compare: filecmp::MetadataCmpSettings {
912 size: true,
913 mtime: true,
914 ..Default::default()
915 },
916 chunk_size: 0,
917 remote_copy_buffer_size: 0,
918 },
919 true,
920 )
921 .await?;
922 Ok(())
923 }
924
925 async fn setup_test_dir_and_copy() -> Result<std::path::PathBuf, anyhow::Error> {
926 let tmp_dir = testutils::setup_test_dir().await?;
927 let test_path = tmp_dir.as_path();
928 let summary = copy(
929 &PROGRESS,
930 &test_path.join("foo"),
931 &test_path.join("bar"),
932 &Settings {
933 dereference: false,
934 fail_early: false,
935 overwrite: false,
936 overwrite_compare: filecmp::MetadataCmpSettings {
937 size: true,
938 mtime: true,
939 ..Default::default()
940 },
941 chunk_size: 0,
942 remote_copy_buffer_size: 0,
943 },
944 &DO_PRESERVE_SETTINGS,
945 false,
946 )
947 .await?;
948 assert_eq!(summary.files_copied, 5);
949 assert_eq!(summary.symlinks_created, 2);
950 assert_eq!(summary.directories_created, 3);
951 Ok(tmp_dir)
952 }
953
954 #[tokio::test]
955 #[traced_test]
956 async fn test_cp_overwrite_basic() -> Result<(), anyhow::Error> {
957 let tmp_dir = setup_test_dir_and_copy().await?;
958 let output_path = &tmp_dir.join("bar");
959 {
960 let summary = rm::rm(
971 &PROGRESS,
972 &output_path.join("bar"),
973 &RmSettings { fail_early: false },
974 )
975 .await?
976 + rm::rm(
977 &PROGRESS,
978 &output_path.join("baz").join("5.txt"),
979 &RmSettings { fail_early: false },
980 )
981 .await?;
982 assert_eq!(summary.files_removed, 3);
983 assert_eq!(summary.symlinks_removed, 1);
984 assert_eq!(summary.directories_removed, 1);
985 }
986 let summary = copy(
987 &PROGRESS,
988 &tmp_dir.join("foo"),
989 output_path,
990 &Settings {
991 dereference: false,
992 fail_early: false,
993 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
995 size: true,
996 mtime: true,
997 ..Default::default()
998 },
999 chunk_size: 0,
1000 remote_copy_buffer_size: 0,
1001 },
1002 &DO_PRESERVE_SETTINGS,
1003 false,
1004 )
1005 .await?;
1006 assert_eq!(summary.files_copied, 3);
1007 assert_eq!(summary.symlinks_created, 1);
1008 assert_eq!(summary.directories_created, 1);
1009 testutils::check_dirs_identical(
1010 &tmp_dir.join("foo"),
1011 output_path,
1012 testutils::FileEqualityCheck::Timestamp,
1013 )
1014 .await?;
1015 Ok(())
1016 }
1017
1018 #[tokio::test]
1019 #[traced_test]
1020 async fn test_cp_overwrite_dir_file() -> Result<(), anyhow::Error> {
1021 let tmp_dir = setup_test_dir_and_copy().await?;
1022 let output_path = &tmp_dir.join("bar");
1023 {
1024 let summary = rm::rm(
1035 &PROGRESS,
1036 &output_path.join("bar").join("1.txt"),
1037 &RmSettings { fail_early: false },
1038 )
1039 .await?
1040 + rm::rm(
1041 &PROGRESS,
1042 &output_path.join("baz"),
1043 &RmSettings { fail_early: false },
1044 )
1045 .await?;
1046 assert_eq!(summary.files_removed, 2);
1047 assert_eq!(summary.symlinks_removed, 2);
1048 assert_eq!(summary.directories_removed, 1);
1049 }
1050 {
1051 tokio::fs::create_dir(&output_path.join("bar").join("1.txt")).await?;
1053 tokio::fs::write(&output_path.join("baz"), "baz").await?;
1055 }
1056 let summary = copy(
1057 &PROGRESS,
1058 &tmp_dir.join("foo"),
1059 output_path,
1060 &Settings {
1061 dereference: false,
1062 fail_early: false,
1063 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1065 size: true,
1066 mtime: true,
1067 ..Default::default()
1068 },
1069 chunk_size: 0,
1070 remote_copy_buffer_size: 0,
1071 },
1072 &DO_PRESERVE_SETTINGS,
1073 false,
1074 )
1075 .await?;
1076 assert_eq!(summary.rm_summary.files_removed, 1);
1077 assert_eq!(summary.rm_summary.symlinks_removed, 0);
1078 assert_eq!(summary.rm_summary.directories_removed, 1);
1079 assert_eq!(summary.files_copied, 2);
1080 assert_eq!(summary.symlinks_created, 2);
1081 assert_eq!(summary.directories_created, 1);
1082 testutils::check_dirs_identical(
1083 &tmp_dir.join("foo"),
1084 output_path,
1085 testutils::FileEqualityCheck::Timestamp,
1086 )
1087 .await?;
1088 Ok(())
1089 }
1090
1091 #[tokio::test]
1092 #[traced_test]
1093 async fn test_cp_overwrite_symlink_file() -> Result<(), anyhow::Error> {
1094 let tmp_dir = setup_test_dir_and_copy().await?;
1095 let output_path = &tmp_dir.join("bar");
1096 {
1097 let summary = rm::rm(
1104 &PROGRESS,
1105 &output_path.join("baz").join("4.txt"),
1106 &RmSettings { fail_early: false },
1107 )
1108 .await?
1109 + rm::rm(
1110 &PROGRESS,
1111 &output_path.join("baz").join("5.txt"),
1112 &RmSettings { fail_early: false },
1113 )
1114 .await?;
1115 assert_eq!(summary.files_removed, 1);
1116 assert_eq!(summary.symlinks_removed, 1);
1117 assert_eq!(summary.directories_removed, 0);
1118 }
1119 {
1120 tokio::fs::symlink("../0.txt", &output_path.join("baz").join("4.txt")).await?;
1122 tokio::fs::write(&output_path.join("baz").join("5.txt"), "baz").await?;
1124 }
1125 let summary = copy(
1126 &PROGRESS,
1127 &tmp_dir.join("foo"),
1128 output_path,
1129 &Settings {
1130 dereference: false,
1131 fail_early: false,
1132 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1134 size: true,
1135 mtime: true,
1136 ..Default::default()
1137 },
1138 chunk_size: 0,
1139 remote_copy_buffer_size: 0,
1140 },
1141 &DO_PRESERVE_SETTINGS,
1142 false,
1143 )
1144 .await?;
1145 assert_eq!(summary.rm_summary.files_removed, 1);
1146 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1147 assert_eq!(summary.rm_summary.directories_removed, 0);
1148 assert_eq!(summary.files_copied, 1);
1149 assert_eq!(summary.symlinks_created, 1);
1150 assert_eq!(summary.directories_created, 0);
1151 testutils::check_dirs_identical(
1152 &tmp_dir.join("foo"),
1153 output_path,
1154 testutils::FileEqualityCheck::Timestamp,
1155 )
1156 .await?;
1157 Ok(())
1158 }
1159
1160 #[tokio::test]
1161 #[traced_test]
1162 async fn test_cp_overwrite_symlink_dir() -> Result<(), anyhow::Error> {
1163 let tmp_dir = setup_test_dir_and_copy().await?;
1164 let output_path = &tmp_dir.join("bar");
1165 {
1166 let summary = rm::rm(
1176 &PROGRESS,
1177 &output_path.join("bar"),
1178 &RmSettings { fail_early: false },
1179 )
1180 .await?
1181 + rm::rm(
1182 &PROGRESS,
1183 &output_path.join("baz").join("5.txt"),
1184 &RmSettings { fail_early: false },
1185 )
1186 .await?;
1187 assert_eq!(summary.files_removed, 3);
1188 assert_eq!(summary.symlinks_removed, 1);
1189 assert_eq!(summary.directories_removed, 1);
1190 }
1191 {
1192 tokio::fs::symlink("0.txt", &output_path.join("bar")).await?;
1194 tokio::fs::create_dir(&output_path.join("baz").join("5.txt")).await?;
1196 }
1197 let summary = copy(
1198 &PROGRESS,
1199 &tmp_dir.join("foo"),
1200 output_path,
1201 &Settings {
1202 dereference: false,
1203 fail_early: false,
1204 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1206 size: true,
1207 mtime: true,
1208 ..Default::default()
1209 },
1210 chunk_size: 0,
1211 remote_copy_buffer_size: 0,
1212 },
1213 &DO_PRESERVE_SETTINGS,
1214 false,
1215 )
1216 .await?;
1217 assert_eq!(summary.rm_summary.files_removed, 0);
1218 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1219 assert_eq!(summary.rm_summary.directories_removed, 1);
1220 assert_eq!(summary.files_copied, 3);
1221 assert_eq!(summary.symlinks_created, 1);
1222 assert_eq!(summary.directories_created, 1);
1223 assert_eq!(summary.files_unchanged, 2);
1224 assert_eq!(summary.symlinks_unchanged, 1);
1225 assert_eq!(summary.directories_unchanged, 2);
1226 testutils::check_dirs_identical(
1227 &tmp_dir.join("foo"),
1228 output_path,
1229 testutils::FileEqualityCheck::Timestamp,
1230 )
1231 .await?;
1232 Ok(())
1233 }
1234
1235 #[tokio::test]
1236 #[traced_test]
1237 async fn test_cp_overwrite_error() -> Result<(), anyhow::Error> {
1238 let tmp_dir = testutils::setup_test_dir().await?;
1239 let test_path = tmp_dir.as_path();
1240 let summary = copy(
1241 &PROGRESS,
1242 &test_path.join("foo"),
1243 &test_path.join("bar"),
1244 &Settings {
1245 dereference: false,
1246 fail_early: false,
1247 overwrite: false,
1248 overwrite_compare: filecmp::MetadataCmpSettings {
1249 size: true,
1250 mtime: true,
1251 ..Default::default()
1252 },
1253 chunk_size: 0,
1254 remote_copy_buffer_size: 0,
1255 },
1256 &NO_PRESERVE_SETTINGS, false,
1258 )
1259 .await?;
1260 assert_eq!(summary.files_copied, 5);
1261 assert_eq!(summary.symlinks_created, 2);
1262 assert_eq!(summary.directories_created, 3);
1263 let source_path = &test_path.join("foo");
1264 let output_path = &tmp_dir.join("bar");
1265 tokio::fs::set_permissions(
1267 &source_path.join("bar"),
1268 std::fs::Permissions::from_mode(0o000),
1269 )
1270 .await?;
1271 tokio::fs::set_permissions(
1272 &source_path.join("baz").join("4.txt"),
1273 std::fs::Permissions::from_mode(0o000),
1274 )
1275 .await?;
1276 match copy(
1284 &PROGRESS,
1285 &tmp_dir.join("foo"),
1286 output_path,
1287 &Settings {
1288 dereference: false,
1289 fail_early: false,
1290 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1292 size: true,
1293 mtime: true,
1294 ..Default::default()
1295 },
1296 chunk_size: 0,
1297 remote_copy_buffer_size: 0,
1298 },
1299 &DO_PRESERVE_SETTINGS,
1300 false,
1301 )
1302 .await
1303 {
1304 Ok(_) => panic!("Expected the copy to error!"),
1305 Err(error) => {
1306 tracing::info!("{}", &error);
1307 assert_eq!(error.summary.files_copied, 1);
1308 assert_eq!(error.summary.symlinks_created, 2);
1309 assert_eq!(error.summary.directories_created, 0);
1310 assert_eq!(error.summary.rm_summary.files_removed, 2);
1311 assert_eq!(error.summary.rm_summary.symlinks_removed, 2);
1312 assert_eq!(error.summary.rm_summary.directories_removed, 0);
1313 }
1314 }
1315 Ok(())
1316 }
1317
1318 #[tokio::test]
1319 #[traced_test]
1320 async fn test_cp_dereference_symlink_chain() -> Result<(), anyhow::Error> {
1321 let tmp_dir = testutils::create_temp_dir().await?;
1323 let test_path = tmp_dir.as_path();
1324 let baz_file = test_path.join("baz_file.txt");
1326 tokio::fs::write(&baz_file, "final content").await?;
1327 let bar_link = test_path.join("bar_link");
1328 let foo_link = test_path.join("foo_link");
1329 tokio::fs::symlink(&baz_file, &bar_link).await?;
1331 tokio::fs::symlink(&bar_link, &foo_link).await?;
1332 let src_dir = test_path.join("src_chain");
1334 tokio::fs::create_dir(&src_dir).await?;
1335 tokio::fs::symlink("../foo_link", &src_dir.join("foo")).await?;
1337 tokio::fs::symlink("../bar_link", &src_dir.join("bar")).await?;
1338 tokio::fs::symlink("../baz_file.txt", &src_dir.join("baz")).await?;
1339 let summary = copy(
1341 &PROGRESS,
1342 &src_dir,
1343 &test_path.join("dst_with_deref"),
1344 &Settings {
1345 dereference: true, fail_early: false,
1347 overwrite: false,
1348 overwrite_compare: filecmp::MetadataCmpSettings {
1349 size: true,
1350 mtime: true,
1351 ..Default::default()
1352 },
1353 chunk_size: 0,
1354 remote_copy_buffer_size: 0,
1355 },
1356 &NO_PRESERVE_SETTINGS,
1357 false,
1358 )
1359 .await?;
1360 assert_eq!(summary.files_copied, 3); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 1);
1363 let dst_dir = test_path.join("dst_with_deref");
1364 let foo_content = tokio::fs::read_to_string(dst_dir.join("foo")).await?;
1366 let bar_content = tokio::fs::read_to_string(dst_dir.join("bar")).await?;
1367 let baz_content = tokio::fs::read_to_string(dst_dir.join("baz")).await?;
1368 assert_eq!(foo_content, "final content");
1369 assert_eq!(bar_content, "final content");
1370 assert_eq!(baz_content, "final content");
1371 assert!(dst_dir.join("foo").is_file());
1373 assert!(dst_dir.join("bar").is_file());
1374 assert!(dst_dir.join("baz").is_file());
1375 assert!(!dst_dir.join("foo").is_symlink());
1376 assert!(!dst_dir.join("bar").is_symlink());
1377 assert!(!dst_dir.join("baz").is_symlink());
1378 Ok(())
1379 }
1380
1381 #[tokio::test]
1382 #[traced_test]
1383 async fn test_cp_dereference_symlink_to_directory() -> Result<(), anyhow::Error> {
1384 let tmp_dir = testutils::create_temp_dir().await?;
1385 let test_path = tmp_dir.as_path();
1386 let target_dir = test_path.join("target_dir");
1388 tokio::fs::create_dir(&target_dir).await?;
1389 tokio::fs::set_permissions(&target_dir, std::fs::Permissions::from_mode(0o755)).await?;
1390 tokio::fs::write(target_dir.join("file1.txt"), "content1").await?;
1392 tokio::fs::write(target_dir.join("file2.txt"), "content2").await?;
1393 tokio::fs::set_permissions(
1394 &target_dir.join("file1.txt"),
1395 std::fs::Permissions::from_mode(0o644),
1396 )
1397 .await?;
1398 tokio::fs::set_permissions(
1399 &target_dir.join("file2.txt"),
1400 std::fs::Permissions::from_mode(0o600),
1401 )
1402 .await?;
1403 let dir_symlink = test_path.join("dir_symlink");
1405 tokio::fs::symlink(&target_dir, &dir_symlink).await?;
1406 let summary = copy(
1408 &PROGRESS,
1409 &dir_symlink,
1410 &test_path.join("copied_dir"),
1411 &Settings {
1412 dereference: true, fail_early: false,
1414 overwrite: false,
1415 overwrite_compare: filecmp::MetadataCmpSettings {
1416 size: true,
1417 mtime: true,
1418 ..Default::default()
1419 },
1420 chunk_size: 0,
1421 remote_copy_buffer_size: 0,
1422 },
1423 &DO_PRESERVE_SETTINGS,
1424 false,
1425 )
1426 .await?;
1427 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");
1431 assert!(copied_dir.is_dir());
1433 assert!(!copied_dir.is_symlink()); let file1_content = tokio::fs::read_to_string(copied_dir.join("file1.txt")).await?;
1436 let file2_content = tokio::fs::read_to_string(copied_dir.join("file2.txt")).await?;
1437 assert_eq!(file1_content, "content1");
1438 assert_eq!(file2_content, "content2");
1439 let copied_dir_metadata = tokio::fs::metadata(&copied_dir).await?;
1441 let file1_metadata = tokio::fs::metadata(copied_dir.join("file1.txt")).await?;
1442 let file2_metadata = tokio::fs::metadata(copied_dir.join("file2.txt")).await?;
1443 assert_eq!(copied_dir_metadata.permissions().mode() & 0o777, 0o755);
1444 assert_eq!(file1_metadata.permissions().mode() & 0o777, 0o644);
1445 assert_eq!(file2_metadata.permissions().mode() & 0o777, 0o600);
1446 Ok(())
1447 }
1448
1449 #[tokio::test]
1450 #[traced_test]
1451 async fn test_cp_dereference_permissions_preserved() -> Result<(), anyhow::Error> {
1452 let tmp_dir = testutils::create_temp_dir().await?;
1453 let test_path = tmp_dir.as_path();
1454 let file1 = test_path.join("file1.txt");
1456 let file2 = test_path.join("file2.txt");
1457 tokio::fs::write(&file1, "content1").await?;
1458 tokio::fs::write(&file2, "content2").await?;
1459 tokio::fs::set_permissions(&file1, std::fs::Permissions::from_mode(0o755)).await?;
1460 tokio::fs::set_permissions(&file2, std::fs::Permissions::from_mode(0o640)).await?;
1461 let symlink1 = test_path.join("symlink1");
1463 let symlink2 = test_path.join("symlink2");
1464 tokio::fs::symlink(&file1, &symlink1).await?;
1465 tokio::fs::symlink(&file2, &symlink2).await?;
1466 let summary1 = copy(
1468 &PROGRESS,
1469 &symlink1,
1470 &test_path.join("copied_file1.txt"),
1471 &Settings {
1472 dereference: true, fail_early: false,
1474 overwrite: false,
1475 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1476 chunk_size: 0,
1477 remote_copy_buffer_size: 0,
1478 },
1479 &DO_PRESERVE_SETTINGS, false,
1481 )
1482 .await?;
1483 let summary2 = copy(
1484 &PROGRESS,
1485 &symlink2,
1486 &test_path.join("copied_file2.txt"),
1487 &Settings {
1488 dereference: true,
1489 fail_early: false,
1490 overwrite: false,
1491 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1492 chunk_size: 0,
1493 remote_copy_buffer_size: 0,
1494 },
1495 &DO_PRESERVE_SETTINGS,
1496 false,
1497 )
1498 .await?;
1499 assert_eq!(summary1.files_copied, 1);
1500 assert_eq!(summary1.symlinks_created, 0);
1501 assert_eq!(summary2.files_copied, 1);
1502 assert_eq!(summary2.symlinks_created, 0);
1503 let copied1 = test_path.join("copied_file1.txt");
1504 let copied2 = test_path.join("copied_file2.txt");
1505 assert!(copied1.is_file());
1507 assert!(!copied1.is_symlink());
1508 assert!(copied2.is_file());
1509 assert!(!copied2.is_symlink());
1510 let content1 = tokio::fs::read_to_string(&copied1).await?;
1512 let content2 = tokio::fs::read_to_string(&copied2).await?;
1513 assert_eq!(content1, "content1");
1514 assert_eq!(content2, "content2");
1515 let copied1_metadata = tokio::fs::metadata(&copied1).await?;
1517 let copied2_metadata = tokio::fs::metadata(&copied2).await?;
1518 assert_eq!(copied1_metadata.permissions().mode() & 0o777, 0o755);
1519 assert_eq!(copied2_metadata.permissions().mode() & 0o777, 0o640);
1520 Ok(())
1521 }
1522
1523 #[tokio::test]
1524 #[traced_test]
1525 async fn test_cp_dereference_dir() -> Result<(), anyhow::Error> {
1526 let tmp_dir = testutils::setup_test_dir().await?;
1527 tokio::fs::symlink("bar", &tmp_dir.join("foo").join("bar-link")).await?;
1529 tokio::fs::symlink("bar-link", &tmp_dir.join("foo").join("bar-link-link")).await?;
1531 let summary = copy(
1532 &PROGRESS,
1533 &tmp_dir.join("foo"),
1534 &tmp_dir.join("bar"),
1535 &Settings {
1536 dereference: true, fail_early: false,
1538 overwrite: false,
1539 overwrite_compare: filecmp::MetadataCmpSettings {
1540 size: true,
1541 mtime: true,
1542 ..Default::default()
1543 },
1544 chunk_size: 0,
1545 remote_copy_buffer_size: 0,
1546 },
1547 &DO_PRESERVE_SETTINGS,
1548 false,
1549 )
1550 .await?;
1551 assert_eq!(summary.files_copied, 13); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 5);
1554 tokio::process::Command::new("cp")
1556 .args(["-r", "-L"])
1557 .arg(tmp_dir.join("foo"))
1558 .arg(tmp_dir.join("bar-cp"))
1559 .output()
1560 .await?;
1561 testutils::check_dirs_identical(
1562 &tmp_dir.join("bar"),
1563 &tmp_dir.join("bar-cp"),
1564 testutils::FileEqualityCheck::Basic,
1565 )
1566 .await?;
1567 Ok(())
1568 }
1569
1570 mod error_message_tests {
1572 use super::*;
1573
1574 fn get_full_error_message(error: &Error) -> String {
1576 format!("{:#}", error.source)
1577 }
1578
1579 #[tokio::test]
1580 #[traced_test]
1581 async fn test_permission_error_includes_root_cause() -> Result<(), anyhow::Error> {
1582 let tmp_dir = testutils::create_temp_dir().await?;
1583 let unreadable = tmp_dir.join("unreadable.txt");
1584 tokio::fs::write(&unreadable, "test").await?;
1585 tokio::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o000)).await?;
1586
1587 let result = copy_file(
1588 &PROGRESS,
1589 &unreadable,
1590 &tmp_dir.join("dest.txt"),
1591 &Settings {
1592 dereference: false,
1593 fail_early: false,
1594 overwrite: false,
1595 overwrite_compare: Default::default(),
1596 chunk_size: 0,
1597 remote_copy_buffer_size: 0,
1598 },
1599 &NO_PRESERVE_SETTINGS,
1600 false,
1601 )
1602 .await;
1603
1604 assert!(result.is_err(), "Should fail with permission error");
1605 let err_msg = get_full_error_message(&result.unwrap_err());
1606
1607 assert!(
1609 err_msg.to_lowercase().contains("permission")
1610 || err_msg.contains("EACCES")
1611 || err_msg.contains("denied"),
1612 "Error message must include permission-related text. Got: {}",
1613 err_msg
1614 );
1615 Ok(())
1616 }
1617
1618 #[tokio::test]
1619 #[traced_test]
1620 async fn test_nonexistent_source_includes_root_cause() -> Result<(), anyhow::Error> {
1621 let tmp_dir = testutils::create_temp_dir().await?;
1622
1623 let result = copy_file(
1624 &PROGRESS,
1625 &tmp_dir.join("does_not_exist.txt"),
1626 &tmp_dir.join("dest.txt"),
1627 &Settings {
1628 dereference: false,
1629 fail_early: false,
1630 overwrite: false,
1631 overwrite_compare: Default::default(),
1632 chunk_size: 0,
1633 remote_copy_buffer_size: 0,
1634 },
1635 &NO_PRESERVE_SETTINGS,
1636 false,
1637 )
1638 .await;
1639
1640 assert!(result.is_err());
1641 let err_msg = get_full_error_message(&result.unwrap_err());
1642
1643 assert!(
1644 err_msg.to_lowercase().contains("no such file")
1645 || err_msg.to_lowercase().contains("not found")
1646 || err_msg.contains("ENOENT"),
1647 "Error message must include file not found text. Got: {}",
1648 err_msg
1649 );
1650 Ok(())
1651 }
1652
1653 #[tokio::test]
1654 #[traced_test]
1655 async fn test_unreadable_directory_includes_root_cause() -> Result<(), anyhow::Error> {
1656 let tmp_dir = testutils::create_temp_dir().await?;
1657 let unreadable_dir = tmp_dir.join("unreadable_dir");
1658 tokio::fs::create_dir(&unreadable_dir).await?;
1659 tokio::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o000))
1660 .await?;
1661
1662 let result = copy(
1663 &PROGRESS,
1664 &unreadable_dir,
1665 &tmp_dir.join("dest"),
1666 &Settings {
1667 dereference: false,
1668 fail_early: true,
1669 overwrite: false,
1670 overwrite_compare: Default::default(),
1671 chunk_size: 0,
1672 remote_copy_buffer_size: 0,
1673 },
1674 &NO_PRESERVE_SETTINGS,
1675 false,
1676 )
1677 .await;
1678
1679 assert!(result.is_err());
1680 let err_msg = get_full_error_message(&result.unwrap_err());
1681
1682 assert!(
1683 err_msg.to_lowercase().contains("permission")
1684 || err_msg.contains("EACCES")
1685 || err_msg.contains("denied"),
1686 "Error message must include permission-related text. Got: {}",
1687 err_msg
1688 );
1689
1690 tokio::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o700))
1692 .await?;
1693 Ok(())
1694 }
1695
1696 #[tokio::test]
1697 #[traced_test]
1698 async fn test_destination_permission_error_includes_root_cause() -> Result<(), anyhow::Error>
1699 {
1700 let tmp_dir = testutils::setup_test_dir().await?;
1701 let test_path = tmp_dir.as_path();
1702 let readonly_parent = test_path.join("readonly_dest");
1703 tokio::fs::create_dir(&readonly_parent).await?;
1704 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o555))
1705 .await?;
1706
1707 let result = copy(
1708 &PROGRESS,
1709 &test_path.join("foo"),
1710 &readonly_parent.join("copy"),
1711 &Settings {
1712 dereference: false,
1713 fail_early: true,
1714 overwrite: false,
1715 overwrite_compare: Default::default(),
1716 chunk_size: 0,
1717 remote_copy_buffer_size: 0,
1718 },
1719 &NO_PRESERVE_SETTINGS,
1720 false,
1721 )
1722 .await;
1723
1724 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o755))
1726 .await?;
1727
1728 assert!(result.is_err(), "copy into read-only parent should fail");
1729 let err_msg = get_full_error_message(&result.unwrap_err());
1730
1731 assert!(
1732 err_msg.to_lowercase().contains("permission denied") || err_msg.contains("EACCES"),
1733 "Error message must include permission denied text. Got: {}",
1734 err_msg
1735 );
1736 Ok(())
1737 }
1738 }
1739}