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