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 all_children_succeeded = 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 all_children_succeeded = 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 tracing::debug!("set 'dst' directory metadata");
488 let metadata_result = preserve::set_dir_metadata(preserve, &src_metadata, dst).await;
489 if !all_children_succeeded {
490 if let Err(metadata_err) = metadata_result {
492 tracing::error!(
493 "copy: {:?} -> {:?} failed to set directory metadata: {:#}",
494 src,
495 dst,
496 &metadata_err
497 );
498 }
499 return Err(Error::new(
500 anyhow!("copy: {:?} -> {:?} failed!", src, dst),
501 copy_summary,
502 ))?;
503 }
504 metadata_result.map_err(|err| Error::new(err, copy_summary))?;
506 Ok(copy_summary)
507}
508
509#[cfg(test)]
510mod copy_tests {
511 use crate::testutils;
512 use anyhow::Context;
513 use std::os::unix::fs::PermissionsExt;
514 use tracing_test::traced_test;
515
516 use super::*;
517
518 lazy_static! {
519 static ref PROGRESS: progress::Progress = progress::Progress::new();
520 static ref NO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_default();
521 static ref DO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_all();
522 }
523
524 #[tokio::test]
525 #[traced_test]
526 async fn check_basic_copy() -> Result<(), anyhow::Error> {
527 let tmp_dir = testutils::setup_test_dir().await?;
528 let test_path = tmp_dir.as_path();
529 let summary = copy(
530 &PROGRESS,
531 &test_path.join("foo"),
532 &test_path.join("bar"),
533 &Settings {
534 dereference: false,
535 fail_early: false,
536 overwrite: false,
537 overwrite_compare: filecmp::MetadataCmpSettings {
538 size: true,
539 mtime: true,
540 ..Default::default()
541 },
542 chunk_size: 0,
543 remote_copy_buffer_size: 0,
544 },
545 &NO_PRESERVE_SETTINGS,
546 false,
547 )
548 .await?;
549 assert_eq!(summary.files_copied, 5);
550 assert_eq!(summary.symlinks_created, 2);
551 assert_eq!(summary.directories_created, 3);
552 testutils::check_dirs_identical(
553 &test_path.join("foo"),
554 &test_path.join("bar"),
555 testutils::FileEqualityCheck::Basic,
556 )
557 .await?;
558 Ok(())
559 }
560
561 #[tokio::test]
562 #[traced_test]
563 async fn no_read_permission() -> Result<(), anyhow::Error> {
564 let tmp_dir = testutils::setup_test_dir().await?;
565 let test_path = tmp_dir.as_path();
566 let filepaths = vec![
567 test_path.join("foo").join("0.txt"),
568 test_path.join("foo").join("baz"),
569 ];
570 for fpath in &filepaths {
571 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o000)).await?;
573 }
574 match copy(
575 &PROGRESS,
576 &test_path.join("foo"),
577 &test_path.join("bar"),
578 &Settings {
579 dereference: false,
580 fail_early: false,
581 overwrite: false,
582 overwrite_compare: filecmp::MetadataCmpSettings {
583 size: true,
584 mtime: true,
585 ..Default::default()
586 },
587 chunk_size: 0,
588 remote_copy_buffer_size: 0,
589 },
590 &NO_PRESERVE_SETTINGS,
591 false,
592 )
593 .await
594 {
595 Ok(_) => panic!("Expected the copy to error!"),
596 Err(error) => {
597 tracing::info!("{}", &error);
598 assert_eq!(error.summary.files_copied, 3);
609 assert_eq!(error.summary.symlinks_created, 0);
610 assert_eq!(error.summary.directories_created, 2);
611 }
612 }
613 for fpath in &filepaths {
615 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o700)).await?;
616 if tokio::fs::symlink_metadata(fpath).await?.is_file() {
617 tokio::fs::remove_file(fpath).await?;
618 } else {
619 tokio::fs::remove_dir_all(fpath).await?;
620 }
621 }
622 testutils::check_dirs_identical(
623 &test_path.join("foo"),
624 &test_path.join("bar"),
625 testutils::FileEqualityCheck::Basic,
626 )
627 .await?;
628 Ok(())
629 }
630
631 #[tokio::test]
632 #[traced_test]
633 async fn check_default_mode() -> Result<(), anyhow::Error> {
634 let tmp_dir = testutils::setup_test_dir().await?;
635 tokio::fs::set_permissions(
637 tmp_dir.join("foo").join("0.txt"),
638 std::fs::Permissions::from_mode(0o700),
639 )
640 .await?;
641 let exec_sticky_file = tmp_dir.join("foo").join("bar").join("1.txt");
643 tokio::fs::set_permissions(&exec_sticky_file, std::fs::Permissions::from_mode(0o3770))
644 .await?;
645 let test_path = tmp_dir.as_path();
646 let summary = copy(
647 &PROGRESS,
648 &test_path.join("foo"),
649 &test_path.join("bar"),
650 &Settings {
651 dereference: false,
652 fail_early: false,
653 overwrite: false,
654 overwrite_compare: filecmp::MetadataCmpSettings {
655 size: true,
656 mtime: true,
657 ..Default::default()
658 },
659 chunk_size: 0,
660 remote_copy_buffer_size: 0,
661 },
662 &NO_PRESERVE_SETTINGS,
663 false,
664 )
665 .await?;
666 assert_eq!(summary.files_copied, 5);
667 assert_eq!(summary.symlinks_created, 2);
668 assert_eq!(summary.directories_created, 3);
669 tokio::fs::set_permissions(
671 &exec_sticky_file,
672 std::fs::Permissions::from_mode(
673 std::fs::symlink_metadata(&exec_sticky_file)?
674 .permissions()
675 .mode()
676 & 0o0777,
677 ),
678 )
679 .await?;
680 testutils::check_dirs_identical(
681 &test_path.join("foo"),
682 &test_path.join("bar"),
683 testutils::FileEqualityCheck::Basic,
684 )
685 .await?;
686 Ok(())
687 }
688
689 #[tokio::test]
690 #[traced_test]
691 async fn no_write_permission() -> Result<(), anyhow::Error> {
692 let tmp_dir = testutils::setup_test_dir().await?;
693 let test_path = tmp_dir.as_path();
694 let non_exec_dir = test_path.join("foo").join("bogey");
696 tokio::fs::create_dir(&non_exec_dir).await?;
697 tokio::fs::set_permissions(&non_exec_dir, std::fs::Permissions::from_mode(0o400)).await?;
698 tokio::fs::set_permissions(
700 &test_path.join("foo").join("baz"),
701 std::fs::Permissions::from_mode(0o500),
702 )
703 .await?;
704 tokio::fs::set_permissions(
706 &test_path.join("foo").join("baz").join("4.txt"),
707 std::fs::Permissions::from_mode(0o440),
708 )
709 .await?;
710 let summary = copy(
711 &PROGRESS,
712 &test_path.join("foo"),
713 &test_path.join("bar"),
714 &Settings {
715 dereference: false,
716 fail_early: false,
717 overwrite: false,
718 overwrite_compare: filecmp::MetadataCmpSettings {
719 size: true,
720 mtime: true,
721 ..Default::default()
722 },
723 chunk_size: 0,
724 remote_copy_buffer_size: 0,
725 },
726 &NO_PRESERVE_SETTINGS,
727 false,
728 )
729 .await?;
730 assert_eq!(summary.files_copied, 5);
731 assert_eq!(summary.symlinks_created, 2);
732 assert_eq!(summary.directories_created, 4);
733 testutils::check_dirs_identical(
734 &test_path.join("foo"),
735 &test_path.join("bar"),
736 testutils::FileEqualityCheck::Basic,
737 )
738 .await?;
739 Ok(())
740 }
741
742 #[tokio::test]
743 #[traced_test]
744 async fn dereference() -> Result<(), anyhow::Error> {
745 let tmp_dir = testutils::setup_test_dir().await?;
746 let test_path = tmp_dir.as_path();
747 let src1 = &test_path.join("foo").join("bar").join("2.txt");
749 let src2 = &test_path.join("foo").join("bar").join("3.txt");
750 let test_mode = 0o440;
751 for f in [src1, src2] {
752 tokio::fs::set_permissions(f, std::fs::Permissions::from_mode(test_mode)).await?;
753 }
754 let summary = copy(
755 &PROGRESS,
756 &test_path.join("foo"),
757 &test_path.join("bar"),
758 &Settings {
759 dereference: true, fail_early: false,
761 overwrite: false,
762 overwrite_compare: filecmp::MetadataCmpSettings {
763 size: true,
764 mtime: true,
765 ..Default::default()
766 },
767 chunk_size: 0,
768 remote_copy_buffer_size: 0,
769 },
770 &NO_PRESERVE_SETTINGS,
771 false,
772 )
773 .await?;
774 assert_eq!(summary.files_copied, 7);
775 assert_eq!(summary.symlinks_created, 0);
776 assert_eq!(summary.directories_created, 3);
777 let dst1 = &test_path.join("bar").join("baz").join("5.txt");
783 let dst2 = &test_path.join("bar").join("baz").join("6.txt");
784 for f in [dst1, dst2] {
785 let metadata = tokio::fs::symlink_metadata(f)
786 .await
787 .with_context(|| format!("failed reading metadata from {:?}", &f))?;
788 assert!(metadata.is_file());
789 assert_eq!(metadata.permissions().mode() & 0o777, test_mode);
791 }
792 Ok(())
793 }
794
795 async fn cp_compare(
796 cp_args: &[&str],
797 rcp_settings: &Settings,
798 preserve: bool,
799 ) -> Result<(), anyhow::Error> {
800 let tmp_dir = testutils::setup_test_dir().await?;
801 let test_path = tmp_dir.as_path();
802 let cp_output = tokio::process::Command::new("cp")
804 .args(cp_args)
805 .arg(test_path.join("foo"))
806 .arg(test_path.join("bar"))
807 .output()
808 .await?;
809 assert!(cp_output.status.success());
810 let summary = copy(
812 &PROGRESS,
813 &test_path.join("foo"),
814 &test_path.join("baz"),
815 rcp_settings,
816 if preserve {
817 &DO_PRESERVE_SETTINGS
818 } else {
819 &NO_PRESERVE_SETTINGS
820 },
821 false,
822 )
823 .await?;
824 if rcp_settings.dereference {
825 assert_eq!(summary.files_copied, 7);
826 assert_eq!(summary.symlinks_created, 0);
827 } else {
828 assert_eq!(summary.files_copied, 5);
829 assert_eq!(summary.symlinks_created, 2);
830 }
831 assert_eq!(summary.directories_created, 3);
832 testutils::check_dirs_identical(
833 &test_path.join("bar"),
834 &test_path.join("baz"),
835 if preserve {
836 testutils::FileEqualityCheck::Timestamp
837 } else {
838 testutils::FileEqualityCheck::Basic
839 },
840 )
841 .await?;
842 Ok(())
843 }
844
845 #[tokio::test]
846 #[traced_test]
847 async fn test_cp_compat() -> Result<(), anyhow::Error> {
848 cp_compare(
849 &["-r"],
850 &Settings {
851 dereference: false,
852 fail_early: false,
853 overwrite: false,
854 overwrite_compare: filecmp::MetadataCmpSettings {
855 size: true,
856 mtime: true,
857 ..Default::default()
858 },
859 chunk_size: 0,
860 remote_copy_buffer_size: 0,
861 },
862 false,
863 )
864 .await?;
865 Ok(())
866 }
867
868 #[tokio::test]
869 #[traced_test]
870 async fn test_cp_compat_preserve() -> Result<(), anyhow::Error> {
871 cp_compare(
872 &["-r", "-p"],
873 &Settings {
874 dereference: false,
875 fail_early: false,
876 overwrite: false,
877 overwrite_compare: filecmp::MetadataCmpSettings {
878 size: true,
879 mtime: true,
880 ..Default::default()
881 },
882 chunk_size: 0,
883 remote_copy_buffer_size: 0,
884 },
885 true,
886 )
887 .await?;
888 Ok(())
889 }
890
891 #[tokio::test]
892 #[traced_test]
893 async fn test_cp_compat_dereference() -> Result<(), anyhow::Error> {
894 cp_compare(
895 &["-r", "-L"],
896 &Settings {
897 dereference: true,
898 fail_early: false,
899 overwrite: false,
900 overwrite_compare: filecmp::MetadataCmpSettings {
901 size: true,
902 mtime: true,
903 ..Default::default()
904 },
905 chunk_size: 0,
906 remote_copy_buffer_size: 0,
907 },
908 false,
909 )
910 .await?;
911 Ok(())
912 }
913
914 #[tokio::test]
915 #[traced_test]
916 async fn test_cp_compat_preserve_and_dereference() -> Result<(), anyhow::Error> {
917 cp_compare(
918 &["-r", "-p", "-L"],
919 &Settings {
920 dereference: true,
921 fail_early: false,
922 overwrite: false,
923 overwrite_compare: filecmp::MetadataCmpSettings {
924 size: true,
925 mtime: true,
926 ..Default::default()
927 },
928 chunk_size: 0,
929 remote_copy_buffer_size: 0,
930 },
931 true,
932 )
933 .await?;
934 Ok(())
935 }
936
937 async fn setup_test_dir_and_copy() -> Result<std::path::PathBuf, anyhow::Error> {
938 let tmp_dir = testutils::setup_test_dir().await?;
939 let test_path = tmp_dir.as_path();
940 let summary = copy(
941 &PROGRESS,
942 &test_path.join("foo"),
943 &test_path.join("bar"),
944 &Settings {
945 dereference: false,
946 fail_early: false,
947 overwrite: false,
948 overwrite_compare: filecmp::MetadataCmpSettings {
949 size: true,
950 mtime: true,
951 ..Default::default()
952 },
953 chunk_size: 0,
954 remote_copy_buffer_size: 0,
955 },
956 &DO_PRESERVE_SETTINGS,
957 false,
958 )
959 .await?;
960 assert_eq!(summary.files_copied, 5);
961 assert_eq!(summary.symlinks_created, 2);
962 assert_eq!(summary.directories_created, 3);
963 Ok(tmp_dir)
964 }
965
966 #[tokio::test]
967 #[traced_test]
968 async fn test_cp_overwrite_basic() -> Result<(), anyhow::Error> {
969 let tmp_dir = setup_test_dir_and_copy().await?;
970 let output_path = &tmp_dir.join("bar");
971 {
972 let summary = rm::rm(
983 &PROGRESS,
984 &output_path.join("bar"),
985 &RmSettings { fail_early: false },
986 )
987 .await?
988 + rm::rm(
989 &PROGRESS,
990 &output_path.join("baz").join("5.txt"),
991 &RmSettings { fail_early: false },
992 )
993 .await?;
994 assert_eq!(summary.files_removed, 3);
995 assert_eq!(summary.symlinks_removed, 1);
996 assert_eq!(summary.directories_removed, 1);
997 }
998 let summary = copy(
999 &PROGRESS,
1000 &tmp_dir.join("foo"),
1001 output_path,
1002 &Settings {
1003 dereference: false,
1004 fail_early: false,
1005 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1007 size: true,
1008 mtime: true,
1009 ..Default::default()
1010 },
1011 chunk_size: 0,
1012 remote_copy_buffer_size: 0,
1013 },
1014 &DO_PRESERVE_SETTINGS,
1015 false,
1016 )
1017 .await?;
1018 assert_eq!(summary.files_copied, 3);
1019 assert_eq!(summary.symlinks_created, 1);
1020 assert_eq!(summary.directories_created, 1);
1021 testutils::check_dirs_identical(
1022 &tmp_dir.join("foo"),
1023 output_path,
1024 testutils::FileEqualityCheck::Timestamp,
1025 )
1026 .await?;
1027 Ok(())
1028 }
1029
1030 #[tokio::test]
1031 #[traced_test]
1032 async fn test_cp_overwrite_dir_file() -> Result<(), anyhow::Error> {
1033 let tmp_dir = setup_test_dir_and_copy().await?;
1034 let output_path = &tmp_dir.join("bar");
1035 {
1036 let summary = rm::rm(
1047 &PROGRESS,
1048 &output_path.join("bar").join("1.txt"),
1049 &RmSettings { fail_early: false },
1050 )
1051 .await?
1052 + rm::rm(
1053 &PROGRESS,
1054 &output_path.join("baz"),
1055 &RmSettings { fail_early: false },
1056 )
1057 .await?;
1058 assert_eq!(summary.files_removed, 2);
1059 assert_eq!(summary.symlinks_removed, 2);
1060 assert_eq!(summary.directories_removed, 1);
1061 }
1062 {
1063 tokio::fs::create_dir(&output_path.join("bar").join("1.txt")).await?;
1065 tokio::fs::write(&output_path.join("baz"), "baz").await?;
1067 }
1068 let summary = copy(
1069 &PROGRESS,
1070 &tmp_dir.join("foo"),
1071 output_path,
1072 &Settings {
1073 dereference: false,
1074 fail_early: false,
1075 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1077 size: true,
1078 mtime: true,
1079 ..Default::default()
1080 },
1081 chunk_size: 0,
1082 remote_copy_buffer_size: 0,
1083 },
1084 &DO_PRESERVE_SETTINGS,
1085 false,
1086 )
1087 .await?;
1088 assert_eq!(summary.rm_summary.files_removed, 1);
1089 assert_eq!(summary.rm_summary.symlinks_removed, 0);
1090 assert_eq!(summary.rm_summary.directories_removed, 1);
1091 assert_eq!(summary.files_copied, 2);
1092 assert_eq!(summary.symlinks_created, 2);
1093 assert_eq!(summary.directories_created, 1);
1094 testutils::check_dirs_identical(
1095 &tmp_dir.join("foo"),
1096 output_path,
1097 testutils::FileEqualityCheck::Timestamp,
1098 )
1099 .await?;
1100 Ok(())
1101 }
1102
1103 #[tokio::test]
1104 #[traced_test]
1105 async fn test_cp_overwrite_symlink_file() -> Result<(), anyhow::Error> {
1106 let tmp_dir = setup_test_dir_and_copy().await?;
1107 let output_path = &tmp_dir.join("bar");
1108 {
1109 let summary = rm::rm(
1116 &PROGRESS,
1117 &output_path.join("baz").join("4.txt"),
1118 &RmSettings { fail_early: false },
1119 )
1120 .await?
1121 + rm::rm(
1122 &PROGRESS,
1123 &output_path.join("baz").join("5.txt"),
1124 &RmSettings { fail_early: false },
1125 )
1126 .await?;
1127 assert_eq!(summary.files_removed, 1);
1128 assert_eq!(summary.symlinks_removed, 1);
1129 assert_eq!(summary.directories_removed, 0);
1130 }
1131 {
1132 tokio::fs::symlink("../0.txt", &output_path.join("baz").join("4.txt")).await?;
1134 tokio::fs::write(&output_path.join("baz").join("5.txt"), "baz").await?;
1136 }
1137 let summary = copy(
1138 &PROGRESS,
1139 &tmp_dir.join("foo"),
1140 output_path,
1141 &Settings {
1142 dereference: false,
1143 fail_early: false,
1144 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1146 size: true,
1147 mtime: true,
1148 ..Default::default()
1149 },
1150 chunk_size: 0,
1151 remote_copy_buffer_size: 0,
1152 },
1153 &DO_PRESERVE_SETTINGS,
1154 false,
1155 )
1156 .await?;
1157 assert_eq!(summary.rm_summary.files_removed, 1);
1158 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1159 assert_eq!(summary.rm_summary.directories_removed, 0);
1160 assert_eq!(summary.files_copied, 1);
1161 assert_eq!(summary.symlinks_created, 1);
1162 assert_eq!(summary.directories_created, 0);
1163 testutils::check_dirs_identical(
1164 &tmp_dir.join("foo"),
1165 output_path,
1166 testutils::FileEqualityCheck::Timestamp,
1167 )
1168 .await?;
1169 Ok(())
1170 }
1171
1172 #[tokio::test]
1173 #[traced_test]
1174 async fn test_cp_overwrite_symlink_dir() -> Result<(), anyhow::Error> {
1175 let tmp_dir = setup_test_dir_and_copy().await?;
1176 let output_path = &tmp_dir.join("bar");
1177 {
1178 let summary = rm::rm(
1188 &PROGRESS,
1189 &output_path.join("bar"),
1190 &RmSettings { fail_early: false },
1191 )
1192 .await?
1193 + rm::rm(
1194 &PROGRESS,
1195 &output_path.join("baz").join("5.txt"),
1196 &RmSettings { fail_early: false },
1197 )
1198 .await?;
1199 assert_eq!(summary.files_removed, 3);
1200 assert_eq!(summary.symlinks_removed, 1);
1201 assert_eq!(summary.directories_removed, 1);
1202 }
1203 {
1204 tokio::fs::symlink("0.txt", &output_path.join("bar")).await?;
1206 tokio::fs::create_dir(&output_path.join("baz").join("5.txt")).await?;
1208 }
1209 let summary = copy(
1210 &PROGRESS,
1211 &tmp_dir.join("foo"),
1212 output_path,
1213 &Settings {
1214 dereference: false,
1215 fail_early: false,
1216 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1218 size: true,
1219 mtime: true,
1220 ..Default::default()
1221 },
1222 chunk_size: 0,
1223 remote_copy_buffer_size: 0,
1224 },
1225 &DO_PRESERVE_SETTINGS,
1226 false,
1227 )
1228 .await?;
1229 assert_eq!(summary.rm_summary.files_removed, 0);
1230 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1231 assert_eq!(summary.rm_summary.directories_removed, 1);
1232 assert_eq!(summary.files_copied, 3);
1233 assert_eq!(summary.symlinks_created, 1);
1234 assert_eq!(summary.directories_created, 1);
1235 assert_eq!(summary.files_unchanged, 2);
1236 assert_eq!(summary.symlinks_unchanged, 1);
1237 assert_eq!(summary.directories_unchanged, 2);
1238 testutils::check_dirs_identical(
1239 &tmp_dir.join("foo"),
1240 output_path,
1241 testutils::FileEqualityCheck::Timestamp,
1242 )
1243 .await?;
1244 Ok(())
1245 }
1246
1247 #[tokio::test]
1248 #[traced_test]
1249 async fn test_cp_overwrite_error() -> Result<(), anyhow::Error> {
1250 let tmp_dir = testutils::setup_test_dir().await?;
1251 let test_path = tmp_dir.as_path();
1252 let summary = copy(
1253 &PROGRESS,
1254 &test_path.join("foo"),
1255 &test_path.join("bar"),
1256 &Settings {
1257 dereference: false,
1258 fail_early: false,
1259 overwrite: false,
1260 overwrite_compare: filecmp::MetadataCmpSettings {
1261 size: true,
1262 mtime: true,
1263 ..Default::default()
1264 },
1265 chunk_size: 0,
1266 remote_copy_buffer_size: 0,
1267 },
1268 &NO_PRESERVE_SETTINGS, false,
1270 )
1271 .await?;
1272 assert_eq!(summary.files_copied, 5);
1273 assert_eq!(summary.symlinks_created, 2);
1274 assert_eq!(summary.directories_created, 3);
1275 let source_path = &test_path.join("foo");
1276 let output_path = &tmp_dir.join("bar");
1277 tokio::fs::set_permissions(
1279 &source_path.join("bar"),
1280 std::fs::Permissions::from_mode(0o000),
1281 )
1282 .await?;
1283 tokio::fs::set_permissions(
1284 &source_path.join("baz").join("4.txt"),
1285 std::fs::Permissions::from_mode(0o000),
1286 )
1287 .await?;
1288 match copy(
1296 &PROGRESS,
1297 &tmp_dir.join("foo"),
1298 output_path,
1299 &Settings {
1300 dereference: false,
1301 fail_early: false,
1302 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1304 size: true,
1305 mtime: true,
1306 ..Default::default()
1307 },
1308 chunk_size: 0,
1309 remote_copy_buffer_size: 0,
1310 },
1311 &DO_PRESERVE_SETTINGS,
1312 false,
1313 )
1314 .await
1315 {
1316 Ok(_) => panic!("Expected the copy to error!"),
1317 Err(error) => {
1318 tracing::info!("{}", &error);
1319 assert_eq!(error.summary.files_copied, 1);
1320 assert_eq!(error.summary.symlinks_created, 2);
1321 assert_eq!(error.summary.directories_created, 0);
1322 assert_eq!(error.summary.rm_summary.files_removed, 2);
1323 assert_eq!(error.summary.rm_summary.symlinks_removed, 2);
1324 assert_eq!(error.summary.rm_summary.directories_removed, 0);
1325 }
1326 }
1327 Ok(())
1328 }
1329
1330 #[tokio::test]
1331 #[traced_test]
1332 async fn test_cp_dereference_symlink_chain() -> Result<(), anyhow::Error> {
1333 let tmp_dir = testutils::create_temp_dir().await?;
1335 let test_path = tmp_dir.as_path();
1336 let baz_file = test_path.join("baz_file.txt");
1338 tokio::fs::write(&baz_file, "final content").await?;
1339 let bar_link = test_path.join("bar_link");
1340 let foo_link = test_path.join("foo_link");
1341 tokio::fs::symlink(&baz_file, &bar_link).await?;
1343 tokio::fs::symlink(&bar_link, &foo_link).await?;
1344 let src_dir = test_path.join("src_chain");
1346 tokio::fs::create_dir(&src_dir).await?;
1347 tokio::fs::symlink("../foo_link", &src_dir.join("foo")).await?;
1349 tokio::fs::symlink("../bar_link", &src_dir.join("bar")).await?;
1350 tokio::fs::symlink("../baz_file.txt", &src_dir.join("baz")).await?;
1351 let summary = copy(
1353 &PROGRESS,
1354 &src_dir,
1355 &test_path.join("dst_with_deref"),
1356 &Settings {
1357 dereference: true, fail_early: false,
1359 overwrite: false,
1360 overwrite_compare: filecmp::MetadataCmpSettings {
1361 size: true,
1362 mtime: true,
1363 ..Default::default()
1364 },
1365 chunk_size: 0,
1366 remote_copy_buffer_size: 0,
1367 },
1368 &NO_PRESERVE_SETTINGS,
1369 false,
1370 )
1371 .await?;
1372 assert_eq!(summary.files_copied, 3); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 1);
1375 let dst_dir = test_path.join("dst_with_deref");
1376 let foo_content = tokio::fs::read_to_string(dst_dir.join("foo")).await?;
1378 let bar_content = tokio::fs::read_to_string(dst_dir.join("bar")).await?;
1379 let baz_content = tokio::fs::read_to_string(dst_dir.join("baz")).await?;
1380 assert_eq!(foo_content, "final content");
1381 assert_eq!(bar_content, "final content");
1382 assert_eq!(baz_content, "final content");
1383 assert!(dst_dir.join("foo").is_file());
1385 assert!(dst_dir.join("bar").is_file());
1386 assert!(dst_dir.join("baz").is_file());
1387 assert!(!dst_dir.join("foo").is_symlink());
1388 assert!(!dst_dir.join("bar").is_symlink());
1389 assert!(!dst_dir.join("baz").is_symlink());
1390 Ok(())
1391 }
1392
1393 #[tokio::test]
1394 #[traced_test]
1395 async fn test_cp_dereference_symlink_to_directory() -> Result<(), anyhow::Error> {
1396 let tmp_dir = testutils::create_temp_dir().await?;
1397 let test_path = tmp_dir.as_path();
1398 let target_dir = test_path.join("target_dir");
1400 tokio::fs::create_dir(&target_dir).await?;
1401 tokio::fs::set_permissions(&target_dir, std::fs::Permissions::from_mode(0o755)).await?;
1402 tokio::fs::write(target_dir.join("file1.txt"), "content1").await?;
1404 tokio::fs::write(target_dir.join("file2.txt"), "content2").await?;
1405 tokio::fs::set_permissions(
1406 &target_dir.join("file1.txt"),
1407 std::fs::Permissions::from_mode(0o644),
1408 )
1409 .await?;
1410 tokio::fs::set_permissions(
1411 &target_dir.join("file2.txt"),
1412 std::fs::Permissions::from_mode(0o600),
1413 )
1414 .await?;
1415 let dir_symlink = test_path.join("dir_symlink");
1417 tokio::fs::symlink(&target_dir, &dir_symlink).await?;
1418 let summary = copy(
1420 &PROGRESS,
1421 &dir_symlink,
1422 &test_path.join("copied_dir"),
1423 &Settings {
1424 dereference: true, fail_early: false,
1426 overwrite: false,
1427 overwrite_compare: filecmp::MetadataCmpSettings {
1428 size: true,
1429 mtime: true,
1430 ..Default::default()
1431 },
1432 chunk_size: 0,
1433 remote_copy_buffer_size: 0,
1434 },
1435 &DO_PRESERVE_SETTINGS,
1436 false,
1437 )
1438 .await?;
1439 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");
1443 assert!(copied_dir.is_dir());
1445 assert!(!copied_dir.is_symlink()); let file1_content = tokio::fs::read_to_string(copied_dir.join("file1.txt")).await?;
1448 let file2_content = tokio::fs::read_to_string(copied_dir.join("file2.txt")).await?;
1449 assert_eq!(file1_content, "content1");
1450 assert_eq!(file2_content, "content2");
1451 let copied_dir_metadata = tokio::fs::metadata(&copied_dir).await?;
1453 let file1_metadata = tokio::fs::metadata(copied_dir.join("file1.txt")).await?;
1454 let file2_metadata = tokio::fs::metadata(copied_dir.join("file2.txt")).await?;
1455 assert_eq!(copied_dir_metadata.permissions().mode() & 0o777, 0o755);
1456 assert_eq!(file1_metadata.permissions().mode() & 0o777, 0o644);
1457 assert_eq!(file2_metadata.permissions().mode() & 0o777, 0o600);
1458 Ok(())
1459 }
1460
1461 #[tokio::test]
1462 #[traced_test]
1463 async fn test_cp_dereference_permissions_preserved() -> Result<(), anyhow::Error> {
1464 let tmp_dir = testutils::create_temp_dir().await?;
1465 let test_path = tmp_dir.as_path();
1466 let file1 = test_path.join("file1.txt");
1468 let file2 = test_path.join("file2.txt");
1469 tokio::fs::write(&file1, "content1").await?;
1470 tokio::fs::write(&file2, "content2").await?;
1471 tokio::fs::set_permissions(&file1, std::fs::Permissions::from_mode(0o755)).await?;
1472 tokio::fs::set_permissions(&file2, std::fs::Permissions::from_mode(0o640)).await?;
1473 let symlink1 = test_path.join("symlink1");
1475 let symlink2 = test_path.join("symlink2");
1476 tokio::fs::symlink(&file1, &symlink1).await?;
1477 tokio::fs::symlink(&file2, &symlink2).await?;
1478 let summary1 = copy(
1480 &PROGRESS,
1481 &symlink1,
1482 &test_path.join("copied_file1.txt"),
1483 &Settings {
1484 dereference: true, fail_early: false,
1486 overwrite: false,
1487 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1488 chunk_size: 0,
1489 remote_copy_buffer_size: 0,
1490 },
1491 &DO_PRESERVE_SETTINGS, false,
1493 )
1494 .await?;
1495 let summary2 = copy(
1496 &PROGRESS,
1497 &symlink2,
1498 &test_path.join("copied_file2.txt"),
1499 &Settings {
1500 dereference: true,
1501 fail_early: false,
1502 overwrite: false,
1503 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1504 chunk_size: 0,
1505 remote_copy_buffer_size: 0,
1506 },
1507 &DO_PRESERVE_SETTINGS,
1508 false,
1509 )
1510 .await?;
1511 assert_eq!(summary1.files_copied, 1);
1512 assert_eq!(summary1.symlinks_created, 0);
1513 assert_eq!(summary2.files_copied, 1);
1514 assert_eq!(summary2.symlinks_created, 0);
1515 let copied1 = test_path.join("copied_file1.txt");
1516 let copied2 = test_path.join("copied_file2.txt");
1517 assert!(copied1.is_file());
1519 assert!(!copied1.is_symlink());
1520 assert!(copied2.is_file());
1521 assert!(!copied2.is_symlink());
1522 let content1 = tokio::fs::read_to_string(&copied1).await?;
1524 let content2 = tokio::fs::read_to_string(&copied2).await?;
1525 assert_eq!(content1, "content1");
1526 assert_eq!(content2, "content2");
1527 let copied1_metadata = tokio::fs::metadata(&copied1).await?;
1529 let copied2_metadata = tokio::fs::metadata(&copied2).await?;
1530 assert_eq!(copied1_metadata.permissions().mode() & 0o777, 0o755);
1531 assert_eq!(copied2_metadata.permissions().mode() & 0o777, 0o640);
1532 Ok(())
1533 }
1534
1535 #[tokio::test]
1536 #[traced_test]
1537 async fn test_cp_dereference_dir() -> Result<(), anyhow::Error> {
1538 let tmp_dir = testutils::setup_test_dir().await?;
1539 tokio::fs::symlink("bar", &tmp_dir.join("foo").join("bar-link")).await?;
1541 tokio::fs::symlink("bar-link", &tmp_dir.join("foo").join("bar-link-link")).await?;
1543 let summary = copy(
1544 &PROGRESS,
1545 &tmp_dir.join("foo"),
1546 &tmp_dir.join("bar"),
1547 &Settings {
1548 dereference: true, fail_early: false,
1550 overwrite: false,
1551 overwrite_compare: filecmp::MetadataCmpSettings {
1552 size: true,
1553 mtime: true,
1554 ..Default::default()
1555 },
1556 chunk_size: 0,
1557 remote_copy_buffer_size: 0,
1558 },
1559 &DO_PRESERVE_SETTINGS,
1560 false,
1561 )
1562 .await?;
1563 assert_eq!(summary.files_copied, 13); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 5);
1566 tokio::process::Command::new("cp")
1568 .args(["-r", "-L"])
1569 .arg(tmp_dir.join("foo"))
1570 .arg(tmp_dir.join("bar-cp"))
1571 .output()
1572 .await?;
1573 testutils::check_dirs_identical(
1574 &tmp_dir.join("bar"),
1575 &tmp_dir.join("bar-cp"),
1576 testutils::FileEqualityCheck::Basic,
1577 )
1578 .await?;
1579 Ok(())
1580 }
1581
1582 mod error_message_tests {
1584 use super::*;
1585
1586 fn get_full_error_message(error: &Error) -> String {
1588 format!("{:#}", error.source)
1589 }
1590
1591 #[tokio::test]
1592 #[traced_test]
1593 async fn test_permission_error_includes_root_cause() -> Result<(), anyhow::Error> {
1594 let tmp_dir = testutils::create_temp_dir().await?;
1595 let unreadable = tmp_dir.join("unreadable.txt");
1596 tokio::fs::write(&unreadable, "test").await?;
1597 tokio::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o000)).await?;
1598
1599 let result = copy_file(
1600 &PROGRESS,
1601 &unreadable,
1602 &tmp_dir.join("dest.txt"),
1603 &Settings {
1604 dereference: false,
1605 fail_early: false,
1606 overwrite: false,
1607 overwrite_compare: Default::default(),
1608 chunk_size: 0,
1609 remote_copy_buffer_size: 0,
1610 },
1611 &NO_PRESERVE_SETTINGS,
1612 false,
1613 )
1614 .await;
1615
1616 assert!(result.is_err(), "Should fail with permission error");
1617 let err_msg = get_full_error_message(&result.unwrap_err());
1618
1619 assert!(
1621 err_msg.to_lowercase().contains("permission")
1622 || err_msg.contains("EACCES")
1623 || err_msg.contains("denied"),
1624 "Error message must include permission-related text. Got: {}",
1625 err_msg
1626 );
1627 Ok(())
1628 }
1629
1630 #[tokio::test]
1631 #[traced_test]
1632 async fn test_nonexistent_source_includes_root_cause() -> Result<(), anyhow::Error> {
1633 let tmp_dir = testutils::create_temp_dir().await?;
1634
1635 let result = copy_file(
1636 &PROGRESS,
1637 &tmp_dir.join("does_not_exist.txt"),
1638 &tmp_dir.join("dest.txt"),
1639 &Settings {
1640 dereference: false,
1641 fail_early: false,
1642 overwrite: false,
1643 overwrite_compare: Default::default(),
1644 chunk_size: 0,
1645 remote_copy_buffer_size: 0,
1646 },
1647 &NO_PRESERVE_SETTINGS,
1648 false,
1649 )
1650 .await;
1651
1652 assert!(result.is_err());
1653 let err_msg = get_full_error_message(&result.unwrap_err());
1654
1655 assert!(
1656 err_msg.to_lowercase().contains("no such file")
1657 || err_msg.to_lowercase().contains("not found")
1658 || err_msg.contains("ENOENT"),
1659 "Error message must include file not found text. Got: {}",
1660 err_msg
1661 );
1662 Ok(())
1663 }
1664
1665 #[tokio::test]
1666 #[traced_test]
1667 async fn test_unreadable_directory_includes_root_cause() -> Result<(), anyhow::Error> {
1668 let tmp_dir = testutils::create_temp_dir().await?;
1669 let unreadable_dir = tmp_dir.join("unreadable_dir");
1670 tokio::fs::create_dir(&unreadable_dir).await?;
1671 tokio::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o000))
1672 .await?;
1673
1674 let result = copy(
1675 &PROGRESS,
1676 &unreadable_dir,
1677 &tmp_dir.join("dest"),
1678 &Settings {
1679 dereference: false,
1680 fail_early: true,
1681 overwrite: false,
1682 overwrite_compare: Default::default(),
1683 chunk_size: 0,
1684 remote_copy_buffer_size: 0,
1685 },
1686 &NO_PRESERVE_SETTINGS,
1687 false,
1688 )
1689 .await;
1690
1691 assert!(result.is_err());
1692 let err_msg = get_full_error_message(&result.unwrap_err());
1693
1694 assert!(
1695 err_msg.to_lowercase().contains("permission")
1696 || err_msg.contains("EACCES")
1697 || err_msg.contains("denied"),
1698 "Error message must include permission-related text. Got: {}",
1699 err_msg
1700 );
1701
1702 tokio::fs::set_permissions(&unreadable_dir, std::fs::Permissions::from_mode(0o700))
1704 .await?;
1705 Ok(())
1706 }
1707
1708 #[tokio::test]
1709 #[traced_test]
1710 async fn test_destination_permission_error_includes_root_cause() -> Result<(), anyhow::Error>
1711 {
1712 let tmp_dir = testutils::setup_test_dir().await?;
1713 let test_path = tmp_dir.as_path();
1714 let readonly_parent = test_path.join("readonly_dest");
1715 tokio::fs::create_dir(&readonly_parent).await?;
1716 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o555))
1717 .await?;
1718
1719 let result = copy(
1720 &PROGRESS,
1721 &test_path.join("foo"),
1722 &readonly_parent.join("copy"),
1723 &Settings {
1724 dereference: false,
1725 fail_early: true,
1726 overwrite: false,
1727 overwrite_compare: Default::default(),
1728 chunk_size: 0,
1729 remote_copy_buffer_size: 0,
1730 },
1731 &NO_PRESERVE_SETTINGS,
1732 false,
1733 )
1734 .await;
1735
1736 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o755))
1738 .await?;
1739
1740 assert!(result.is_err(), "copy into read-only parent should fail");
1741 let err_msg = get_full_error_message(&result.unwrap_err());
1742
1743 assert!(
1744 err_msg.to_lowercase().contains("permission denied") || err_msg.contains("EACCES"),
1745 "Error message must include permission denied text. Got: {}",
1746 err_msg
1747 );
1748 Ok(())
1749 }
1750 }
1751
1752 #[tokio::test]
1756 #[traced_test]
1757 async fn test_directory_metadata_applied_on_child_error() -> Result<(), anyhow::Error> {
1758 let tmp_dir = testutils::create_temp_dir().await?;
1759 let test_path = tmp_dir.as_path();
1760 let src_dir = test_path.join("src");
1762 tokio::fs::create_dir(&src_dir).await?;
1763 tokio::fs::set_permissions(&src_dir, std::fs::Permissions::from_mode(0o750)).await?;
1764 let readable_file = src_dir.join("readable.txt");
1766 tokio::fs::write(&readable_file, "content").await?;
1767 let unreadable_file = src_dir.join("unreadable.txt");
1768 tokio::fs::write(&unreadable_file, "secret").await?;
1769 tokio::fs::set_permissions(&unreadable_file, std::fs::Permissions::from_mode(0o000))
1770 .await?;
1771 let dst_dir = test_path.join("dst");
1772 let result = copy(
1774 &PROGRESS,
1775 &src_dir,
1776 &dst_dir,
1777 &Settings {
1778 dereference: false,
1779 fail_early: false,
1780 overwrite: false,
1781 overwrite_compare: Default::default(),
1782 chunk_size: 0,
1783 remote_copy_buffer_size: 0,
1784 },
1785 &DO_PRESERVE_SETTINGS,
1786 false,
1787 )
1788 .await;
1789 tokio::fs::set_permissions(&unreadable_file, std::fs::Permissions::from_mode(0o644))
1791 .await?;
1792 assert!(result.is_err(), "copy should fail due to unreadable file");
1794 let error = result.unwrap_err();
1795 assert_eq!(error.summary.files_copied, 1);
1797 assert_eq!(error.summary.directories_created, 1);
1798 let dst_metadata = tokio::fs::metadata(&dst_dir).await?;
1800 assert!(dst_metadata.is_dir());
1801 let actual_mode = dst_metadata.permissions().mode() & 0o7777;
1802 assert_eq!(
1803 actual_mode, 0o750,
1804 "directory should have preserved source permissions (0o750), got {:o}",
1805 actual_mode
1806 );
1807 Ok(())
1808 }
1809}