1use anyhow::{anyhow, Context};
2use async_recursion::async_recursion;
3use std::os::linux::fs::MetadataExt as LinuxMetadataExt;
4use tracing::instrument;
5
6use crate::copy;
7use crate::copy::{Settings as CopySettings, Summary as CopySummary};
8use crate::filecmp;
9use crate::preserve;
10use crate::progress;
11use crate::rm;
12
13lazy_static! {
14 static ref RLINK_PRESERVE_SETTINGS: preserve::Settings = preserve::preserve_all();
15}
16
17#[derive(Debug, thiserror::Error)]
28#[error("{source:#}")]
29pub struct Error {
30 #[source]
31 pub source: anyhow::Error,
32 pub summary: Summary,
33}
34
35impl Error {
36 #[must_use]
37 pub fn new(source: anyhow::Error, summary: Summary) -> Self {
38 Error { source, summary }
39 }
40}
41
42#[derive(Debug, Copy, Clone)]
43pub struct Settings {
44 pub copy_settings: CopySettings,
45 pub update_compare: filecmp::MetadataCmpSettings,
46 pub update_exclusive: bool,
47}
48
49#[derive(Copy, Clone, Debug, Default)]
50pub struct Summary {
51 pub hard_links_created: usize,
52 pub hard_links_unchanged: usize,
53 pub copy_summary: CopySummary,
54}
55
56impl std::ops::Add for Summary {
57 type Output = Self;
58 fn add(self, other: Self) -> Self {
59 Self {
60 hard_links_created: self.hard_links_created + other.hard_links_created,
61 hard_links_unchanged: self.hard_links_unchanged + other.hard_links_unchanged,
62 copy_summary: self.copy_summary + other.copy_summary,
63 }
64 }
65}
66
67impl std::fmt::Display for Summary {
68 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
69 write!(
70 f,
71 "{}hard-links created: {}\nhard links unchanged: {}\n",
72 &self.copy_summary, self.hard_links_created, self.hard_links_unchanged
73 )
74 }
75}
76
77fn is_hard_link(md1: &std::fs::Metadata, md2: &std::fs::Metadata) -> bool {
78 copy::is_file_type_same(md1, md2)
79 && md2.st_dev() == md1.st_dev()
80 && md2.st_ino() == md1.st_ino()
81}
82
83#[instrument(skip(prog_track))]
84async fn hard_link_helper(
85 prog_track: &'static progress::Progress,
86 src: &std::path::Path,
87 src_metadata: &std::fs::Metadata,
88 dst: &std::path::Path,
89 settings: &Settings,
90) -> Result<Summary, Error> {
91 let mut link_summary = Summary::default();
92 if let Err(error) = tokio::fs::hard_link(src, dst).await {
93 if settings.copy_settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
94 tracing::debug!("'dst' already exists, check if we need to update");
95 let dst_metadata = tokio::fs::symlink_metadata(dst)
96 .await
97 .with_context(|| format!("cannot read {dst:?} metadata"))
98 .map_err(|err| Error::new(err, Default::default()))?;
99 if is_hard_link(src_metadata, &dst_metadata) {
100 tracing::debug!("no change, leaving file as is");
101 prog_track.hard_links_unchanged.inc();
102 return Ok(Summary {
103 hard_links_unchanged: 1,
104 ..Default::default()
105 });
106 }
107 tracing::info!("'dst' file type changed, removing and hard-linking");
108 let rm_summary = rm::rm(
109 prog_track,
110 dst,
111 &rm::Settings {
112 fail_early: settings.copy_settings.fail_early,
113 },
114 )
115 .await
116 .map_err(|err| {
117 let rm_summary = err.summary;
118 link_summary.copy_summary.rm_summary = rm_summary;
119 Error::new(err.source, link_summary)
120 })?;
121 link_summary.copy_summary.rm_summary = rm_summary;
122 tokio::fs::hard_link(src, dst)
123 .await
124 .with_context(|| format!("failed to hard link {:?} to {:?}", src, dst))
125 .map_err(|err| Error::new(err, link_summary))?;
126 }
127 }
128 prog_track.hard_links_created.inc();
129 link_summary.hard_links_created = 1;
130 Ok(link_summary)
131}
132
133#[instrument(skip(prog_track))]
134#[async_recursion]
135pub async fn link(
136 prog_track: &'static progress::Progress,
137 cwd: &std::path::Path,
138 src: &std::path::Path,
139 dst: &std::path::Path,
140 update: &Option<std::path::PathBuf>,
141 settings: &Settings,
142 mut is_fresh: bool,
143) -> Result<Summary, Error> {
144 let _prog_guard = prog_track.ops.guard();
145 tracing::debug!("reading source metadata");
146 let src_metadata = tokio::fs::symlink_metadata(src)
147 .await
148 .with_context(|| format!("failed reading metadata from {:?}", &src))
149 .map_err(|err| Error::new(err, Default::default()))?;
150 let update_metadata_opt = match update {
151 Some(update) => {
152 tracing::debug!("reading 'update' metadata");
153 let update_metadata_res = tokio::fs::symlink_metadata(update).await;
154 match update_metadata_res {
155 Ok(update_metadata) => Some(update_metadata),
156 Err(error) => {
157 if error.kind() == std::io::ErrorKind::NotFound {
158 if settings.update_exclusive {
159 return Ok(Default::default());
161 }
162 None
163 } else {
164 return Err(Error::new(
165 anyhow!("failed reading metadata from {:?}", &update),
166 Default::default(),
167 ));
168 }
169 }
170 }
171 }
172 None => None,
173 };
174 if let Some(update_metadata) = update_metadata_opt.as_ref() {
175 let update = update.as_ref().unwrap();
176 if !copy::is_file_type_same(&src_metadata, update_metadata) {
177 tracing::debug!(
179 "link: file type of {:?} ({:?}) and {:?} ({:?}) differs - copying from update",
180 src,
181 src_metadata.file_type(),
182 update,
183 update_metadata.file_type()
184 );
185 let copy_summary = copy::copy(
186 prog_track,
187 update,
188 dst,
189 &settings.copy_settings,
190 &RLINK_PRESERVE_SETTINGS,
191 is_fresh,
192 )
193 .await
194 .map_err(|err| {
195 let copy_summary = err.summary;
196 let link_summary = Summary {
197 copy_summary,
198 ..Default::default()
199 };
200 Error::new(err.source, link_summary)
201 })?;
202 return Ok(Summary {
203 copy_summary,
204 ..Default::default()
205 });
206 }
207 if update_metadata.is_file() {
208 if filecmp::metadata_equal(&settings.update_compare, &src_metadata, update_metadata) {
210 tracing::debug!("no change, hard link 'src'");
211 return hard_link_helper(prog_track, src, &src_metadata, dst, settings).await;
212 }
213 tracing::debug!(
214 "link: {:?} metadata has changed, copying from {:?}",
215 src,
216 update
217 );
218 return Ok(Summary {
219 copy_summary: copy::copy_file(
220 prog_track,
221 update,
222 dst,
223 &settings.copy_settings,
224 &RLINK_PRESERVE_SETTINGS,
225 is_fresh,
226 )
227 .await
228 .map_err(|err| {
229 let copy_summary = err.summary;
230 let link_summary = Summary {
231 copy_summary,
232 ..Default::default()
233 };
234 Error::new(err.source, link_summary)
235 })?,
236 ..Default::default()
237 });
238 }
239 if update_metadata.is_symlink() {
240 tracing::debug!("'update' is a symlink so just symlink that");
241 let copy_summary = copy::copy(
243 prog_track,
244 update,
245 dst,
246 &settings.copy_settings,
247 &RLINK_PRESERVE_SETTINGS,
248 is_fresh,
249 )
250 .await
251 .map_err(|err| {
252 let copy_summary = err.summary;
253 let link_summary = Summary {
254 copy_summary,
255 ..Default::default()
256 };
257 Error::new(err.source, link_summary)
258 })?;
259 return Ok(Summary {
260 copy_summary,
261 ..Default::default()
262 });
263 }
264 } else {
265 tracing::debug!("no 'update' specified");
267 if src_metadata.is_file() {
268 return hard_link_helper(prog_track, src, &src_metadata, dst, settings).await;
269 }
270 if src_metadata.is_symlink() {
271 tracing::debug!("'src' is a symlink so just symlink that");
272 let copy_summary = copy::copy(
274 prog_track,
275 src,
276 dst,
277 &settings.copy_settings,
278 &RLINK_PRESERVE_SETTINGS,
279 is_fresh,
280 )
281 .await
282 .map_err(|err| {
283 let copy_summary = err.summary;
284 let link_summary = Summary {
285 copy_summary,
286 ..Default::default()
287 };
288 Error::new(err.source, link_summary)
289 })?;
290 return Ok(Summary {
291 copy_summary,
292 ..Default::default()
293 });
294 }
295 }
296 if !src_metadata.is_dir() {
297 return Err(Error::new(
298 anyhow!(
299 "copy: {:?} -> {:?} failed, unsupported src file type: {:?}",
300 src,
301 dst,
302 src_metadata.file_type()
303 ),
304 Default::default(),
305 ));
306 }
307 assert!(update_metadata_opt.is_none() || update_metadata_opt.as_ref().unwrap().is_dir());
308 tracing::debug!("process contents of 'src' directory");
309 let mut src_entries = tokio::fs::read_dir(src)
310 .await
311 .with_context(|| format!("cannot open directory {src:?} for reading"))
312 .map_err(|err| Error::new(err, Default::default()))?;
313 let copy_summary = {
314 if let Err(error) = tokio::fs::create_dir(dst).await {
315 assert!(!is_fresh, "unexpected error creating directory: {:?}", &dst);
316 if settings.copy_settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists
317 {
318 let dst_metadata = tokio::fs::metadata(dst)
323 .await
324 .with_context(|| format!("failed reading metadata from {:?}", &dst))
325 .map_err(|err| Error::new(err, Default::default()))?;
326 if dst_metadata.is_dir() {
327 tracing::debug!("'dst' is a directory, leaving it as is");
328 CopySummary {
329 directories_unchanged: 1,
330 ..Default::default()
331 }
332 } else {
333 tracing::info!("'dst' is not a directory, removing and creating a new one");
334 let mut copy_summary = CopySummary::default();
335 let rm_summary = rm::rm(
336 prog_track,
337 dst,
338 &rm::Settings {
339 fail_early: settings.copy_settings.fail_early,
340 },
341 )
342 .await
343 .map_err(|err| {
344 let rm_summary = err.summary;
345 copy_summary.rm_summary = rm_summary;
346 Error::new(
347 err.source,
348 Summary {
349 copy_summary,
350 ..Default::default()
351 },
352 )
353 })?;
354 tokio::fs::create_dir(dst)
355 .await
356 .with_context(|| format!("cannot create directory {dst:?}"))
357 .map_err(|err| {
358 copy_summary.rm_summary = rm_summary;
359 Error::new(
360 err,
361 Summary {
362 copy_summary,
363 ..Default::default()
364 },
365 )
366 })?;
367 is_fresh = true;
369 CopySummary {
370 rm_summary,
371 directories_created: 1,
372 ..Default::default()
373 }
374 }
375 } else {
376 return Err(error)
377 .with_context(|| format!("cannot create directory {dst:?}"))
378 .map_err(|err| Error::new(err, Default::default()))?;
379 }
380 } else {
381 is_fresh = true;
383 CopySummary {
384 directories_created: 1,
385 ..Default::default()
386 }
387 }
388 };
389 let mut link_summary = Summary {
390 copy_summary,
391 ..Default::default()
392 };
393 let mut join_set = tokio::task::JoinSet::new();
394 let mut all_children_succeeded = true;
395 let mut processed_files = std::collections::HashSet::new();
397 while let Some(src_entry) = src_entries
399 .next_entry()
400 .await
401 .with_context(|| format!("failed traversing directory {:?}", &src))
402 .map_err(|err| Error::new(err, link_summary))?
403 {
404 throttle::get_ops_token().await;
408 let cwd_path = cwd.to_owned();
409 let entry_path = src_entry.path();
410 let entry_name = entry_path.file_name().unwrap();
411 processed_files.insert(entry_name.to_owned());
412 let dst_path = dst.join(entry_name);
413 let update_path = update.as_ref().map(|s| s.join(entry_name));
414 let settings = *settings;
415 let do_link = || async move {
416 link(
417 prog_track,
418 &cwd_path,
419 &entry_path,
420 &dst_path,
421 &update_path,
422 &settings,
423 is_fresh,
424 )
425 .await
426 };
427 join_set.spawn(do_link());
428 }
429 drop(src_entries);
432 if update_metadata_opt.is_some() {
434 let update = update.as_ref().unwrap();
435 tracing::debug!("process contents of 'update' directory");
436 let mut update_entries = tokio::fs::read_dir(update)
437 .await
438 .with_context(|| format!("cannot open directory {:?} for reading", &update))
439 .map_err(|err| Error::new(err, link_summary))?;
440 while let Some(update_entry) = update_entries
442 .next_entry()
443 .await
444 .with_context(|| format!("failed traversing directory {:?}", &update))
445 .map_err(|err| Error::new(err, link_summary))?
446 {
447 let entry_path = update_entry.path();
448 let entry_name = entry_path.file_name().unwrap();
449 if processed_files.contains(entry_name) {
450 continue;
452 }
453 tracing::debug!("found a new entry in the 'update' directory");
454 let dst_path = dst.join(entry_name);
455 let update_path = update.join(entry_name);
456 let settings = *settings;
457 let do_copy = || async move {
458 let copy_summary = copy::copy(
459 prog_track,
460 &update_path,
461 &dst_path,
462 &settings.copy_settings,
463 &RLINK_PRESERVE_SETTINGS,
464 is_fresh,
465 )
466 .await
467 .map_err(|err| {
468 link_summary.copy_summary = link_summary.copy_summary + err.summary;
469 Error::new(err.source, link_summary)
470 })?;
471 Ok(Summary {
472 copy_summary,
473 ..Default::default()
474 })
475 };
476 join_set.spawn(do_copy());
477 }
478 drop(update_entries);
481 }
482 while let Some(res) = join_set.join_next().await {
483 match res {
484 Ok(result) => match result {
485 Ok(summary) => link_summary = link_summary + summary,
486 Err(error) => {
487 tracing::error!(
488 "link: {:?} {:?} -> {:?} failed with: {:#}",
489 src,
490 update,
491 dst,
492 &error
493 );
494 if settings.copy_settings.fail_early {
495 return Err(error);
496 }
497 all_children_succeeded = false;
498 }
499 },
500 Err(error) => {
501 if settings.copy_settings.fail_early {
502 return Err(Error::new(error.into(), link_summary));
503 }
504 }
505 }
506 }
507 tracing::debug!("set 'dst' directory metadata");
511 let preserve_metadata = if let Some(update_metadata) = update_metadata_opt.as_ref() {
512 update_metadata
513 } else {
514 &src_metadata
515 };
516 let metadata_result =
517 preserve::set_dir_metadata(&RLINK_PRESERVE_SETTINGS, preserve_metadata, dst).await;
518 if !all_children_succeeded {
519 if let Err(metadata_err) = metadata_result {
521 tracing::error!(
522 "link: {:?} {:?} -> {:?} failed to set directory metadata: {:#}",
523 src,
524 update,
525 dst,
526 &metadata_err
527 );
528 }
529 return Err(Error::new(
530 anyhow!("link: {:?} {:?} -> {:?} failed!", src, update, dst),
531 link_summary,
532 ))?;
533 }
534 metadata_result.map_err(|err| Error::new(err, link_summary))?;
536 Ok(link_summary)
537}
538
539#[cfg(test)]
540mod link_tests {
541 use crate::testutils;
542 use std::os::unix::fs::PermissionsExt;
543 use tracing_test::traced_test;
544
545 use super::*;
546
547 lazy_static! {
548 static ref PROGRESS: progress::Progress = progress::Progress::new();
549 }
550
551 fn common_settings(dereference: bool, overwrite: bool) -> Settings {
552 Settings {
553 copy_settings: CopySettings {
554 dereference,
555 fail_early: false,
556 overwrite,
557 overwrite_compare: filecmp::MetadataCmpSettings {
558 size: true,
559 mtime: true,
560 ..Default::default()
561 },
562 chunk_size: 0,
563 remote_copy_buffer_size: 0,
564 },
565 update_compare: filecmp::MetadataCmpSettings {
566 size: true,
567 mtime: true,
568 ..Default::default()
569 },
570 update_exclusive: false,
571 }
572 }
573
574 #[tokio::test]
575 #[traced_test]
576 async fn test_basic_link() -> Result<(), anyhow::Error> {
577 let tmp_dir = testutils::setup_test_dir().await?;
578 let test_path = tmp_dir.as_path();
579 let summary = link(
580 &PROGRESS,
581 test_path,
582 &test_path.join("foo"),
583 &test_path.join("bar"),
584 &None,
585 &common_settings(false, false),
586 false,
587 )
588 .await?;
589 assert_eq!(summary.hard_links_created, 5);
590 assert_eq!(summary.copy_summary.files_copied, 0);
591 assert_eq!(summary.copy_summary.symlinks_created, 2);
592 assert_eq!(summary.copy_summary.directories_created, 3);
593 testutils::check_dirs_identical(
594 &test_path.join("foo"),
595 &test_path.join("bar"),
596 testutils::FileEqualityCheck::Timestamp,
597 )
598 .await?;
599 Ok(())
600 }
601
602 #[tokio::test]
603 #[traced_test]
604 async fn test_basic_link_update() -> Result<(), anyhow::Error> {
605 let tmp_dir = testutils::setup_test_dir().await?;
606 let test_path = tmp_dir.as_path();
607 let summary = link(
608 &PROGRESS,
609 test_path,
610 &test_path.join("foo"),
611 &test_path.join("bar"),
612 &Some(test_path.join("foo")),
613 &common_settings(false, false),
614 false,
615 )
616 .await?;
617 assert_eq!(summary.hard_links_created, 5);
618 assert_eq!(summary.copy_summary.files_copied, 0);
619 assert_eq!(summary.copy_summary.symlinks_created, 2);
620 assert_eq!(summary.copy_summary.directories_created, 3);
621 testutils::check_dirs_identical(
622 &test_path.join("foo"),
623 &test_path.join("bar"),
624 testutils::FileEqualityCheck::Timestamp,
625 )
626 .await?;
627 Ok(())
628 }
629
630 #[tokio::test]
631 #[traced_test]
632 async fn test_basic_link_empty_src() -> Result<(), anyhow::Error> {
633 let tmp_dir = testutils::setup_test_dir().await?;
634 tokio::fs::create_dir(tmp_dir.join("baz")).await?;
635 let test_path = tmp_dir.as_path();
636 let summary = link(
637 &PROGRESS,
638 test_path,
639 &test_path.join("baz"), &test_path.join("bar"),
641 &Some(test_path.join("foo")),
642 &common_settings(false, false),
643 false,
644 )
645 .await?;
646 assert_eq!(summary.hard_links_created, 0);
647 assert_eq!(summary.copy_summary.files_copied, 5);
648 assert_eq!(summary.copy_summary.symlinks_created, 2);
649 assert_eq!(summary.copy_summary.directories_created, 3);
650 testutils::check_dirs_identical(
651 &test_path.join("foo"),
652 &test_path.join("bar"),
653 testutils::FileEqualityCheck::Timestamp,
654 )
655 .await?;
656 Ok(())
657 }
658
659 #[tokio::test]
660 #[traced_test]
661 async fn test_link_destination_permission_error_includes_root_cause(
662 ) -> Result<(), anyhow::Error> {
663 let tmp_dir = testutils::setup_test_dir().await?;
664 let test_path = tmp_dir.as_path();
665 let readonly_parent = test_path.join("readonly_dest");
666 tokio::fs::create_dir(&readonly_parent).await?;
667 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o555))
668 .await?;
669
670 let mut settings = common_settings(false, false);
671 settings.copy_settings.fail_early = true;
672
673 let result = link(
674 &PROGRESS,
675 test_path,
676 &test_path.join("foo"),
677 &readonly_parent.join("bar"),
678 &None,
679 &settings,
680 false,
681 )
682 .await;
683
684 tokio::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o755))
686 .await?;
687
688 assert!(result.is_err(), "link into read-only parent should fail");
689 let err = result.unwrap_err();
690 let err_msg = format!("{:#}", err.source);
691 assert!(
692 err_msg.to_lowercase().contains("permission denied") || err_msg.contains("EACCES"),
693 "Error message must include permission denied text. Got: {}",
694 err_msg
695 );
696 Ok(())
697 }
698
699 pub async fn setup_update_dir(tmp_dir: &std::path::Path) -> Result<(), anyhow::Error> {
700 let foo_path = tmp_dir.join("update");
706 tokio::fs::create_dir(&foo_path).await.unwrap();
707 tokio::fs::write(foo_path.join("0.txt"), "0-new")
708 .await
709 .unwrap();
710 let bar_path = foo_path.join("bar");
711 tokio::fs::create_dir(&bar_path).await.unwrap();
712 tokio::fs::write(bar_path.join("1.txt"), "1-new")
713 .await
714 .unwrap();
715 tokio::fs::symlink("../1.txt", bar_path.join("2.txt"))
716 .await
717 .unwrap();
718 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
719 Ok(())
720 }
721
722 #[tokio::test]
723 #[traced_test]
724 async fn test_link_update() -> Result<(), anyhow::Error> {
725 let tmp_dir = testutils::setup_test_dir().await?;
726 setup_update_dir(&tmp_dir).await?;
727 let test_path = tmp_dir.as_path();
728 let summary = link(
729 &PROGRESS,
730 test_path,
731 &test_path.join("foo"),
732 &test_path.join("bar"),
733 &Some(test_path.join("update")),
734 &common_settings(false, false),
735 false,
736 )
737 .await?;
738 assert_eq!(summary.hard_links_created, 2);
739 assert_eq!(summary.copy_summary.files_copied, 2);
740 assert_eq!(summary.copy_summary.symlinks_created, 3);
741 assert_eq!(summary.copy_summary.directories_created, 3);
742 testutils::check_dirs_identical(
744 &test_path.join("foo").join("baz"),
745 &test_path.join("bar").join("baz"),
746 testutils::FileEqualityCheck::HardLink,
747 )
748 .await?;
749 testutils::check_dirs_identical(
751 &test_path.join("update"),
752 &test_path.join("bar"),
753 testutils::FileEqualityCheck::Timestamp,
754 )
755 .await?;
756 Ok(())
757 }
758
759 #[tokio::test]
760 #[traced_test]
761 async fn test_link_update_exclusive() -> Result<(), anyhow::Error> {
762 let tmp_dir = testutils::setup_test_dir().await?;
763 setup_update_dir(&tmp_dir).await?;
764 let test_path = tmp_dir.as_path();
765 let mut settings = common_settings(false, false);
766 settings.update_exclusive = true;
767 let summary = link(
768 &PROGRESS,
769 test_path,
770 &test_path.join("foo"),
771 &test_path.join("bar"),
772 &Some(test_path.join("update")),
773 &settings,
774 false,
775 )
776 .await?;
777 assert_eq!(summary.hard_links_created, 0);
783 assert_eq!(summary.copy_summary.files_copied, 2);
784 assert_eq!(summary.copy_summary.symlinks_created, 1);
785 assert_eq!(summary.copy_summary.directories_created, 2);
786 testutils::check_dirs_identical(
788 &test_path.join("update"),
789 &test_path.join("bar"),
790 testutils::FileEqualityCheck::Timestamp,
791 )
792 .await?;
793 Ok(())
794 }
795
796 async fn setup_test_dir_and_link() -> Result<std::path::PathBuf, anyhow::Error> {
797 let tmp_dir = testutils::setup_test_dir().await?;
798 let test_path = tmp_dir.as_path();
799 let summary = link(
800 &PROGRESS,
801 test_path,
802 &test_path.join("foo"),
803 &test_path.join("bar"),
804 &None,
805 &common_settings(false, false),
806 false,
807 )
808 .await?;
809 assert_eq!(summary.hard_links_created, 5);
810 assert_eq!(summary.copy_summary.symlinks_created, 2);
811 assert_eq!(summary.copy_summary.directories_created, 3);
812 Ok(tmp_dir)
813 }
814
815 #[tokio::test]
816 #[traced_test]
817 async fn test_link_overwrite_basic() -> Result<(), anyhow::Error> {
818 let tmp_dir = setup_test_dir_and_link().await?;
819 let output_path = &tmp_dir.join("bar");
820 {
821 let summary = rm::rm(
832 &PROGRESS,
833 &output_path.join("bar"),
834 &rm::Settings { fail_early: false },
835 )
836 .await?
837 + rm::rm(
838 &PROGRESS,
839 &output_path.join("baz").join("5.txt"),
840 &rm::Settings { fail_early: false },
841 )
842 .await?;
843 assert_eq!(summary.files_removed, 3);
844 assert_eq!(summary.symlinks_removed, 1);
845 assert_eq!(summary.directories_removed, 1);
846 }
847 let summary = link(
848 &PROGRESS,
849 &tmp_dir,
850 &tmp_dir.join("foo"),
851 output_path,
852 &None,
853 &common_settings(false, true), false,
855 )
856 .await?;
857 assert_eq!(summary.hard_links_created, 3);
858 assert_eq!(summary.copy_summary.symlinks_created, 1);
859 assert_eq!(summary.copy_summary.directories_created, 1);
860 testutils::check_dirs_identical(
861 &tmp_dir.join("foo"),
862 output_path,
863 testutils::FileEqualityCheck::Timestamp,
864 )
865 .await?;
866 Ok(())
867 }
868
869 #[tokio::test]
870 #[traced_test]
871 async fn test_link_update_overwrite_basic() -> Result<(), anyhow::Error> {
872 let tmp_dir = setup_test_dir_and_link().await?;
873 let output_path = &tmp_dir.join("bar");
874 {
875 let summary = rm::rm(
886 &PROGRESS,
887 &output_path.join("bar"),
888 &rm::Settings { fail_early: false },
889 )
890 .await?
891 + rm::rm(
892 &PROGRESS,
893 &output_path.join("baz").join("5.txt"),
894 &rm::Settings { fail_early: false },
895 )
896 .await?;
897 assert_eq!(summary.files_removed, 3);
898 assert_eq!(summary.symlinks_removed, 1);
899 assert_eq!(summary.directories_removed, 1);
900 }
901 setup_update_dir(&tmp_dir).await?;
902 let summary = link(
908 &PROGRESS,
909 &tmp_dir,
910 &tmp_dir.join("foo"),
911 output_path,
912 &Some(tmp_dir.join("update")),
913 &common_settings(false, true), false,
915 )
916 .await?;
917 assert_eq!(summary.hard_links_created, 1); assert_eq!(summary.copy_summary.files_copied, 2); assert_eq!(summary.copy_summary.symlinks_created, 2); assert_eq!(summary.copy_summary.directories_created, 1);
921 testutils::check_dirs_identical(
923 &tmp_dir.join("foo").join("baz"),
924 &tmp_dir.join("bar").join("baz"),
925 testutils::FileEqualityCheck::HardLink,
926 )
927 .await?;
928 testutils::check_dirs_identical(
930 &tmp_dir.join("update"),
931 &tmp_dir.join("bar"),
932 testutils::FileEqualityCheck::Timestamp,
933 )
934 .await?;
935 Ok(())
936 }
937
938 #[tokio::test]
939 #[traced_test]
940 async fn test_link_overwrite_hardlink_file() -> Result<(), anyhow::Error> {
941 let tmp_dir = setup_test_dir_and_link().await?;
942 let output_path = &tmp_dir.join("bar");
943 {
944 let bar_path = output_path.join("bar");
953 let summary = rm::rm(
954 &PROGRESS,
955 &bar_path.join("1.txt"),
956 &rm::Settings { fail_early: false },
957 )
958 .await?
959 + rm::rm(
960 &PROGRESS,
961 &bar_path.join("2.txt"),
962 &rm::Settings { fail_early: false },
963 )
964 .await?
965 + rm::rm(
966 &PROGRESS,
967 &bar_path.join("3.txt"),
968 &rm::Settings { fail_early: false },
969 )
970 .await?
971 + rm::rm(
972 &PROGRESS,
973 &output_path.join("baz"),
974 &rm::Settings { fail_early: false },
975 )
976 .await?;
977 assert_eq!(summary.files_removed, 4);
978 assert_eq!(summary.symlinks_removed, 2);
979 assert_eq!(summary.directories_removed, 1);
980 tokio::fs::write(bar_path.join("1.txt"), "1-new")
982 .await
983 .unwrap();
984 tokio::fs::symlink("../0.txt", bar_path.join("2.txt"))
985 .await
986 .unwrap();
987 tokio::fs::create_dir(&bar_path.join("3.txt"))
988 .await
989 .unwrap();
990 tokio::fs::write(&output_path.join("baz"), "baz")
991 .await
992 .unwrap();
993 }
994 let summary = link(
995 &PROGRESS,
996 &tmp_dir,
997 &tmp_dir.join("foo"),
998 output_path,
999 &None,
1000 &common_settings(false, true), false,
1002 )
1003 .await?;
1004 assert_eq!(summary.hard_links_created, 4);
1005 assert_eq!(summary.copy_summary.files_copied, 0);
1006 assert_eq!(summary.copy_summary.symlinks_created, 2);
1007 assert_eq!(summary.copy_summary.directories_created, 1);
1008 testutils::check_dirs_identical(
1009 &tmp_dir.join("foo"),
1010 &tmp_dir.join("bar"),
1011 testutils::FileEqualityCheck::HardLink,
1012 )
1013 .await?;
1014 Ok(())
1015 }
1016
1017 #[tokio::test]
1018 #[traced_test]
1019 async fn test_link_overwrite_error() -> Result<(), anyhow::Error> {
1020 let tmp_dir = setup_test_dir_and_link().await?;
1021 let output_path = &tmp_dir.join("bar");
1022 {
1023 let bar_path = output_path.join("bar");
1032 let summary = rm::rm(
1033 &PROGRESS,
1034 &bar_path.join("1.txt"),
1035 &rm::Settings { fail_early: false },
1036 )
1037 .await?
1038 + rm::rm(
1039 &PROGRESS,
1040 &bar_path.join("2.txt"),
1041 &rm::Settings { fail_early: false },
1042 )
1043 .await?
1044 + rm::rm(
1045 &PROGRESS,
1046 &bar_path.join("3.txt"),
1047 &rm::Settings { fail_early: false },
1048 )
1049 .await?
1050 + rm::rm(
1051 &PROGRESS,
1052 &output_path.join("baz"),
1053 &rm::Settings { fail_early: false },
1054 )
1055 .await?;
1056 assert_eq!(summary.files_removed, 4);
1057 assert_eq!(summary.symlinks_removed, 2);
1058 assert_eq!(summary.directories_removed, 1);
1059 tokio::fs::write(bar_path.join("1.txt"), "1-new")
1061 .await
1062 .unwrap();
1063 tokio::fs::symlink("../0.txt", bar_path.join("2.txt"))
1064 .await
1065 .unwrap();
1066 tokio::fs::create_dir(&bar_path.join("3.txt"))
1067 .await
1068 .unwrap();
1069 tokio::fs::write(&output_path.join("baz"), "baz")
1070 .await
1071 .unwrap();
1072 }
1073 let source_path = &tmp_dir.join("foo");
1074 tokio::fs::set_permissions(
1076 &source_path.join("baz"),
1077 std::fs::Permissions::from_mode(0o000),
1078 )
1079 .await?;
1080 match link(
1084 &PROGRESS,
1085 &tmp_dir,
1086 &tmp_dir.join("foo"),
1087 output_path,
1088 &None,
1089 &common_settings(false, true), false,
1091 )
1092 .await
1093 {
1094 Ok(_) => panic!("Expected the link to error!"),
1095 Err(error) => {
1096 tracing::info!("{}", &error);
1097 assert_eq!(error.summary.hard_links_created, 3);
1098 assert_eq!(error.summary.copy_summary.files_copied, 0);
1099 assert_eq!(error.summary.copy_summary.symlinks_created, 0);
1100 assert_eq!(error.summary.copy_summary.directories_created, 0);
1101 assert_eq!(error.summary.copy_summary.rm_summary.files_removed, 1);
1102 assert_eq!(error.summary.copy_summary.rm_summary.directories_removed, 1);
1103 assert_eq!(error.summary.copy_summary.rm_summary.symlinks_removed, 1);
1104 }
1105 }
1106 Ok(())
1107 }
1108
1109 #[tokio::test]
1113 #[traced_test]
1114 async fn test_link_directory_metadata_applied_on_child_error() -> Result<(), anyhow::Error> {
1115 let tmp_dir = testutils::create_temp_dir().await?;
1116 let test_path = tmp_dir.as_path();
1117 let src_dir = test_path.join("src");
1119 tokio::fs::create_dir(&src_dir).await?;
1120 tokio::fs::set_permissions(&src_dir, std::fs::Permissions::from_mode(0o750)).await?;
1121 tokio::fs::write(src_dir.join("readable.txt"), "content").await?;
1123 let unreadable_subdir = src_dir.join("unreadable_subdir");
1126 tokio::fs::create_dir(&unreadable_subdir).await?;
1127 tokio::fs::write(unreadable_subdir.join("hidden.txt"), "secret").await?;
1128 tokio::fs::set_permissions(&unreadable_subdir, std::fs::Permissions::from_mode(0o000))
1129 .await?;
1130 let dst_dir = test_path.join("dst");
1131 let result = link(
1133 &PROGRESS,
1134 test_path,
1135 &src_dir,
1136 &dst_dir,
1137 &None,
1138 &common_settings(false, false),
1139 false,
1140 )
1141 .await;
1142 tokio::fs::set_permissions(&unreadable_subdir, std::fs::Permissions::from_mode(0o755))
1144 .await?;
1145 assert!(
1147 result.is_err(),
1148 "link should fail due to unreadable subdirectory"
1149 );
1150 let error = result.unwrap_err();
1151 assert_eq!(error.summary.hard_links_created, 1);
1153 let dst_metadata = tokio::fs::metadata(&dst_dir).await?;
1155 assert!(dst_metadata.is_dir());
1156 let actual_mode = dst_metadata.permissions().mode() & 0o7777;
1157 assert_eq!(
1158 actual_mode, 0o750,
1159 "directory should have preserved source permissions (0o750), got {:o}",
1160 actual_mode
1161 );
1162 Ok(())
1163 }
1164}