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)]
15#[error("{source}")]
16pub struct Error {
17 #[source]
18 pub source: anyhow::Error,
19 pub summary: Summary,
20}
21
22impl Error {
23 #[must_use]
24 pub fn new(source: anyhow::Error, summary: Summary) -> Self {
25 Error { source, summary }
26 }
27}
28
29#[derive(Debug, Copy, Clone)]
30pub struct Settings {
31 pub dereference: bool,
32 pub fail_early: bool,
33 pub overwrite: bool,
34 pub overwrite_compare: filecmp::MetadataCmpSettings,
35 pub chunk_size: u64,
36}
37
38#[instrument]
39pub fn is_file_type_same(md1: &std::fs::Metadata, md2: &std::fs::Metadata) -> bool {
40 let ft1 = md1.file_type();
41 let ft2 = md2.file_type();
42 ft1.is_dir() == ft2.is_dir()
43 && ft1.is_file() == ft2.is_file()
44 && ft1.is_symlink() == ft2.is_symlink()
45}
46
47#[instrument(skip(prog_track))]
48pub async fn copy_file(
49 prog_track: &'static progress::Progress,
50 src: &std::path::Path,
51 dst: &std::path::Path,
52 settings: &Settings,
53 preserve: &preserve::Settings,
54 is_fresh: bool,
55) -> Result<Summary, Error> {
56 let _open_file_guard = throttle::open_file_permit().await;
57 tracing::debug!("opening 'src' for reading and 'dst' for writing");
58 let src_metadata = tokio::fs::symlink_metadata(src)
59 .await
60 .with_context(|| format!("failed reading metadata from {:?}", &src))
61 .map_err(|err| Error::new(err, Default::default()))?;
62 get_file_iops_tokens(settings.chunk_size, src_metadata.size()).await;
63 let mut rm_summary = RmSummary::default();
64 if !is_fresh && dst.exists() {
65 if settings.overwrite {
66 tracing::debug!("file exists, check if it's identical");
67 let dst_metadata = tokio::fs::symlink_metadata(dst)
68 .await
69 .with_context(|| format!("failed reading metadata from {:?}", &dst))
70 .map_err(|err| Error::new(err, Default::default()))?;
71 if is_file_type_same(&src_metadata, &dst_metadata)
72 && filecmp::metadata_equal(
73 &settings.overwrite_compare,
74 &src_metadata,
75 &dst_metadata,
76 )
77 {
78 tracing::debug!("file is identical, skipping");
79 prog_track.files_unchanged.inc();
80 return Ok(Summary {
81 files_unchanged: 1,
82 ..Default::default()
83 });
84 }
85 tracing::info!("file is different, removing existing file");
86 rm_summary = rm::rm(
88 prog_track,
89 dst,
90 &RmSettings {
91 fail_early: settings.fail_early,
92 },
93 )
94 .await
95 .map_err(|err| {
96 let rm_summary = err.summary;
97 let copy_summary = Summary {
98 rm_summary,
99 ..Default::default()
100 };
101 Error::new(anyhow::Error::msg(err), copy_summary)
102 })?;
103 } else {
104 return Err(Error::new(
105 anyhow!(
106 "destination {:?} already exists, did you intend to specify --overwrite?",
107 dst
108 ),
109 Default::default(),
110 ));
111 }
112 }
113 tracing::debug!("copying data");
114 let mut copy_summary = Summary {
115 rm_summary,
116 ..Default::default()
117 };
118 tokio::fs::copy(src, dst)
119 .await
120 .with_context(|| format!("failed copying {:?} to {:?}", &src, &dst))
121 .map_err(|err| Error::new(err, copy_summary))?;
122 prog_track.files_copied.inc();
123 prog_track.bytes_copied.add(src_metadata.len());
124 tracing::debug!("setting permissions");
125 preserve::set_file_metadata(preserve, &src_metadata, dst)
126 .await
127 .map_err(|err| Error::new(err, copy_summary))?;
128 copy_summary.bytes_copied += src_metadata.len();
130 copy_summary.files_copied += 1;
131 Ok(copy_summary)
132}
133
134#[derive(Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
135pub struct Summary {
136 pub bytes_copied: u64,
137 pub files_copied: usize,
138 pub symlinks_created: usize,
139 pub directories_created: usize,
140 pub files_unchanged: usize,
141 pub symlinks_unchanged: usize,
142 pub directories_unchanged: usize,
143 pub rm_summary: RmSummary,
144}
145
146impl std::ops::Add for Summary {
147 type Output = Self;
148 fn add(self, other: Self) -> Self {
149 Self {
150 bytes_copied: self.bytes_copied + other.bytes_copied,
151 files_copied: self.files_copied + other.files_copied,
152 symlinks_created: self.symlinks_created + other.symlinks_created,
153 directories_created: self.directories_created + other.directories_created,
154 files_unchanged: self.files_unchanged + other.files_unchanged,
155 symlinks_unchanged: self.symlinks_unchanged + other.symlinks_unchanged,
156 directories_unchanged: self.directories_unchanged + other.directories_unchanged,
157 rm_summary: self.rm_summary + other.rm_summary,
158 }
159 }
160}
161
162impl std::fmt::Display for Summary {
163 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
164 write!(
165 f,
166 "bytes copied: {}\n\
167 files copied: {}\n\
168 symlinks created: {}\n\
169 directories created: {}\n\
170 files unchanged: {}\n\
171 symlinks unchanged: {}\n\
172 directories unchanged: {}\n\
173 {}",
174 bytesize::ByteSize(self.bytes_copied),
175 self.files_copied,
176 self.symlinks_created,
177 self.directories_created,
178 self.files_unchanged,
179 self.symlinks_unchanged,
180 self.directories_unchanged,
181 &self.rm_summary,
182 )
183 }
184}
185
186#[instrument(skip(prog_track))]
187#[async_recursion]
188pub async fn copy(
189 prog_track: &'static progress::Progress,
190 src: &std::path::Path,
191 dst: &std::path::Path,
192 settings: &Settings,
193 preserve: &preserve::Settings,
194 mut is_fresh: bool,
195) -> Result<Summary, Error> {
196 let _ops_guard = prog_track.ops.guard();
197 tracing::debug!("reading source metadata");
198 let src_metadata = tokio::fs::symlink_metadata(src)
199 .await
200 .with_context(|| format!("failed reading metadata from src: {:?}", &src))
201 .map_err(|err| Error::new(err, Default::default()))?;
202 if settings.dereference && src_metadata.is_symlink() {
203 let link = tokio::fs::canonicalize(&src)
204 .await
205 .with_context(|| format!("failed reading src symlink {:?}", &src))
206 .map_err(|err| Error::new(err, Default::default()))?;
207 return copy(prog_track, &link, dst, settings, preserve, is_fresh).await;
208 }
209 if src_metadata.is_file() {
210 return copy_file(prog_track, src, dst, settings, preserve, is_fresh).await;
211 }
212 if src_metadata.is_symlink() {
213 let mut rm_summary = RmSummary::default();
214 let link = tokio::fs::read_link(src)
215 .await
216 .with_context(|| format!("failed reading symlink {:?}", &src))
217 .map_err(|err| Error::new(err, Default::default()))?;
218 if let Err(error) = tokio::fs::symlink(&link, dst).await {
220 if settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
221 let dst_metadata = tokio::fs::symlink_metadata(dst)
222 .await
223 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
224 .map_err(|err| Error::new(err, Default::default()))?;
225 if is_file_type_same(&src_metadata, &dst_metadata) {
226 let dst_link = tokio::fs::read_link(dst)
227 .await
228 .with_context(|| format!("failed reading dst symlink: {:?}", &dst))
229 .map_err(|err| Error::new(err, Default::default()))?;
230 if link == dst_link {
231 tracing::debug!(
232 "'dst' is a symlink and points to the same location as 'src'"
233 );
234 if preserve.symlink.any() {
235 let dst_metadata = tokio::fs::symlink_metadata(dst)
237 .await
238 .with_context(|| {
239 format!("failed reading metadata from dst: {:?}", &dst)
240 })
241 .map_err(|err| Error::new(err, Default::default()))?;
242 if !filecmp::metadata_equal(
243 &settings.overwrite_compare,
244 &src_metadata,
245 &dst_metadata,
246 ) {
247 tracing::debug!("'dst' metadata is different, updating");
248 preserve::set_symlink_metadata(preserve, &src_metadata, dst)
249 .await
250 .map_err(|err| Error::new(err, Default::default()))?;
251 prog_track.symlinks_removed.inc();
252 prog_track.symlinks_created.inc();
253 return Ok(Summary {
254 rm_summary: RmSummary {
255 symlinks_removed: 1,
256 ..Default::default()
257 },
258 symlinks_created: 1,
259 ..Default::default()
260 });
261 }
262 }
263 tracing::debug!("symlink already exists, skipping");
264 prog_track.symlinks_unchanged.inc();
265 return Ok(Summary {
266 symlinks_unchanged: 1,
267 ..Default::default()
268 });
269 }
270 tracing::debug!("'dst' is a symlink but points to a different path, updating");
271 } else {
272 tracing::info!("'dst' is not a symlink, updating");
273 }
274 rm_summary = rm::rm(
275 prog_track,
276 dst,
277 &RmSettings {
278 fail_early: settings.fail_early,
279 },
280 )
281 .await
282 .map_err(|err| {
283 let rm_summary = err.summary;
284 let copy_summary = Summary {
285 rm_summary,
286 ..Default::default()
287 };
288 Error::new(err.source, copy_summary)
289 })?;
290 tokio::fs::symlink(&link, dst)
291 .await
292 .with_context(|| format!("failed creating symlink {:?}", &dst))
293 .map_err(|err| {
294 let copy_summary = Summary {
295 rm_summary,
296 ..Default::default()
297 };
298 Error::new(err, copy_summary)
299 })?;
300 } else {
301 return Err(Error::new(
302 anyhow!("failed creating symlink {:?}", &dst),
303 Default::default(),
304 ));
305 }
306 }
307 preserve::set_symlink_metadata(preserve, &src_metadata, dst)
308 .await
309 .map_err(|err| {
310 let copy_summary = Summary {
311 rm_summary,
312 ..Default::default()
313 };
314 Error::new(err, copy_summary)
315 })?;
316 prog_track.symlinks_created.inc();
317 return Ok(Summary {
318 rm_summary,
319 symlinks_created: 1,
320 ..Default::default()
321 });
322 }
323 if !src_metadata.is_dir() {
324 return Err(Error::new(
325 anyhow!(
326 "copy: {:?} -> {:?} failed, unsupported src file type: {:?}",
327 src,
328 dst,
329 src_metadata.file_type()
330 ),
331 Default::default(),
332 ));
333 }
334 tracing::debug!("process contents of 'src' directory");
335 let mut entries = tokio::fs::read_dir(src)
336 .await
337 .with_context(|| format!("cannot open directory {src:?} for reading"))
338 .map_err(|err| Error::new(err, Default::default()))?;
339 let mut copy_summary = {
340 if let Err(error) = tokio::fs::create_dir(dst).await {
341 assert!(
342 !is_fresh,
343 "unexpected error creating directory: {dst:?}: {error}"
344 );
345 if settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
346 let dst_metadata = tokio::fs::metadata(dst)
351 .await
352 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
353 .map_err(|err| Error::new(err, Default::default()))?;
354 if dst_metadata.is_dir() {
355 tracing::debug!("'dst' is a directory, leaving it as is");
356 prog_track.directories_unchanged.inc();
357 Summary {
358 directories_unchanged: 1,
359 ..Default::default()
360 }
361 } else {
362 tracing::info!("'dst' is not a directory, removing and creating a new one");
363 let rm_summary = rm::rm(
364 prog_track,
365 dst,
366 &RmSettings {
367 fail_early: settings.fail_early,
368 },
369 )
370 .await
371 .map_err(|err| {
372 let rm_summary = err.summary;
373 let copy_summary = Summary {
374 rm_summary,
375 ..Default::default()
376 };
377 Error::new(err.source, copy_summary)
378 })?;
379 tokio::fs::create_dir(dst)
380 .await
381 .with_context(|| format!("cannot create directory {dst:?}"))
382 .map_err(|err| {
383 let copy_summary = Summary {
384 rm_summary,
385 ..Default::default()
386 };
387 Error::new(anyhow::Error::msg(err), copy_summary)
388 })?;
389 is_fresh = true;
391 prog_track.directories_created.inc();
392 Summary {
393 rm_summary,
394 directories_created: 1,
395 ..Default::default()
396 }
397 }
398 } else {
399 tracing::error!("{:?}", &error);
400 return Err(Error::new(
401 anyhow!("cannot create directory {:?}", dst),
402 Default::default(),
403 ));
404 }
405 } else {
406 is_fresh = true;
408 prog_track.directories_created.inc();
409 Summary {
410 directories_created: 1,
411 ..Default::default()
412 }
413 }
414 };
415 let mut join_set = tokio::task::JoinSet::new();
416 let mut success = true;
417 while let Some(entry) = entries
418 .next_entry()
419 .await
420 .with_context(|| format!("failed traversing src directory {:?}", &src))
421 .map_err(|err| Error::new(err, copy_summary))?
422 {
423 throttle::get_ops_token().await;
427 let entry_path = entry.path();
428 let entry_name = entry_path.file_name().unwrap();
429 let dst_path = dst.join(entry_name);
430 let settings = *settings;
431 let preserve = *preserve;
432 let do_copy = || async move {
433 copy(
434 prog_track,
435 &entry_path,
436 &dst_path,
437 &settings,
438 &preserve,
439 is_fresh,
440 )
441 .await
442 };
443 join_set.spawn(do_copy());
444 }
445 drop(entries);
448 while let Some(res) = join_set.join_next().await {
449 match res {
450 Ok(result) => match result {
451 Ok(summary) => copy_summary = copy_summary + summary,
452 Err(error) => {
453 tracing::error!("copy: {:?} -> {:?} failed with: {}", src, dst, &error);
454 copy_summary = copy_summary + error.summary;
455 if settings.fail_early {
456 return Err(Error::new(error.source, copy_summary));
457 }
458 success = false;
459 }
460 },
461 Err(error) => {
462 if settings.fail_early {
463 return Err(Error::new(anyhow::Error::msg(error), copy_summary));
464 }
465 }
466 }
467 }
468 if !success {
469 return Err(Error::new(
470 anyhow!("copy: {:?} -> {:?} failed!", src, dst),
471 copy_summary,
472 ))?;
473 }
474 tracing::debug!("set 'dst' directory metadata");
475 preserve::set_dir_metadata(preserve, &src_metadata, dst)
476 .await
477 .map_err(|err| Error::new(err, copy_summary))?;
478 Ok(copy_summary)
479}
480
481#[cfg(test)]
482mod copy_tests {
483 use crate::testutils;
484 use anyhow::Context;
485 use std::os::unix::fs::PermissionsExt;
486 use tracing_test::traced_test;
487
488 use super::*;
489
490 lazy_static! {
491 static ref PROGRESS: progress::Progress = progress::Progress::new();
492 static ref NO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_default();
493 static ref DO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_all();
494 }
495
496 #[tokio::test]
497 #[traced_test]
498 async fn check_basic_copy() -> Result<(), anyhow::Error> {
499 let tmp_dir = testutils::setup_test_dir().await?;
500 let test_path = tmp_dir.as_path();
501 let summary = copy(
502 &PROGRESS,
503 &test_path.join("foo"),
504 &test_path.join("bar"),
505 &Settings {
506 dereference: false,
507 fail_early: false,
508 overwrite: false,
509 overwrite_compare: filecmp::MetadataCmpSettings {
510 size: true,
511 mtime: true,
512 ..Default::default()
513 },
514 chunk_size: 0,
515 },
516 &NO_PRESERVE_SETTINGS,
517 false,
518 )
519 .await?;
520 assert_eq!(summary.files_copied, 5);
521 assert_eq!(summary.symlinks_created, 2);
522 assert_eq!(summary.directories_created, 3);
523 testutils::check_dirs_identical(
524 &test_path.join("foo"),
525 &test_path.join("bar"),
526 testutils::FileEqualityCheck::Basic,
527 )
528 .await?;
529 Ok(())
530 }
531
532 #[tokio::test]
533 #[traced_test]
534 async fn no_read_permission() -> Result<(), anyhow::Error> {
535 let tmp_dir = testutils::setup_test_dir().await?;
536 let test_path = tmp_dir.as_path();
537 let filepaths = vec![
538 test_path.join("foo").join("0.txt"),
539 test_path.join("foo").join("baz"),
540 ];
541 for fpath in &filepaths {
542 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o000)).await?;
544 }
545 match copy(
546 &PROGRESS,
547 &test_path.join("foo"),
548 &test_path.join("bar"),
549 &Settings {
550 dereference: false,
551 fail_early: false,
552 overwrite: false,
553 overwrite_compare: filecmp::MetadataCmpSettings {
554 size: true,
555 mtime: true,
556 ..Default::default()
557 },
558 chunk_size: 0,
559 },
560 &NO_PRESERVE_SETTINGS,
561 false,
562 )
563 .await
564 {
565 Ok(_) => panic!("Expected the copy to error!"),
566 Err(error) => {
567 tracing::info!("{}", &error);
568 assert_eq!(error.summary.files_copied, 3);
579 assert_eq!(error.summary.symlinks_created, 0);
580 assert_eq!(error.summary.directories_created, 2);
581 }
582 }
583 for fpath in &filepaths {
585 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o700)).await?;
586 if tokio::fs::symlink_metadata(fpath).await?.is_file() {
587 tokio::fs::remove_file(fpath).await?;
588 } else {
589 tokio::fs::remove_dir_all(fpath).await?;
590 }
591 }
592 testutils::check_dirs_identical(
593 &test_path.join("foo"),
594 &test_path.join("bar"),
595 testutils::FileEqualityCheck::Basic,
596 )
597 .await?;
598 Ok(())
599 }
600
601 #[tokio::test]
602 #[traced_test]
603 async fn check_default_mode() -> Result<(), anyhow::Error> {
604 let tmp_dir = testutils::setup_test_dir().await?;
605 tokio::fs::set_permissions(
607 tmp_dir.join("foo").join("0.txt"),
608 std::fs::Permissions::from_mode(0o700),
609 )
610 .await?;
611 let exec_sticky_file = tmp_dir.join("foo").join("bar").join("1.txt");
613 tokio::fs::set_permissions(&exec_sticky_file, std::fs::Permissions::from_mode(0o3770))
614 .await?;
615 let test_path = tmp_dir.as_path();
616 let summary = copy(
617 &PROGRESS,
618 &test_path.join("foo"),
619 &test_path.join("bar"),
620 &Settings {
621 dereference: false,
622 fail_early: false,
623 overwrite: false,
624 overwrite_compare: filecmp::MetadataCmpSettings {
625 size: true,
626 mtime: true,
627 ..Default::default()
628 },
629 chunk_size: 0,
630 },
631 &NO_PRESERVE_SETTINGS,
632 false,
633 )
634 .await?;
635 assert_eq!(summary.files_copied, 5);
636 assert_eq!(summary.symlinks_created, 2);
637 assert_eq!(summary.directories_created, 3);
638 tokio::fs::set_permissions(
640 &exec_sticky_file,
641 std::fs::Permissions::from_mode(
642 std::fs::symlink_metadata(&exec_sticky_file)?
643 .permissions()
644 .mode()
645 & 0o0777,
646 ),
647 )
648 .await?;
649 testutils::check_dirs_identical(
650 &test_path.join("foo"),
651 &test_path.join("bar"),
652 testutils::FileEqualityCheck::Basic,
653 )
654 .await?;
655 Ok(())
656 }
657
658 #[tokio::test]
659 #[traced_test]
660 async fn no_write_permission() -> Result<(), anyhow::Error> {
661 let tmp_dir = testutils::setup_test_dir().await?;
662 let test_path = tmp_dir.as_path();
663 let non_exec_dir = test_path.join("foo").join("bogey");
665 tokio::fs::create_dir(&non_exec_dir).await?;
666 tokio::fs::set_permissions(&non_exec_dir, std::fs::Permissions::from_mode(0o400)).await?;
667 tokio::fs::set_permissions(
669 &test_path.join("foo").join("baz"),
670 std::fs::Permissions::from_mode(0o500),
671 )
672 .await?;
673 tokio::fs::set_permissions(
675 &test_path.join("foo").join("baz").join("4.txt"),
676 std::fs::Permissions::from_mode(0o440),
677 )
678 .await?;
679 let summary = copy(
680 &PROGRESS,
681 &test_path.join("foo"),
682 &test_path.join("bar"),
683 &Settings {
684 dereference: false,
685 fail_early: false,
686 overwrite: false,
687 overwrite_compare: filecmp::MetadataCmpSettings {
688 size: true,
689 mtime: true,
690 ..Default::default()
691 },
692 chunk_size: 0,
693 },
694 &NO_PRESERVE_SETTINGS,
695 false,
696 )
697 .await?;
698 assert_eq!(summary.files_copied, 5);
699 assert_eq!(summary.symlinks_created, 2);
700 assert_eq!(summary.directories_created, 4);
701 testutils::check_dirs_identical(
702 &test_path.join("foo"),
703 &test_path.join("bar"),
704 testutils::FileEqualityCheck::Basic,
705 )
706 .await?;
707 Ok(())
708 }
709
710 #[tokio::test]
711 #[traced_test]
712 async fn dereference() -> Result<(), anyhow::Error> {
713 let tmp_dir = testutils::setup_test_dir().await?;
714 let test_path = tmp_dir.as_path();
715 let src1 = &test_path.join("foo").join("bar").join("2.txt");
717 let src2 = &test_path.join("foo").join("bar").join("3.txt");
718 let test_mode = 0o440;
719 for f in [src1, src2] {
720 tokio::fs::set_permissions(f, std::fs::Permissions::from_mode(test_mode)).await?;
721 }
722 let summary = copy(
723 &PROGRESS,
724 &test_path.join("foo"),
725 &test_path.join("bar"),
726 &Settings {
727 dereference: true, fail_early: false,
729 overwrite: false,
730 overwrite_compare: filecmp::MetadataCmpSettings {
731 size: true,
732 mtime: true,
733 ..Default::default()
734 },
735 chunk_size: 0,
736 },
737 &NO_PRESERVE_SETTINGS,
738 false,
739 )
740 .await?;
741 assert_eq!(summary.files_copied, 7);
742 assert_eq!(summary.symlinks_created, 0);
743 assert_eq!(summary.directories_created, 3);
744 let dst1 = &test_path.join("bar").join("baz").join("5.txt");
750 let dst2 = &test_path.join("bar").join("baz").join("6.txt");
751 for f in [dst1, dst2] {
752 let metadata = tokio::fs::symlink_metadata(f)
753 .await
754 .with_context(|| format!("failed reading metadata from {:?}", &f))?;
755 assert!(metadata.is_file());
756 assert_eq!(metadata.permissions().mode() & 0o777, test_mode);
758 }
759 Ok(())
760 }
761
762 async fn cp_compare(
763 cp_args: &[&str],
764 rcp_settings: &Settings,
765 preserve: bool,
766 ) -> Result<(), anyhow::Error> {
767 let tmp_dir = testutils::setup_test_dir().await?;
768 let test_path = tmp_dir.as_path();
769 let cp_output = tokio::process::Command::new("cp")
771 .args(cp_args)
772 .arg(test_path.join("foo"))
773 .arg(test_path.join("bar"))
774 .output()
775 .await?;
776 assert!(cp_output.status.success());
777 let summary = copy(
779 &PROGRESS,
780 &test_path.join("foo"),
781 &test_path.join("baz"),
782 rcp_settings,
783 if preserve {
784 &DO_PRESERVE_SETTINGS
785 } else {
786 &NO_PRESERVE_SETTINGS
787 },
788 false,
789 )
790 .await?;
791 if rcp_settings.dereference {
792 assert_eq!(summary.files_copied, 7);
793 assert_eq!(summary.symlinks_created, 0);
794 } else {
795 assert_eq!(summary.files_copied, 5);
796 assert_eq!(summary.symlinks_created, 2);
797 }
798 assert_eq!(summary.directories_created, 3);
799 testutils::check_dirs_identical(
800 &test_path.join("bar"),
801 &test_path.join("baz"),
802 if preserve {
803 testutils::FileEqualityCheck::Timestamp
804 } else {
805 testutils::FileEqualityCheck::Basic
806 },
807 )
808 .await?;
809 Ok(())
810 }
811
812 #[tokio::test]
813 #[traced_test]
814 async fn test_cp_compat() -> Result<(), anyhow::Error> {
815 cp_compare(
816 &["-r"],
817 &Settings {
818 dereference: false,
819 fail_early: false,
820 overwrite: false,
821 overwrite_compare: filecmp::MetadataCmpSettings {
822 size: true,
823 mtime: true,
824 ..Default::default()
825 },
826 chunk_size: 0,
827 },
828 false,
829 )
830 .await?;
831 Ok(())
832 }
833
834 #[tokio::test]
835 #[traced_test]
836 async fn test_cp_compat_preserve() -> Result<(), anyhow::Error> {
837 cp_compare(
838 &["-r", "-p"],
839 &Settings {
840 dereference: false,
841 fail_early: false,
842 overwrite: false,
843 overwrite_compare: filecmp::MetadataCmpSettings {
844 size: true,
845 mtime: true,
846 ..Default::default()
847 },
848 chunk_size: 0,
849 },
850 true,
851 )
852 .await?;
853 Ok(())
854 }
855
856 #[tokio::test]
857 #[traced_test]
858 async fn test_cp_compat_dereference() -> Result<(), anyhow::Error> {
859 cp_compare(
860 &["-r", "-L"],
861 &Settings {
862 dereference: true,
863 fail_early: false,
864 overwrite: false,
865 overwrite_compare: filecmp::MetadataCmpSettings {
866 size: true,
867 mtime: true,
868 ..Default::default()
869 },
870 chunk_size: 0,
871 },
872 false,
873 )
874 .await?;
875 Ok(())
876 }
877
878 #[tokio::test]
879 #[traced_test]
880 async fn test_cp_compat_preserve_and_dereference() -> Result<(), anyhow::Error> {
881 cp_compare(
882 &["-r", "-p", "-L"],
883 &Settings {
884 dereference: true,
885 fail_early: false,
886 overwrite: false,
887 overwrite_compare: filecmp::MetadataCmpSettings {
888 size: true,
889 mtime: true,
890 ..Default::default()
891 },
892 chunk_size: 0,
893 },
894 true,
895 )
896 .await?;
897 Ok(())
898 }
899
900 async fn setup_test_dir_and_copy() -> Result<std::path::PathBuf, anyhow::Error> {
901 let tmp_dir = testutils::setup_test_dir().await?;
902 let test_path = tmp_dir.as_path();
903 let summary = copy(
904 &PROGRESS,
905 &test_path.join("foo"),
906 &test_path.join("bar"),
907 &Settings {
908 dereference: false,
909 fail_early: false,
910 overwrite: false,
911 overwrite_compare: filecmp::MetadataCmpSettings {
912 size: true,
913 mtime: true,
914 ..Default::default()
915 },
916 chunk_size: 0,
917 },
918 &DO_PRESERVE_SETTINGS,
919 false,
920 )
921 .await?;
922 assert_eq!(summary.files_copied, 5);
923 assert_eq!(summary.symlinks_created, 2);
924 assert_eq!(summary.directories_created, 3);
925 Ok(tmp_dir)
926 }
927
928 #[tokio::test]
929 #[traced_test]
930 async fn test_cp_overwrite_basic() -> Result<(), anyhow::Error> {
931 let tmp_dir = setup_test_dir_and_copy().await?;
932 let output_path = &tmp_dir.join("bar");
933 {
934 let summary = rm::rm(
945 &PROGRESS,
946 &output_path.join("bar"),
947 &RmSettings { fail_early: false },
948 )
949 .await?
950 + rm::rm(
951 &PROGRESS,
952 &output_path.join("baz").join("5.txt"),
953 &RmSettings { fail_early: false },
954 )
955 .await?;
956 assert_eq!(summary.files_removed, 3);
957 assert_eq!(summary.symlinks_removed, 1);
958 assert_eq!(summary.directories_removed, 1);
959 }
960 let summary = copy(
961 &PROGRESS,
962 &tmp_dir.join("foo"),
963 output_path,
964 &Settings {
965 dereference: false,
966 fail_early: false,
967 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
969 size: true,
970 mtime: true,
971 ..Default::default()
972 },
973 chunk_size: 0,
974 },
975 &DO_PRESERVE_SETTINGS,
976 false,
977 )
978 .await?;
979 assert_eq!(summary.files_copied, 3);
980 assert_eq!(summary.symlinks_created, 1);
981 assert_eq!(summary.directories_created, 1);
982 testutils::check_dirs_identical(
983 &tmp_dir.join("foo"),
984 output_path,
985 testutils::FileEqualityCheck::Timestamp,
986 )
987 .await?;
988 Ok(())
989 }
990
991 #[tokio::test]
992 #[traced_test]
993 async fn test_cp_overwrite_dir_file() -> Result<(), anyhow::Error> {
994 let tmp_dir = setup_test_dir_and_copy().await?;
995 let output_path = &tmp_dir.join("bar");
996 {
997 let summary = rm::rm(
1008 &PROGRESS,
1009 &output_path.join("bar").join("1.txt"),
1010 &RmSettings { fail_early: false },
1011 )
1012 .await?
1013 + rm::rm(
1014 &PROGRESS,
1015 &output_path.join("baz"),
1016 &RmSettings { fail_early: false },
1017 )
1018 .await?;
1019 assert_eq!(summary.files_removed, 2);
1020 assert_eq!(summary.symlinks_removed, 2);
1021 assert_eq!(summary.directories_removed, 1);
1022 }
1023 {
1024 tokio::fs::create_dir(&output_path.join("bar").join("1.txt")).await?;
1026 tokio::fs::write(&output_path.join("baz"), "baz").await?;
1028 }
1029 let summary = copy(
1030 &PROGRESS,
1031 &tmp_dir.join("foo"),
1032 output_path,
1033 &Settings {
1034 dereference: false,
1035 fail_early: false,
1036 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1038 size: true,
1039 mtime: true,
1040 ..Default::default()
1041 },
1042 chunk_size: 0,
1043 },
1044 &DO_PRESERVE_SETTINGS,
1045 false,
1046 )
1047 .await?;
1048 assert_eq!(summary.rm_summary.files_removed, 1);
1049 assert_eq!(summary.rm_summary.symlinks_removed, 0);
1050 assert_eq!(summary.rm_summary.directories_removed, 1);
1051 assert_eq!(summary.files_copied, 2);
1052 assert_eq!(summary.symlinks_created, 2);
1053 assert_eq!(summary.directories_created, 1);
1054 testutils::check_dirs_identical(
1055 &tmp_dir.join("foo"),
1056 output_path,
1057 testutils::FileEqualityCheck::Timestamp,
1058 )
1059 .await?;
1060 Ok(())
1061 }
1062
1063 #[tokio::test]
1064 #[traced_test]
1065 async fn test_cp_overwrite_symlink_file() -> Result<(), anyhow::Error> {
1066 let tmp_dir = setup_test_dir_and_copy().await?;
1067 let output_path = &tmp_dir.join("bar");
1068 {
1069 let summary = rm::rm(
1076 &PROGRESS,
1077 &output_path.join("baz").join("4.txt"),
1078 &RmSettings { fail_early: false },
1079 )
1080 .await?
1081 + rm::rm(
1082 &PROGRESS,
1083 &output_path.join("baz").join("5.txt"),
1084 &RmSettings { fail_early: false },
1085 )
1086 .await?;
1087 assert_eq!(summary.files_removed, 1);
1088 assert_eq!(summary.symlinks_removed, 1);
1089 assert_eq!(summary.directories_removed, 0);
1090 }
1091 {
1092 tokio::fs::symlink("../0.txt", &output_path.join("baz").join("4.txt")).await?;
1094 tokio::fs::write(&output_path.join("baz").join("5.txt"), "baz").await?;
1096 }
1097 let summary = copy(
1098 &PROGRESS,
1099 &tmp_dir.join("foo"),
1100 output_path,
1101 &Settings {
1102 dereference: false,
1103 fail_early: false,
1104 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1106 size: true,
1107 mtime: true,
1108 ..Default::default()
1109 },
1110 chunk_size: 0,
1111 },
1112 &DO_PRESERVE_SETTINGS,
1113 false,
1114 )
1115 .await?;
1116 assert_eq!(summary.rm_summary.files_removed, 1);
1117 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1118 assert_eq!(summary.rm_summary.directories_removed, 0);
1119 assert_eq!(summary.files_copied, 1);
1120 assert_eq!(summary.symlinks_created, 1);
1121 assert_eq!(summary.directories_created, 0);
1122 testutils::check_dirs_identical(
1123 &tmp_dir.join("foo"),
1124 output_path,
1125 testutils::FileEqualityCheck::Timestamp,
1126 )
1127 .await?;
1128 Ok(())
1129 }
1130
1131 #[tokio::test]
1132 #[traced_test]
1133 async fn test_cp_overwrite_symlink_dir() -> Result<(), anyhow::Error> {
1134 let tmp_dir = setup_test_dir_and_copy().await?;
1135 let output_path = &tmp_dir.join("bar");
1136 {
1137 let summary = rm::rm(
1147 &PROGRESS,
1148 &output_path.join("bar"),
1149 &RmSettings { fail_early: false },
1150 )
1151 .await?
1152 + rm::rm(
1153 &PROGRESS,
1154 &output_path.join("baz").join("5.txt"),
1155 &RmSettings { fail_early: false },
1156 )
1157 .await?;
1158 assert_eq!(summary.files_removed, 3);
1159 assert_eq!(summary.symlinks_removed, 1);
1160 assert_eq!(summary.directories_removed, 1);
1161 }
1162 {
1163 tokio::fs::symlink("0.txt", &output_path.join("bar")).await?;
1165 tokio::fs::create_dir(&output_path.join("baz").join("5.txt")).await?;
1167 }
1168 let summary = copy(
1169 &PROGRESS,
1170 &tmp_dir.join("foo"),
1171 output_path,
1172 &Settings {
1173 dereference: false,
1174 fail_early: false,
1175 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1177 size: true,
1178 mtime: true,
1179 ..Default::default()
1180 },
1181 chunk_size: 0,
1182 },
1183 &DO_PRESERVE_SETTINGS,
1184 false,
1185 )
1186 .await?;
1187 assert_eq!(summary.rm_summary.files_removed, 0);
1188 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1189 assert_eq!(summary.rm_summary.directories_removed, 1);
1190 assert_eq!(summary.files_copied, 3);
1191 assert_eq!(summary.symlinks_created, 1);
1192 assert_eq!(summary.directories_created, 1);
1193 assert_eq!(summary.files_unchanged, 2);
1194 assert_eq!(summary.symlinks_unchanged, 1);
1195 assert_eq!(summary.directories_unchanged, 2);
1196 testutils::check_dirs_identical(
1197 &tmp_dir.join("foo"),
1198 output_path,
1199 testutils::FileEqualityCheck::Timestamp,
1200 )
1201 .await?;
1202 Ok(())
1203 }
1204
1205 #[tokio::test]
1206 #[traced_test]
1207 async fn test_cp_overwrite_error() -> Result<(), anyhow::Error> {
1208 let tmp_dir = testutils::setup_test_dir().await?;
1209 let test_path = tmp_dir.as_path();
1210 let summary = copy(
1211 &PROGRESS,
1212 &test_path.join("foo"),
1213 &test_path.join("bar"),
1214 &Settings {
1215 dereference: false,
1216 fail_early: false,
1217 overwrite: false,
1218 overwrite_compare: filecmp::MetadataCmpSettings {
1219 size: true,
1220 mtime: true,
1221 ..Default::default()
1222 },
1223 chunk_size: 0,
1224 },
1225 &NO_PRESERVE_SETTINGS, false,
1227 )
1228 .await?;
1229 assert_eq!(summary.files_copied, 5);
1230 assert_eq!(summary.symlinks_created, 2);
1231 assert_eq!(summary.directories_created, 3);
1232 let source_path = &test_path.join("foo");
1233 let output_path = &tmp_dir.join("bar");
1234 tokio::fs::set_permissions(
1236 &source_path.join("bar"),
1237 std::fs::Permissions::from_mode(0o000),
1238 )
1239 .await?;
1240 tokio::fs::set_permissions(
1241 &source_path.join("baz").join("4.txt"),
1242 std::fs::Permissions::from_mode(0o000),
1243 )
1244 .await?;
1245 match copy(
1253 &PROGRESS,
1254 &tmp_dir.join("foo"),
1255 output_path,
1256 &Settings {
1257 dereference: false,
1258 fail_early: false,
1259 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1261 size: true,
1262 mtime: true,
1263 ..Default::default()
1264 },
1265 chunk_size: 0,
1266 },
1267 &DO_PRESERVE_SETTINGS,
1268 false,
1269 )
1270 .await
1271 {
1272 Ok(_) => panic!("Expected the copy to error!"),
1273 Err(error) => {
1274 tracing::info!("{}", &error);
1275 assert_eq!(error.summary.files_copied, 1);
1276 assert_eq!(error.summary.symlinks_created, 2);
1277 assert_eq!(error.summary.directories_created, 0);
1278 assert_eq!(error.summary.rm_summary.files_removed, 2);
1279 assert_eq!(error.summary.rm_summary.symlinks_removed, 2);
1280 assert_eq!(error.summary.rm_summary.directories_removed, 0);
1281 }
1282 }
1283 Ok(())
1284 }
1285
1286 #[tokio::test]
1287 #[traced_test]
1288 async fn test_cp_dereference_symlink_chain() -> Result<(), anyhow::Error> {
1289 let tmp_dir = testutils::create_temp_dir().await?;
1291 let test_path = tmp_dir.as_path();
1292 let baz_file = test_path.join("baz_file.txt");
1294 tokio::fs::write(&baz_file, "final content").await?;
1295 let bar_link = test_path.join("bar_link");
1296 let foo_link = test_path.join("foo_link");
1297 tokio::fs::symlink(&baz_file, &bar_link).await?;
1299 tokio::fs::symlink(&bar_link, &foo_link).await?;
1300 let src_dir = test_path.join("src_chain");
1302 tokio::fs::create_dir(&src_dir).await?;
1303 tokio::fs::symlink("../foo_link", &src_dir.join("foo")).await?;
1305 tokio::fs::symlink("../bar_link", &src_dir.join("bar")).await?;
1306 tokio::fs::symlink("../baz_file.txt", &src_dir.join("baz")).await?;
1307 let summary = copy(
1309 &PROGRESS,
1310 &src_dir,
1311 &test_path.join("dst_with_deref"),
1312 &Settings {
1313 dereference: true, fail_early: false,
1315 overwrite: false,
1316 overwrite_compare: filecmp::MetadataCmpSettings {
1317 size: true,
1318 mtime: true,
1319 ..Default::default()
1320 },
1321 chunk_size: 0,
1322 },
1323 &NO_PRESERVE_SETTINGS,
1324 false,
1325 )
1326 .await?;
1327 assert_eq!(summary.files_copied, 3); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 1);
1330 let dst_dir = test_path.join("dst_with_deref");
1331 let foo_content = tokio::fs::read_to_string(dst_dir.join("foo")).await?;
1333 let bar_content = tokio::fs::read_to_string(dst_dir.join("bar")).await?;
1334 let baz_content = tokio::fs::read_to_string(dst_dir.join("baz")).await?;
1335 assert_eq!(foo_content, "final content");
1336 assert_eq!(bar_content, "final content");
1337 assert_eq!(baz_content, "final content");
1338 assert!(dst_dir.join("foo").is_file());
1340 assert!(dst_dir.join("bar").is_file());
1341 assert!(dst_dir.join("baz").is_file());
1342 assert!(!dst_dir.join("foo").is_symlink());
1343 assert!(!dst_dir.join("bar").is_symlink());
1344 assert!(!dst_dir.join("baz").is_symlink());
1345 Ok(())
1346 }
1347
1348 #[tokio::test]
1349 #[traced_test]
1350 async fn test_cp_dereference_symlink_to_directory() -> Result<(), anyhow::Error> {
1351 let tmp_dir = testutils::create_temp_dir().await?;
1352 let test_path = tmp_dir.as_path();
1353 let target_dir = test_path.join("target_dir");
1355 tokio::fs::create_dir(&target_dir).await?;
1356 tokio::fs::set_permissions(&target_dir, std::fs::Permissions::from_mode(0o755)).await?;
1357 tokio::fs::write(target_dir.join("file1.txt"), "content1").await?;
1359 tokio::fs::write(target_dir.join("file2.txt"), "content2").await?;
1360 tokio::fs::set_permissions(
1361 &target_dir.join("file1.txt"),
1362 std::fs::Permissions::from_mode(0o644),
1363 )
1364 .await?;
1365 tokio::fs::set_permissions(
1366 &target_dir.join("file2.txt"),
1367 std::fs::Permissions::from_mode(0o600),
1368 )
1369 .await?;
1370 let dir_symlink = test_path.join("dir_symlink");
1372 tokio::fs::symlink(&target_dir, &dir_symlink).await?;
1373 let summary = copy(
1375 &PROGRESS,
1376 &dir_symlink,
1377 &test_path.join("copied_dir"),
1378 &Settings {
1379 dereference: true, fail_early: false,
1381 overwrite: false,
1382 overwrite_compare: filecmp::MetadataCmpSettings {
1383 size: true,
1384 mtime: true,
1385 ..Default::default()
1386 },
1387 chunk_size: 0,
1388 },
1389 &DO_PRESERVE_SETTINGS,
1390 false,
1391 )
1392 .await?;
1393 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");
1397 assert!(copied_dir.is_dir());
1399 assert!(!copied_dir.is_symlink()); let file1_content = tokio::fs::read_to_string(copied_dir.join("file1.txt")).await?;
1402 let file2_content = tokio::fs::read_to_string(copied_dir.join("file2.txt")).await?;
1403 assert_eq!(file1_content, "content1");
1404 assert_eq!(file2_content, "content2");
1405 let copied_dir_metadata = tokio::fs::metadata(&copied_dir).await?;
1407 let file1_metadata = tokio::fs::metadata(copied_dir.join("file1.txt")).await?;
1408 let file2_metadata = tokio::fs::metadata(copied_dir.join("file2.txt")).await?;
1409 assert_eq!(copied_dir_metadata.permissions().mode() & 0o777, 0o755);
1410 assert_eq!(file1_metadata.permissions().mode() & 0o777, 0o644);
1411 assert_eq!(file2_metadata.permissions().mode() & 0o777, 0o600);
1412 Ok(())
1413 }
1414
1415 #[tokio::test]
1416 #[traced_test]
1417 async fn test_cp_dereference_permissions_preserved() -> Result<(), anyhow::Error> {
1418 let tmp_dir = testutils::create_temp_dir().await?;
1419 let test_path = tmp_dir.as_path();
1420 let file1 = test_path.join("file1.txt");
1422 let file2 = test_path.join("file2.txt");
1423 tokio::fs::write(&file1, "content1").await?;
1424 tokio::fs::write(&file2, "content2").await?;
1425 tokio::fs::set_permissions(&file1, std::fs::Permissions::from_mode(0o755)).await?;
1426 tokio::fs::set_permissions(&file2, std::fs::Permissions::from_mode(0o640)).await?;
1427 let symlink1 = test_path.join("symlink1");
1429 let symlink2 = test_path.join("symlink2");
1430 tokio::fs::symlink(&file1, &symlink1).await?;
1431 tokio::fs::symlink(&file2, &symlink2).await?;
1432 let summary1 = copy(
1434 &PROGRESS,
1435 &symlink1,
1436 &test_path.join("copied_file1.txt"),
1437 &Settings {
1438 dereference: true, fail_early: false,
1440 overwrite: false,
1441 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1442 chunk_size: 0,
1443 },
1444 &DO_PRESERVE_SETTINGS, false,
1446 )
1447 .await?;
1448 let summary2 = copy(
1449 &PROGRESS,
1450 &symlink2,
1451 &test_path.join("copied_file2.txt"),
1452 &Settings {
1453 dereference: true,
1454 fail_early: false,
1455 overwrite: false,
1456 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1457 chunk_size: 0,
1458 },
1459 &DO_PRESERVE_SETTINGS,
1460 false,
1461 )
1462 .await?;
1463 assert_eq!(summary1.files_copied, 1);
1464 assert_eq!(summary1.symlinks_created, 0);
1465 assert_eq!(summary2.files_copied, 1);
1466 assert_eq!(summary2.symlinks_created, 0);
1467 let copied1 = test_path.join("copied_file1.txt");
1468 let copied2 = test_path.join("copied_file2.txt");
1469 assert!(copied1.is_file());
1471 assert!(!copied1.is_symlink());
1472 assert!(copied2.is_file());
1473 assert!(!copied2.is_symlink());
1474 let content1 = tokio::fs::read_to_string(&copied1).await?;
1476 let content2 = tokio::fs::read_to_string(&copied2).await?;
1477 assert_eq!(content1, "content1");
1478 assert_eq!(content2, "content2");
1479 let copied1_metadata = tokio::fs::metadata(&copied1).await?;
1481 let copied2_metadata = tokio::fs::metadata(&copied2).await?;
1482 assert_eq!(copied1_metadata.permissions().mode() & 0o777, 0o755);
1483 assert_eq!(copied2_metadata.permissions().mode() & 0o777, 0o640);
1484 Ok(())
1485 }
1486
1487 #[tokio::test]
1488 #[traced_test]
1489 async fn test_cp_dereference_dir() -> Result<(), anyhow::Error> {
1490 let tmp_dir = testutils::setup_test_dir().await?;
1491 tokio::fs::symlink("bar", &tmp_dir.join("foo").join("bar-link")).await?;
1493 tokio::fs::symlink("bar-link", &tmp_dir.join("foo").join("bar-link-link")).await?;
1495 let summary = copy(
1496 &PROGRESS,
1497 &tmp_dir.join("foo"),
1498 &tmp_dir.join("bar"),
1499 &Settings {
1500 dereference: true, fail_early: false,
1502 overwrite: false,
1503 overwrite_compare: filecmp::MetadataCmpSettings {
1504 size: true,
1505 mtime: true,
1506 ..Default::default()
1507 },
1508 chunk_size: 0,
1509 },
1510 &DO_PRESERVE_SETTINGS,
1511 false,
1512 )
1513 .await?;
1514 assert_eq!(summary.files_copied, 13); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 5);
1517 tokio::process::Command::new("cp")
1519 .args(["-r", "-L"])
1520 .arg(tmp_dir.join("foo"))
1521 .arg(tmp_dir.join("bar-cp"))
1522 .output()
1523 .await?;
1524 testutils::check_dirs_identical(
1525 &tmp_dir.join("bar"),
1526 &tmp_dir.join("bar-cp"),
1527 testutils::FileEqualityCheck::Basic,
1528 )
1529 .await?;
1530 Ok(())
1531 }
1532}