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!(!is_fresh, "unexpected error creating directory: {:?}", &dst);
342 if settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
343 let dst_metadata = tokio::fs::metadata(dst)
348 .await
349 .with_context(|| format!("failed reading metadata from dst: {:?}", &dst))
350 .map_err(|err| Error::new(err, Default::default()))?;
351 if dst_metadata.is_dir() {
352 tracing::debug!("'dst' is a directory, leaving it as is");
353 prog_track.directories_unchanged.inc();
354 Summary {
355 directories_unchanged: 1,
356 ..Default::default()
357 }
358 } else {
359 tracing::info!("'dst' is not a directory, removing and creating a new one");
360 let rm_summary = rm::rm(
361 prog_track,
362 dst,
363 &RmSettings {
364 fail_early: settings.fail_early,
365 },
366 )
367 .await
368 .map_err(|err| {
369 let rm_summary = err.summary;
370 let copy_summary = Summary {
371 rm_summary,
372 ..Default::default()
373 };
374 Error::new(err.source, copy_summary)
375 })?;
376 tokio::fs::create_dir(dst)
377 .await
378 .with_context(|| format!("cannot create directory {dst:?}"))
379 .map_err(|err| {
380 let copy_summary = Summary {
381 rm_summary,
382 ..Default::default()
383 };
384 Error::new(anyhow::Error::msg(err), copy_summary)
385 })?;
386 is_fresh = true;
388 prog_track.directories_created.inc();
389 Summary {
390 rm_summary,
391 directories_created: 1,
392 ..Default::default()
393 }
394 }
395 } else {
396 tracing::error!("{:?}", &error);
397 return Err(Error::new(
398 anyhow!("cannot create directory {:?}", dst),
399 Default::default(),
400 ));
401 }
402 } else {
403 is_fresh = true;
405 prog_track.directories_created.inc();
406 Summary {
407 directories_created: 1,
408 ..Default::default()
409 }
410 }
411 };
412 let mut join_set = tokio::task::JoinSet::new();
413 let mut success = true;
414 while let Some(entry) = entries
415 .next_entry()
416 .await
417 .with_context(|| format!("failed traversing src directory {:?}", &src))
418 .map_err(|err| Error::new(err, copy_summary))?
419 {
420 throttle::get_ops_token().await;
424 let entry_path = entry.path();
425 let entry_name = entry_path.file_name().unwrap();
426 let dst_path = dst.join(entry_name);
427 let settings = *settings;
428 let preserve = *preserve;
429 let do_copy = || async move {
430 copy(
431 prog_track,
432 &entry_path,
433 &dst_path,
434 &settings,
435 &preserve,
436 is_fresh,
437 )
438 .await
439 };
440 join_set.spawn(do_copy());
441 }
442 drop(entries);
445 while let Some(res) = join_set.join_next().await {
446 match res {
447 Ok(result) => match result {
448 Ok(summary) => copy_summary = copy_summary + summary,
449 Err(error) => {
450 tracing::error!("copy: {:?} -> {:?} failed with: {}", src, dst, &error);
451 copy_summary = copy_summary + error.summary;
452 if settings.fail_early {
453 return Err(Error::new(error.source, copy_summary));
454 }
455 success = false;
456 }
457 },
458 Err(error) => {
459 if settings.fail_early {
460 return Err(Error::new(anyhow::Error::msg(error), copy_summary));
461 }
462 }
463 }
464 }
465 if !success {
466 return Err(Error::new(
467 anyhow!("copy: {:?} -> {:?} failed!", src, dst),
468 copy_summary,
469 ))?;
470 }
471 tracing::debug!("set 'dst' directory metadata");
472 preserve::set_dir_metadata(preserve, &src_metadata, dst)
473 .await
474 .map_err(|err| Error::new(err, copy_summary))?;
475 Ok(copy_summary)
476}
477
478#[cfg(test)]
479mod copy_tests {
480 use crate::testutils;
481 use anyhow::Context;
482 use std::os::unix::fs::PermissionsExt;
483 use tracing_test::traced_test;
484
485 use super::*;
486
487 lazy_static! {
488 static ref PROGRESS: progress::Progress = progress::Progress::new();
489 static ref NO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_default();
490 static ref DO_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_all();
491 }
492
493 #[tokio::test]
494 #[traced_test]
495 async fn check_basic_copy() -> Result<(), anyhow::Error> {
496 let tmp_dir = testutils::setup_test_dir().await?;
497 let test_path = tmp_dir.as_path();
498 let summary = copy(
499 &PROGRESS,
500 &test_path.join("foo"),
501 &test_path.join("bar"),
502 &Settings {
503 dereference: false,
504 fail_early: false,
505 overwrite: false,
506 overwrite_compare: filecmp::MetadataCmpSettings {
507 size: true,
508 mtime: true,
509 ..Default::default()
510 },
511 chunk_size: 0,
512 },
513 &NO_PRESERVE_SETTINGS,
514 false,
515 )
516 .await?;
517 assert_eq!(summary.files_copied, 5);
518 assert_eq!(summary.symlinks_created, 2);
519 assert_eq!(summary.directories_created, 3);
520 testutils::check_dirs_identical(
521 &test_path.join("foo"),
522 &test_path.join("bar"),
523 testutils::FileEqualityCheck::Basic,
524 )
525 .await?;
526 Ok(())
527 }
528
529 #[tokio::test]
530 #[traced_test]
531 async fn no_read_permission() -> Result<(), anyhow::Error> {
532 let tmp_dir = testutils::setup_test_dir().await?;
533 let test_path = tmp_dir.as_path();
534 let filepaths = vec![
535 test_path.join("foo").join("0.txt"),
536 test_path.join("foo").join("baz"),
537 ];
538 for fpath in &filepaths {
539 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o000)).await?;
541 }
542 match copy(
543 &PROGRESS,
544 &test_path.join("foo"),
545 &test_path.join("bar"),
546 &Settings {
547 dereference: false,
548 fail_early: false,
549 overwrite: false,
550 overwrite_compare: filecmp::MetadataCmpSettings {
551 size: true,
552 mtime: true,
553 ..Default::default()
554 },
555 chunk_size: 0,
556 },
557 &NO_PRESERVE_SETTINGS,
558 false,
559 )
560 .await
561 {
562 Ok(_) => panic!("Expected the copy to error!"),
563 Err(error) => {
564 tracing::info!("{}", &error);
565 assert_eq!(error.summary.files_copied, 3);
576 assert_eq!(error.summary.symlinks_created, 0);
577 assert_eq!(error.summary.directories_created, 2);
578 }
579 }
580 for fpath in &filepaths {
582 tokio::fs::set_permissions(&fpath, std::fs::Permissions::from_mode(0o700)).await?;
583 if tokio::fs::symlink_metadata(fpath).await?.is_file() {
584 tokio::fs::remove_file(fpath).await?;
585 } else {
586 tokio::fs::remove_dir_all(fpath).await?;
587 }
588 }
589 testutils::check_dirs_identical(
590 &test_path.join("foo"),
591 &test_path.join("bar"),
592 testutils::FileEqualityCheck::Basic,
593 )
594 .await?;
595 Ok(())
596 }
597
598 #[tokio::test]
599 #[traced_test]
600 async fn check_default_mode() -> Result<(), anyhow::Error> {
601 let tmp_dir = testutils::setup_test_dir().await?;
602 tokio::fs::set_permissions(
604 tmp_dir.join("foo").join("0.txt"),
605 std::fs::Permissions::from_mode(0o700),
606 )
607 .await?;
608 let exec_sticky_file = tmp_dir.join("foo").join("bar").join("1.txt");
610 tokio::fs::set_permissions(&exec_sticky_file, std::fs::Permissions::from_mode(0o3770))
611 .await?;
612 let test_path = tmp_dir.as_path();
613 let summary = copy(
614 &PROGRESS,
615 &test_path.join("foo"),
616 &test_path.join("bar"),
617 &Settings {
618 dereference: false,
619 fail_early: false,
620 overwrite: false,
621 overwrite_compare: filecmp::MetadataCmpSettings {
622 size: true,
623 mtime: true,
624 ..Default::default()
625 },
626 chunk_size: 0,
627 },
628 &NO_PRESERVE_SETTINGS,
629 false,
630 )
631 .await?;
632 assert_eq!(summary.files_copied, 5);
633 assert_eq!(summary.symlinks_created, 2);
634 assert_eq!(summary.directories_created, 3);
635 tokio::fs::set_permissions(
637 &exec_sticky_file,
638 std::fs::Permissions::from_mode(
639 std::fs::symlink_metadata(&exec_sticky_file)?
640 .permissions()
641 .mode()
642 & 0o0777,
643 ),
644 )
645 .await?;
646 testutils::check_dirs_identical(
647 &test_path.join("foo"),
648 &test_path.join("bar"),
649 testutils::FileEqualityCheck::Basic,
650 )
651 .await?;
652 Ok(())
653 }
654
655 #[tokio::test]
656 #[traced_test]
657 async fn no_write_permission() -> Result<(), anyhow::Error> {
658 let tmp_dir = testutils::setup_test_dir().await?;
659 let test_path = tmp_dir.as_path();
660 let non_exec_dir = test_path.join("foo").join("bogey");
662 tokio::fs::create_dir(&non_exec_dir).await?;
663 tokio::fs::set_permissions(&non_exec_dir, std::fs::Permissions::from_mode(0o400)).await?;
664 tokio::fs::set_permissions(
666 &test_path.join("foo").join("baz"),
667 std::fs::Permissions::from_mode(0o500),
668 )
669 .await?;
670 tokio::fs::set_permissions(
672 &test_path.join("foo").join("baz").join("4.txt"),
673 std::fs::Permissions::from_mode(0o440),
674 )
675 .await?;
676 let summary = copy(
677 &PROGRESS,
678 &test_path.join("foo"),
679 &test_path.join("bar"),
680 &Settings {
681 dereference: false,
682 fail_early: false,
683 overwrite: false,
684 overwrite_compare: filecmp::MetadataCmpSettings {
685 size: true,
686 mtime: true,
687 ..Default::default()
688 },
689 chunk_size: 0,
690 },
691 &NO_PRESERVE_SETTINGS,
692 false,
693 )
694 .await?;
695 assert_eq!(summary.files_copied, 5);
696 assert_eq!(summary.symlinks_created, 2);
697 assert_eq!(summary.directories_created, 4);
698 testutils::check_dirs_identical(
699 &test_path.join("foo"),
700 &test_path.join("bar"),
701 testutils::FileEqualityCheck::Basic,
702 )
703 .await?;
704 Ok(())
705 }
706
707 #[tokio::test]
708 #[traced_test]
709 async fn dereference() -> Result<(), anyhow::Error> {
710 let tmp_dir = testutils::setup_test_dir().await?;
711 let test_path = tmp_dir.as_path();
712 let src1 = &test_path.join("foo").join("bar").join("2.txt");
714 let src2 = &test_path.join("foo").join("bar").join("3.txt");
715 let test_mode = 0o440;
716 for f in [src1, src2] {
717 tokio::fs::set_permissions(f, std::fs::Permissions::from_mode(test_mode)).await?;
718 }
719 let summary = copy(
720 &PROGRESS,
721 &test_path.join("foo"),
722 &test_path.join("bar"),
723 &Settings {
724 dereference: true, fail_early: false,
726 overwrite: false,
727 overwrite_compare: filecmp::MetadataCmpSettings {
728 size: true,
729 mtime: true,
730 ..Default::default()
731 },
732 chunk_size: 0,
733 },
734 &NO_PRESERVE_SETTINGS,
735 false,
736 )
737 .await?;
738 assert_eq!(summary.files_copied, 7);
739 assert_eq!(summary.symlinks_created, 0);
740 assert_eq!(summary.directories_created, 3);
741 let dst1 = &test_path.join("bar").join("baz").join("5.txt");
747 let dst2 = &test_path.join("bar").join("baz").join("6.txt");
748 for f in [dst1, dst2] {
749 let metadata = tokio::fs::symlink_metadata(f)
750 .await
751 .with_context(|| format!("failed reading metadata from {:?}", &f))?;
752 assert!(metadata.is_file());
753 assert_eq!(metadata.permissions().mode() & 0o777, test_mode);
755 }
756 Ok(())
757 }
758
759 async fn cp_compare(
760 cp_args: &[&str],
761 rcp_settings: &Settings,
762 preserve: bool,
763 ) -> Result<(), anyhow::Error> {
764 let tmp_dir = testutils::setup_test_dir().await?;
765 let test_path = tmp_dir.as_path();
766 let cp_output = tokio::process::Command::new("cp")
768 .args(cp_args)
769 .arg(test_path.join("foo"))
770 .arg(test_path.join("bar"))
771 .output()
772 .await?;
773 assert!(cp_output.status.success());
774 let summary = copy(
776 &PROGRESS,
777 &test_path.join("foo"),
778 &test_path.join("baz"),
779 rcp_settings,
780 if preserve {
781 &DO_PRESERVE_SETTINGS
782 } else {
783 &NO_PRESERVE_SETTINGS
784 },
785 false,
786 )
787 .await?;
788 if rcp_settings.dereference {
789 assert_eq!(summary.files_copied, 7);
790 assert_eq!(summary.symlinks_created, 0);
791 } else {
792 assert_eq!(summary.files_copied, 5);
793 assert_eq!(summary.symlinks_created, 2);
794 }
795 assert_eq!(summary.directories_created, 3);
796 testutils::check_dirs_identical(
797 &test_path.join("bar"),
798 &test_path.join("baz"),
799 if preserve {
800 testutils::FileEqualityCheck::Timestamp
801 } else {
802 testutils::FileEqualityCheck::Basic
803 },
804 )
805 .await?;
806 Ok(())
807 }
808
809 #[tokio::test]
810 #[traced_test]
811 async fn test_cp_compat() -> Result<(), anyhow::Error> {
812 cp_compare(
813 &["-r"],
814 &Settings {
815 dereference: false,
816 fail_early: false,
817 overwrite: false,
818 overwrite_compare: filecmp::MetadataCmpSettings {
819 size: true,
820 mtime: true,
821 ..Default::default()
822 },
823 chunk_size: 0,
824 },
825 false,
826 )
827 .await?;
828 Ok(())
829 }
830
831 #[tokio::test]
832 #[traced_test]
833 async fn test_cp_compat_preserve() -> Result<(), anyhow::Error> {
834 cp_compare(
835 &["-r", "-p"],
836 &Settings {
837 dereference: false,
838 fail_early: false,
839 overwrite: false,
840 overwrite_compare: filecmp::MetadataCmpSettings {
841 size: true,
842 mtime: true,
843 ..Default::default()
844 },
845 chunk_size: 0,
846 },
847 true,
848 )
849 .await?;
850 Ok(())
851 }
852
853 #[tokio::test]
854 #[traced_test]
855 async fn test_cp_compat_dereference() -> Result<(), anyhow::Error> {
856 cp_compare(
857 &["-r", "-L"],
858 &Settings {
859 dereference: true,
860 fail_early: false,
861 overwrite: false,
862 overwrite_compare: filecmp::MetadataCmpSettings {
863 size: true,
864 mtime: true,
865 ..Default::default()
866 },
867 chunk_size: 0,
868 },
869 false,
870 )
871 .await?;
872 Ok(())
873 }
874
875 #[tokio::test]
876 #[traced_test]
877 async fn test_cp_compat_preserve_and_dereference() -> Result<(), anyhow::Error> {
878 cp_compare(
879 &["-r", "-p", "-L"],
880 &Settings {
881 dereference: true,
882 fail_early: false,
883 overwrite: false,
884 overwrite_compare: filecmp::MetadataCmpSettings {
885 size: true,
886 mtime: true,
887 ..Default::default()
888 },
889 chunk_size: 0,
890 },
891 true,
892 )
893 .await?;
894 Ok(())
895 }
896
897 async fn setup_test_dir_and_copy() -> Result<std::path::PathBuf, anyhow::Error> {
898 let tmp_dir = testutils::setup_test_dir().await?;
899 let test_path = tmp_dir.as_path();
900 let summary = copy(
901 &PROGRESS,
902 &test_path.join("foo"),
903 &test_path.join("bar"),
904 &Settings {
905 dereference: false,
906 fail_early: false,
907 overwrite: false,
908 overwrite_compare: filecmp::MetadataCmpSettings {
909 size: true,
910 mtime: true,
911 ..Default::default()
912 },
913 chunk_size: 0,
914 },
915 &DO_PRESERVE_SETTINGS,
916 false,
917 )
918 .await?;
919 assert_eq!(summary.files_copied, 5);
920 assert_eq!(summary.symlinks_created, 2);
921 assert_eq!(summary.directories_created, 3);
922 Ok(tmp_dir)
923 }
924
925 #[tokio::test]
926 #[traced_test]
927 async fn test_cp_overwrite_basic() -> Result<(), anyhow::Error> {
928 let tmp_dir = setup_test_dir_and_copy().await?;
929 let output_path = &tmp_dir.join("bar");
930 {
931 let summary = rm::rm(
942 &PROGRESS,
943 &output_path.join("bar"),
944 &RmSettings { fail_early: false },
945 )
946 .await?
947 + rm::rm(
948 &PROGRESS,
949 &output_path.join("baz").join("5.txt"),
950 &RmSettings { fail_early: false },
951 )
952 .await?;
953 assert_eq!(summary.files_removed, 3);
954 assert_eq!(summary.symlinks_removed, 1);
955 assert_eq!(summary.directories_removed, 1);
956 }
957 let summary = copy(
958 &PROGRESS,
959 &tmp_dir.join("foo"),
960 output_path,
961 &Settings {
962 dereference: false,
963 fail_early: false,
964 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
966 size: true,
967 mtime: true,
968 ..Default::default()
969 },
970 chunk_size: 0,
971 },
972 &DO_PRESERVE_SETTINGS,
973 false,
974 )
975 .await?;
976 assert_eq!(summary.files_copied, 3);
977 assert_eq!(summary.symlinks_created, 1);
978 assert_eq!(summary.directories_created, 1);
979 testutils::check_dirs_identical(
980 &tmp_dir.join("foo"),
981 output_path,
982 testutils::FileEqualityCheck::Timestamp,
983 )
984 .await?;
985 Ok(())
986 }
987
988 #[tokio::test]
989 #[traced_test]
990 async fn test_cp_overwrite_dir_file() -> Result<(), anyhow::Error> {
991 let tmp_dir = setup_test_dir_and_copy().await?;
992 let output_path = &tmp_dir.join("bar");
993 {
994 let summary = rm::rm(
1005 &PROGRESS,
1006 &output_path.join("bar").join("1.txt"),
1007 &RmSettings { fail_early: false },
1008 )
1009 .await?
1010 + rm::rm(
1011 &PROGRESS,
1012 &output_path.join("baz"),
1013 &RmSettings { fail_early: false },
1014 )
1015 .await?;
1016 assert_eq!(summary.files_removed, 2);
1017 assert_eq!(summary.symlinks_removed, 2);
1018 assert_eq!(summary.directories_removed, 1);
1019 }
1020 {
1021 tokio::fs::create_dir(&output_path.join("bar").join("1.txt")).await?;
1023 tokio::fs::write(&output_path.join("baz"), "baz").await?;
1025 }
1026 let summary = copy(
1027 &PROGRESS,
1028 &tmp_dir.join("foo"),
1029 output_path,
1030 &Settings {
1031 dereference: false,
1032 fail_early: false,
1033 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1035 size: true,
1036 mtime: true,
1037 ..Default::default()
1038 },
1039 chunk_size: 0,
1040 },
1041 &DO_PRESERVE_SETTINGS,
1042 false,
1043 )
1044 .await?;
1045 assert_eq!(summary.rm_summary.files_removed, 1);
1046 assert_eq!(summary.rm_summary.symlinks_removed, 0);
1047 assert_eq!(summary.rm_summary.directories_removed, 1);
1048 assert_eq!(summary.files_copied, 2);
1049 assert_eq!(summary.symlinks_created, 2);
1050 assert_eq!(summary.directories_created, 1);
1051 testutils::check_dirs_identical(
1052 &tmp_dir.join("foo"),
1053 output_path,
1054 testutils::FileEqualityCheck::Timestamp,
1055 )
1056 .await?;
1057 Ok(())
1058 }
1059
1060 #[tokio::test]
1061 #[traced_test]
1062 async fn test_cp_overwrite_symlink_file() -> Result<(), anyhow::Error> {
1063 let tmp_dir = setup_test_dir_and_copy().await?;
1064 let output_path = &tmp_dir.join("bar");
1065 {
1066 let summary = rm::rm(
1073 &PROGRESS,
1074 &output_path.join("baz").join("4.txt"),
1075 &RmSettings { fail_early: false },
1076 )
1077 .await?
1078 + rm::rm(
1079 &PROGRESS,
1080 &output_path.join("baz").join("5.txt"),
1081 &RmSettings { fail_early: false },
1082 )
1083 .await?;
1084 assert_eq!(summary.files_removed, 1);
1085 assert_eq!(summary.symlinks_removed, 1);
1086 assert_eq!(summary.directories_removed, 0);
1087 }
1088 {
1089 tokio::fs::symlink("../0.txt", &output_path.join("baz").join("4.txt")).await?;
1091 tokio::fs::write(&output_path.join("baz").join("5.txt"), "baz").await?;
1093 }
1094 let summary = copy(
1095 &PROGRESS,
1096 &tmp_dir.join("foo"),
1097 output_path,
1098 &Settings {
1099 dereference: false,
1100 fail_early: false,
1101 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1103 size: true,
1104 mtime: true,
1105 ..Default::default()
1106 },
1107 chunk_size: 0,
1108 },
1109 &DO_PRESERVE_SETTINGS,
1110 false,
1111 )
1112 .await?;
1113 assert_eq!(summary.rm_summary.files_removed, 1);
1114 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1115 assert_eq!(summary.rm_summary.directories_removed, 0);
1116 assert_eq!(summary.files_copied, 1);
1117 assert_eq!(summary.symlinks_created, 1);
1118 assert_eq!(summary.directories_created, 0);
1119 testutils::check_dirs_identical(
1120 &tmp_dir.join("foo"),
1121 output_path,
1122 testutils::FileEqualityCheck::Timestamp,
1123 )
1124 .await?;
1125 Ok(())
1126 }
1127
1128 #[tokio::test]
1129 #[traced_test]
1130 async fn test_cp_overwrite_symlink_dir() -> Result<(), anyhow::Error> {
1131 let tmp_dir = setup_test_dir_and_copy().await?;
1132 let output_path = &tmp_dir.join("bar");
1133 {
1134 let summary = rm::rm(
1144 &PROGRESS,
1145 &output_path.join("bar"),
1146 &RmSettings { fail_early: false },
1147 )
1148 .await?
1149 + rm::rm(
1150 &PROGRESS,
1151 &output_path.join("baz").join("5.txt"),
1152 &RmSettings { fail_early: false },
1153 )
1154 .await?;
1155 assert_eq!(summary.files_removed, 3);
1156 assert_eq!(summary.symlinks_removed, 1);
1157 assert_eq!(summary.directories_removed, 1);
1158 }
1159 {
1160 tokio::fs::symlink("0.txt", &output_path.join("bar")).await?;
1162 tokio::fs::create_dir(&output_path.join("baz").join("5.txt")).await?;
1164 }
1165 let summary = copy(
1166 &PROGRESS,
1167 &tmp_dir.join("foo"),
1168 output_path,
1169 &Settings {
1170 dereference: false,
1171 fail_early: false,
1172 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1174 size: true,
1175 mtime: true,
1176 ..Default::default()
1177 },
1178 chunk_size: 0,
1179 },
1180 &DO_PRESERVE_SETTINGS,
1181 false,
1182 )
1183 .await?;
1184 assert_eq!(summary.rm_summary.files_removed, 0);
1185 assert_eq!(summary.rm_summary.symlinks_removed, 1);
1186 assert_eq!(summary.rm_summary.directories_removed, 1);
1187 assert_eq!(summary.files_copied, 3);
1188 assert_eq!(summary.symlinks_created, 1);
1189 assert_eq!(summary.directories_created, 1);
1190 assert_eq!(summary.files_unchanged, 2);
1191 assert_eq!(summary.symlinks_unchanged, 1);
1192 assert_eq!(summary.directories_unchanged, 2);
1193 testutils::check_dirs_identical(
1194 &tmp_dir.join("foo"),
1195 output_path,
1196 testutils::FileEqualityCheck::Timestamp,
1197 )
1198 .await?;
1199 Ok(())
1200 }
1201
1202 #[tokio::test]
1203 #[traced_test]
1204 async fn test_cp_overwrite_error() -> Result<(), anyhow::Error> {
1205 let tmp_dir = testutils::setup_test_dir().await?;
1206 let test_path = tmp_dir.as_path();
1207 let summary = copy(
1208 &PROGRESS,
1209 &test_path.join("foo"),
1210 &test_path.join("bar"),
1211 &Settings {
1212 dereference: false,
1213 fail_early: false,
1214 overwrite: false,
1215 overwrite_compare: filecmp::MetadataCmpSettings {
1216 size: true,
1217 mtime: true,
1218 ..Default::default()
1219 },
1220 chunk_size: 0,
1221 },
1222 &NO_PRESERVE_SETTINGS, false,
1224 )
1225 .await?;
1226 assert_eq!(summary.files_copied, 5);
1227 assert_eq!(summary.symlinks_created, 2);
1228 assert_eq!(summary.directories_created, 3);
1229 let source_path = &test_path.join("foo");
1230 let output_path = &tmp_dir.join("bar");
1231 tokio::fs::set_permissions(
1233 &source_path.join("bar"),
1234 std::fs::Permissions::from_mode(0o000),
1235 )
1236 .await?;
1237 tokio::fs::set_permissions(
1238 &source_path.join("baz").join("4.txt"),
1239 std::fs::Permissions::from_mode(0o000),
1240 )
1241 .await?;
1242 match copy(
1250 &PROGRESS,
1251 &tmp_dir.join("foo"),
1252 output_path,
1253 &Settings {
1254 dereference: false,
1255 fail_early: false,
1256 overwrite: true, overwrite_compare: filecmp::MetadataCmpSettings {
1258 size: true,
1259 mtime: true,
1260 ..Default::default()
1261 },
1262 chunk_size: 0,
1263 },
1264 &DO_PRESERVE_SETTINGS,
1265 false,
1266 )
1267 .await
1268 {
1269 Ok(_) => panic!("Expected the copy to error!"),
1270 Err(error) => {
1271 tracing::info!("{}", &error);
1272 assert_eq!(error.summary.files_copied, 1);
1273 assert_eq!(error.summary.symlinks_created, 2);
1274 assert_eq!(error.summary.directories_created, 0);
1275 assert_eq!(error.summary.rm_summary.files_removed, 2);
1276 assert_eq!(error.summary.rm_summary.symlinks_removed, 2);
1277 assert_eq!(error.summary.rm_summary.directories_removed, 0);
1278 }
1279 }
1280 Ok(())
1281 }
1282
1283 #[tokio::test]
1284 #[traced_test]
1285 async fn test_cp_dereference_symlink_chain() -> Result<(), anyhow::Error> {
1286 let tmp_dir = testutils::create_temp_dir().await?;
1288 let test_path = tmp_dir.as_path();
1289 let baz_file = test_path.join("baz_file.txt");
1291 tokio::fs::write(&baz_file, "final content").await?;
1292 let bar_link = test_path.join("bar_link");
1293 let foo_link = test_path.join("foo_link");
1294 tokio::fs::symlink(&baz_file, &bar_link).await?;
1296 tokio::fs::symlink(&bar_link, &foo_link).await?;
1297 let src_dir = test_path.join("src_chain");
1299 tokio::fs::create_dir(&src_dir).await?;
1300 tokio::fs::symlink("../foo_link", &src_dir.join("foo")).await?;
1302 tokio::fs::symlink("../bar_link", &src_dir.join("bar")).await?;
1303 tokio::fs::symlink("../baz_file.txt", &src_dir.join("baz")).await?;
1304 let summary = copy(
1306 &PROGRESS,
1307 &src_dir,
1308 &test_path.join("dst_with_deref"),
1309 &Settings {
1310 dereference: true, fail_early: false,
1312 overwrite: false,
1313 overwrite_compare: filecmp::MetadataCmpSettings {
1314 size: true,
1315 mtime: true,
1316 ..Default::default()
1317 },
1318 chunk_size: 0,
1319 },
1320 &NO_PRESERVE_SETTINGS,
1321 false,
1322 )
1323 .await?;
1324 assert_eq!(summary.files_copied, 3); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 1);
1327 let dst_dir = test_path.join("dst_with_deref");
1328 let foo_content = tokio::fs::read_to_string(dst_dir.join("foo")).await?;
1330 let bar_content = tokio::fs::read_to_string(dst_dir.join("bar")).await?;
1331 let baz_content = tokio::fs::read_to_string(dst_dir.join("baz")).await?;
1332 assert_eq!(foo_content, "final content");
1333 assert_eq!(bar_content, "final content");
1334 assert_eq!(baz_content, "final content");
1335 assert!(dst_dir.join("foo").is_file());
1337 assert!(dst_dir.join("bar").is_file());
1338 assert!(dst_dir.join("baz").is_file());
1339 assert!(!dst_dir.join("foo").is_symlink());
1340 assert!(!dst_dir.join("bar").is_symlink());
1341 assert!(!dst_dir.join("baz").is_symlink());
1342 Ok(())
1343 }
1344
1345 #[tokio::test]
1346 #[traced_test]
1347 async fn test_cp_dereference_symlink_to_directory() -> Result<(), anyhow::Error> {
1348 let tmp_dir = testutils::create_temp_dir().await?;
1349 let test_path = tmp_dir.as_path();
1350 let target_dir = test_path.join("target_dir");
1352 tokio::fs::create_dir(&target_dir).await?;
1353 tokio::fs::set_permissions(&target_dir, std::fs::Permissions::from_mode(0o755)).await?;
1354 tokio::fs::write(target_dir.join("file1.txt"), "content1").await?;
1356 tokio::fs::write(target_dir.join("file2.txt"), "content2").await?;
1357 tokio::fs::set_permissions(
1358 &target_dir.join("file1.txt"),
1359 std::fs::Permissions::from_mode(0o644),
1360 )
1361 .await?;
1362 tokio::fs::set_permissions(
1363 &target_dir.join("file2.txt"),
1364 std::fs::Permissions::from_mode(0o600),
1365 )
1366 .await?;
1367 let dir_symlink = test_path.join("dir_symlink");
1369 tokio::fs::symlink(&target_dir, &dir_symlink).await?;
1370 let summary = copy(
1372 &PROGRESS,
1373 &dir_symlink,
1374 &test_path.join("copied_dir"),
1375 &Settings {
1376 dereference: true, fail_early: false,
1378 overwrite: false,
1379 overwrite_compare: filecmp::MetadataCmpSettings {
1380 size: true,
1381 mtime: true,
1382 ..Default::default()
1383 },
1384 chunk_size: 0,
1385 },
1386 &DO_PRESERVE_SETTINGS,
1387 false,
1388 )
1389 .await?;
1390 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");
1394 assert!(copied_dir.is_dir());
1396 assert!(!copied_dir.is_symlink()); let file1_content = tokio::fs::read_to_string(copied_dir.join("file1.txt")).await?;
1399 let file2_content = tokio::fs::read_to_string(copied_dir.join("file2.txt")).await?;
1400 assert_eq!(file1_content, "content1");
1401 assert_eq!(file2_content, "content2");
1402 let copied_dir_metadata = tokio::fs::metadata(&copied_dir).await?;
1404 let file1_metadata = tokio::fs::metadata(copied_dir.join("file1.txt")).await?;
1405 let file2_metadata = tokio::fs::metadata(copied_dir.join("file2.txt")).await?;
1406 assert_eq!(copied_dir_metadata.permissions().mode() & 0o777, 0o755);
1407 assert_eq!(file1_metadata.permissions().mode() & 0o777, 0o644);
1408 assert_eq!(file2_metadata.permissions().mode() & 0o777, 0o600);
1409 Ok(())
1410 }
1411
1412 #[tokio::test]
1413 #[traced_test]
1414 async fn test_cp_dereference_permissions_preserved() -> Result<(), anyhow::Error> {
1415 let tmp_dir = testutils::create_temp_dir().await?;
1416 let test_path = tmp_dir.as_path();
1417 let file1 = test_path.join("file1.txt");
1419 let file2 = test_path.join("file2.txt");
1420 tokio::fs::write(&file1, "content1").await?;
1421 tokio::fs::write(&file2, "content2").await?;
1422 tokio::fs::set_permissions(&file1, std::fs::Permissions::from_mode(0o755)).await?;
1423 tokio::fs::set_permissions(&file2, std::fs::Permissions::from_mode(0o640)).await?;
1424 let symlink1 = test_path.join("symlink1");
1426 let symlink2 = test_path.join("symlink2");
1427 tokio::fs::symlink(&file1, &symlink1).await?;
1428 tokio::fs::symlink(&file2, &symlink2).await?;
1429 let summary1 = copy(
1431 &PROGRESS,
1432 &symlink1,
1433 &test_path.join("copied_file1.txt"),
1434 &Settings {
1435 dereference: true, fail_early: false,
1437 overwrite: false,
1438 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1439 chunk_size: 0,
1440 },
1441 &DO_PRESERVE_SETTINGS, false,
1443 )
1444 .await?;
1445 let summary2 = copy(
1446 &PROGRESS,
1447 &symlink2,
1448 &test_path.join("copied_file2.txt"),
1449 &Settings {
1450 dereference: true,
1451 fail_early: false,
1452 overwrite: false,
1453 overwrite_compare: filecmp::MetadataCmpSettings::default(),
1454 chunk_size: 0,
1455 },
1456 &DO_PRESERVE_SETTINGS,
1457 false,
1458 )
1459 .await?;
1460 assert_eq!(summary1.files_copied, 1);
1461 assert_eq!(summary1.symlinks_created, 0);
1462 assert_eq!(summary2.files_copied, 1);
1463 assert_eq!(summary2.symlinks_created, 0);
1464 let copied1 = test_path.join("copied_file1.txt");
1465 let copied2 = test_path.join("copied_file2.txt");
1466 assert!(copied1.is_file());
1468 assert!(!copied1.is_symlink());
1469 assert!(copied2.is_file());
1470 assert!(!copied2.is_symlink());
1471 let content1 = tokio::fs::read_to_string(&copied1).await?;
1473 let content2 = tokio::fs::read_to_string(&copied2).await?;
1474 assert_eq!(content1, "content1");
1475 assert_eq!(content2, "content2");
1476 let copied1_metadata = tokio::fs::metadata(&copied1).await?;
1478 let copied2_metadata = tokio::fs::metadata(&copied2).await?;
1479 assert_eq!(copied1_metadata.permissions().mode() & 0o777, 0o755);
1480 assert_eq!(copied2_metadata.permissions().mode() & 0o777, 0o640);
1481 Ok(())
1482 }
1483
1484 #[tokio::test]
1485 #[traced_test]
1486 async fn test_cp_dereference_dir() -> Result<(), anyhow::Error> {
1487 let tmp_dir = testutils::setup_test_dir().await?;
1488 tokio::fs::symlink("bar", &tmp_dir.join("foo").join("bar-link")).await?;
1490 tokio::fs::symlink("bar-link", &tmp_dir.join("foo").join("bar-link-link")).await?;
1492 let summary = copy(
1493 &PROGRESS,
1494 &tmp_dir.join("foo"),
1495 &tmp_dir.join("bar"),
1496 &Settings {
1497 dereference: true, fail_early: false,
1499 overwrite: false,
1500 overwrite_compare: filecmp::MetadataCmpSettings {
1501 size: true,
1502 mtime: true,
1503 ..Default::default()
1504 },
1505 chunk_size: 0,
1506 },
1507 &DO_PRESERVE_SETTINGS,
1508 false,
1509 )
1510 .await?;
1511 assert_eq!(summary.files_copied, 13); assert_eq!(summary.symlinks_created, 0); assert_eq!(summary.directories_created, 5);
1514 tokio::process::Command::new("cp")
1516 .args(["-r", "-L"])
1517 .arg(tmp_dir.join("foo"))
1518 .arg(tmp_dir.join("bar-cp"))
1519 .output()
1520 .await?;
1521 testutils::check_dirs_identical(
1522 &tmp_dir.join("bar"),
1523 &tmp_dir.join("bar-cp"),
1524 testutils::FileEqualityCheck::Basic,
1525 )
1526 .await?;
1527 Ok(())
1528 }
1529}