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)]
18#[error("{source}")]
19pub struct Error {
20 #[source]
21 pub source: anyhow::Error,
22 pub summary: Summary,
23}
24
25impl Error {
26 #[must_use]
27 pub fn new(source: anyhow::Error, summary: Summary) -> Self {
28 Error { source, summary }
29 }
30}
31
32#[derive(Debug, Copy, Clone)]
33pub struct Settings {
34 pub copy_settings: CopySettings,
35 pub update_compare: filecmp::MetadataCmpSettings,
36 pub update_exclusive: bool,
37}
38
39#[derive(Copy, Clone, Debug, Default)]
40pub struct Summary {
41 pub hard_links_created: usize,
42 pub hard_links_unchanged: usize,
43 pub copy_summary: CopySummary,
44}
45
46impl std::ops::Add for Summary {
47 type Output = Self;
48 fn add(self, other: Self) -> Self {
49 Self {
50 hard_links_created: self.hard_links_created + other.hard_links_created,
51 hard_links_unchanged: self.hard_links_unchanged + other.hard_links_unchanged,
52 copy_summary: self.copy_summary + other.copy_summary,
53 }
54 }
55}
56
57impl std::fmt::Display for Summary {
58 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59 write!(
60 f,
61 "{}hard-links created: {}\nhard links unchanged: {}\n",
62 &self.copy_summary, self.hard_links_created, self.hard_links_unchanged
63 )
64 }
65}
66
67fn is_hard_link(md1: &std::fs::Metadata, md2: &std::fs::Metadata) -> bool {
68 copy::is_file_type_same(md1, md2)
69 && md2.st_dev() == md1.st_dev()
70 && md2.st_ino() == md1.st_ino()
71}
72
73#[instrument(skip(prog_track))]
74async fn hard_link_helper(
75 prog_track: &'static progress::Progress,
76 src: &std::path::Path,
77 src_metadata: &std::fs::Metadata,
78 dst: &std::path::Path,
79 settings: &Settings,
80) -> Result<Summary, Error> {
81 let mut link_summary = Summary::default();
82 if let Err(error) = tokio::fs::hard_link(src, dst).await {
83 if settings.copy_settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists {
84 tracing::debug!("'dst' already exists, check if we need to update");
85 let dst_metadata = tokio::fs::symlink_metadata(dst)
86 .await
87 .with_context(|| format!("cannot read {dst:?} metadata"))
88 .map_err(|err| Error::new(err, Default::default()))?;
89 if is_hard_link(src_metadata, &dst_metadata) {
90 tracing::debug!("no change, leaving file as is");
91 prog_track.hard_links_unchanged.inc();
92 return Ok(Summary {
93 hard_links_unchanged: 1,
94 ..Default::default()
95 });
96 }
97 tracing::info!("'dst' file type changed, removing and hard-linking");
98 let rm_summary = rm::rm(
99 prog_track,
100 dst,
101 &rm::Settings {
102 fail_early: settings.copy_settings.fail_early,
103 },
104 )
105 .await
106 .map_err(|err| {
107 let rm_summary = err.summary;
108 link_summary.copy_summary.rm_summary = rm_summary;
109 Error::new(anyhow::Error::msg(err), link_summary)
110 })?;
111 link_summary.copy_summary.rm_summary = rm_summary;
112 tokio::fs::hard_link(src, dst)
113 .await
114 .map_err(|err| Error::new(anyhow::Error::msg(err), link_summary))?;
115 }
116 }
117 prog_track.hard_links_created.inc();
118 link_summary.hard_links_created = 1;
119 Ok(link_summary)
120}
121
122#[instrument(skip(prog_track))]
123#[async_recursion]
124pub async fn link(
125 prog_track: &'static progress::Progress,
126 cwd: &std::path::Path,
127 src: &std::path::Path,
128 dst: &std::path::Path,
129 update: &Option<std::path::PathBuf>,
130 settings: &Settings,
131 mut is_fresh: bool,
132) -> Result<Summary, Error> {
133 let _prog_guard = prog_track.ops.guard();
134 tracing::debug!("reading source metadata");
135 let src_metadata = tokio::fs::symlink_metadata(src)
136 .await
137 .with_context(|| format!("failed reading metadata from {:?}", &src))
138 .map_err(|err| Error::new(err, Default::default()))?;
139 let update_metadata_opt = match update {
140 Some(update) => {
141 tracing::debug!("reading 'update' metadata");
142 let update_metadata_res = tokio::fs::symlink_metadata(update).await;
143 match update_metadata_res {
144 Ok(update_metadata) => Some(update_metadata),
145 Err(error) => {
146 if error.kind() == std::io::ErrorKind::NotFound {
147 if settings.update_exclusive {
148 return Ok(Default::default());
150 }
151 None
152 } else {
153 return Err(Error::new(
154 anyhow!("failed reading metadata from {:?}", &update),
155 Default::default(),
156 ));
157 }
158 }
159 }
160 }
161 None => None,
162 };
163 if let Some(update_metadata) = update_metadata_opt.as_ref() {
164 let update = update.as_ref().unwrap();
165 if !copy::is_file_type_same(&src_metadata, update_metadata) {
166 tracing::debug!(
168 "link: file type of {:?} ({:?}) and {:?} ({:?}) differs - copying from update",
169 src,
170 src_metadata.file_type(),
171 update,
172 update_metadata.file_type()
173 );
174 let copy_summary = copy::copy(
175 prog_track,
176 update,
177 dst,
178 &settings.copy_settings,
179 &RLINK_PRESERVE_SETTINGS,
180 is_fresh,
181 )
182 .await
183 .map_err(|err| {
184 let copy_summary = err.summary;
185 let link_summary = Summary {
186 copy_summary,
187 ..Default::default()
188 };
189 Error::new(err.source, link_summary)
190 })?;
191 return Ok(Summary {
192 copy_summary,
193 ..Default::default()
194 });
195 }
196 if update_metadata.is_file() {
197 if filecmp::metadata_equal(&settings.update_compare, &src_metadata, update_metadata) {
199 tracing::debug!("no change, hard link 'src'");
200 return hard_link_helper(prog_track, src, &src_metadata, dst, settings).await;
201 }
202 tracing::debug!(
203 "link: {:?} metadata has changed, copying from {:?}",
204 src,
205 update
206 );
207 return Ok(Summary {
208 copy_summary: copy::copy_file(
209 prog_track,
210 update,
211 dst,
212 &settings.copy_settings,
213 &RLINK_PRESERVE_SETTINGS,
214 is_fresh,
215 )
216 .await
217 .map_err(|err| {
218 let copy_summary = err.summary;
219 let link_summary = Summary {
220 copy_summary,
221 ..Default::default()
222 };
223 Error::new(err.source, link_summary)
224 })?,
225 ..Default::default()
226 });
227 }
228 if update_metadata.is_symlink() {
229 tracing::debug!("'update' is a symlink so just symlink that");
230 let copy_summary = copy::copy(
232 prog_track,
233 update,
234 dst,
235 &settings.copy_settings,
236 &RLINK_PRESERVE_SETTINGS,
237 is_fresh,
238 )
239 .await
240 .map_err(|err| {
241 let copy_summary = err.summary;
242 let link_summary = Summary {
243 copy_summary,
244 ..Default::default()
245 };
246 Error::new(err.source, link_summary)
247 })?;
248 return Ok(Summary {
249 copy_summary,
250 ..Default::default()
251 });
252 }
253 } else {
254 tracing::debug!("no 'update' specified");
256 if src_metadata.is_file() {
257 return hard_link_helper(prog_track, src, &src_metadata, dst, settings).await;
258 }
259 if src_metadata.is_symlink() {
260 tracing::debug!("'src' is a symlink so just symlink that");
261 let copy_summary = copy::copy(
263 prog_track,
264 src,
265 dst,
266 &settings.copy_settings,
267 &RLINK_PRESERVE_SETTINGS,
268 is_fresh,
269 )
270 .await
271 .map_err(|err| {
272 let copy_summary = err.summary;
273 let link_summary = Summary {
274 copy_summary,
275 ..Default::default()
276 };
277 Error::new(err.source, link_summary)
278 })?;
279 return Ok(Summary {
280 copy_summary,
281 ..Default::default()
282 });
283 }
284 }
285 if !src_metadata.is_dir() {
286 return Err(Error::new(
287 anyhow!(
288 "copy: {:?} -> {:?} failed, unsupported src file type: {:?}",
289 src,
290 dst,
291 src_metadata.file_type()
292 ),
293 Default::default(),
294 ));
295 }
296 assert!(update_metadata_opt.is_none() || update_metadata_opt.as_ref().unwrap().is_dir());
297 tracing::debug!("process contents of 'src' directory");
298 let mut src_entries = tokio::fs::read_dir(src)
299 .await
300 .with_context(|| format!("cannot open directory {src:?} for reading"))
301 .map_err(|err| Error::new(err, Default::default()))?;
302 let copy_summary = {
303 if let Err(error) = tokio::fs::create_dir(dst).await {
304 assert!(!is_fresh, "unexpected error creating directory: {:?}", &dst);
305 if settings.copy_settings.overwrite && error.kind() == std::io::ErrorKind::AlreadyExists
306 {
307 let dst_metadata = tokio::fs::metadata(dst)
312 .await
313 .with_context(|| format!("failed reading metadata from {:?}", &dst))
314 .map_err(|err| Error::new(err, Default::default()))?;
315 if dst_metadata.is_dir() {
316 tracing::debug!("'dst' is a directory, leaving it as is");
317 CopySummary {
318 directories_unchanged: 1,
319 ..Default::default()
320 }
321 } else {
322 tracing::info!("'dst' is not a directory, removing and creating a new one");
323 let mut copy_summary = CopySummary::default();
324 let rm_summary = rm::rm(
325 prog_track,
326 dst,
327 &rm::Settings {
328 fail_early: settings.copy_settings.fail_early,
329 },
330 )
331 .await
332 .map_err(|err| {
333 let rm_summary = err.summary;
334 copy_summary.rm_summary = rm_summary;
335 Error::new(
336 anyhow::Error::msg(err),
337 Summary {
338 copy_summary,
339 ..Default::default()
340 },
341 )
342 })?;
343 tokio::fs::create_dir(dst)
344 .await
345 .with_context(|| format!("cannot create directory {dst:?}"))
346 .map_err(|err| {
347 copy_summary.rm_summary = rm_summary;
348 Error::new(
349 anyhow::Error::msg(err),
350 Summary {
351 copy_summary,
352 ..Default::default()
353 },
354 )
355 })?;
356 is_fresh = true;
358 CopySummary {
359 rm_summary,
360 directories_created: 1,
361 ..Default::default()
362 }
363 }
364 } else {
365 return Err(error)
366 .with_context(|| format!("cannot create directory {dst:?}"))
367 .map_err(|err| Error::new(err, Default::default()))?;
368 }
369 } else {
370 is_fresh = true;
372 CopySummary {
373 directories_created: 1,
374 ..Default::default()
375 }
376 }
377 };
378 let mut link_summary = Summary {
379 copy_summary,
380 ..Default::default()
381 };
382 let mut join_set = tokio::task::JoinSet::new();
383 let mut success = true;
384 let mut processed_files = std::collections::HashSet::new();
386 while let Some(src_entry) = src_entries
388 .next_entry()
389 .await
390 .with_context(|| format!("failed traversing directory {:?}", &src))
391 .map_err(|err| Error::new(err, link_summary))?
392 {
393 throttle::get_ops_token().await;
397 let cwd_path = cwd.to_owned();
398 let entry_path = src_entry.path();
399 let entry_name = entry_path.file_name().unwrap();
400 processed_files.insert(entry_name.to_owned());
401 let dst_path = dst.join(entry_name);
402 let update_path = update.as_ref().map(|s| s.join(entry_name));
403 let settings = *settings;
404 let do_link = || async move {
405 link(
406 prog_track,
407 &cwd_path,
408 &entry_path,
409 &dst_path,
410 &update_path,
411 &settings,
412 is_fresh,
413 )
414 .await
415 };
416 join_set.spawn(do_link());
417 }
418 drop(src_entries);
421 if update_metadata_opt.is_some() {
423 let update = update.as_ref().unwrap();
424 tracing::debug!("process contents of 'update' directory");
425 let mut update_entries = tokio::fs::read_dir(update)
426 .await
427 .with_context(|| format!("cannot open directory {:?} for reading", &update))
428 .map_err(|err| Error::new(err, link_summary))?;
429 while let Some(update_entry) = update_entries
431 .next_entry()
432 .await
433 .with_context(|| format!("failed traversing directory {:?}", &update))
434 .map_err(|err| Error::new(err, link_summary))?
435 {
436 let entry_path = update_entry.path();
437 let entry_name = entry_path.file_name().unwrap();
438 if processed_files.contains(entry_name) {
439 continue;
441 }
442 tracing::debug!("found a new entry in the 'update' directory");
443 let dst_path = dst.join(entry_name);
444 let update_path = update.join(entry_name);
445 let settings = *settings;
446 let do_copy = || async move {
447 let copy_summary = copy::copy(
448 prog_track,
449 &update_path,
450 &dst_path,
451 &settings.copy_settings,
452 &RLINK_PRESERVE_SETTINGS,
453 is_fresh,
454 )
455 .await
456 .map_err(|err| {
457 link_summary.copy_summary = link_summary.copy_summary + err.summary;
458 Error::new(err.source, link_summary)
459 })?;
460 Ok(Summary {
461 copy_summary,
462 ..Default::default()
463 })
464 };
465 join_set.spawn(do_copy());
466 }
467 drop(update_entries);
470 }
471 while let Some(res) = join_set.join_next().await {
472 match res {
473 Ok(result) => match result {
474 Ok(summary) => link_summary = link_summary + summary,
475 Err(error) => {
476 tracing::error!(
477 "link: {:?} {:?} -> {:?} failed with: {}",
478 src,
479 update,
480 dst,
481 &error
482 );
483 if settings.copy_settings.fail_early {
484 return Err(error);
485 }
486 success = false;
487 }
488 },
489 Err(error) => {
490 if settings.copy_settings.fail_early {
491 return Err(Error::new(anyhow::Error::msg(error), link_summary));
492 }
493 }
494 }
495 }
496 if !success {
497 return Err(Error::new(
498 anyhow!("link: {:?} {:?} -> {:?} failed!", src, update, dst),
499 link_summary,
500 ))?;
501 }
502 tracing::debug!("set 'dst' directory metadata");
503 let preserve_metadata = if let Some(update_metadata) = update_metadata_opt.as_ref() {
504 update_metadata
505 } else {
506 &src_metadata
507 };
508 preserve::set_dir_metadata(&RLINK_PRESERVE_SETTINGS, preserve_metadata, dst)
509 .await
510 .map_err(|err| Error::new(err, link_summary))?;
511 Ok(link_summary)
512}
513
514#[cfg(test)]
515mod link_tests {
516 use crate::testutils;
517 use std::os::unix::fs::PermissionsExt;
518 use tracing_test::traced_test;
519
520 use super::*;
521
522 lazy_static! {
523 static ref PROGRESS: progress::Progress = progress::Progress::new();
524 }
525
526 fn common_settings(dereference: bool, overwrite: bool) -> Settings {
527 Settings {
528 copy_settings: CopySettings {
529 dereference,
530 fail_early: false,
531 overwrite,
532 overwrite_compare: filecmp::MetadataCmpSettings {
533 size: true,
534 mtime: true,
535 ..Default::default()
536 },
537 chunk_size: 0,
538 },
539 update_compare: filecmp::MetadataCmpSettings {
540 size: true,
541 mtime: true,
542 ..Default::default()
543 },
544 update_exclusive: false,
545 }
546 }
547
548 #[tokio::test]
549 #[traced_test]
550 async fn test_basic_link() -> Result<(), anyhow::Error> {
551 let tmp_dir = testutils::setup_test_dir().await?;
552 let test_path = tmp_dir.as_path();
553 let summary = link(
554 &PROGRESS,
555 test_path,
556 &test_path.join("foo"),
557 &test_path.join("bar"),
558 &None,
559 &common_settings(false, false),
560 false,
561 )
562 .await?;
563 assert_eq!(summary.hard_links_created, 5);
564 assert_eq!(summary.copy_summary.files_copied, 0);
565 assert_eq!(summary.copy_summary.symlinks_created, 2);
566 assert_eq!(summary.copy_summary.directories_created, 3);
567 testutils::check_dirs_identical(
568 &test_path.join("foo"),
569 &test_path.join("bar"),
570 testutils::FileEqualityCheck::Timestamp,
571 )
572 .await?;
573 Ok(())
574 }
575
576 #[tokio::test]
577 #[traced_test]
578 async fn test_basic_link_update() -> Result<(), anyhow::Error> {
579 let tmp_dir = testutils::setup_test_dir().await?;
580 let test_path = tmp_dir.as_path();
581 let summary = link(
582 &PROGRESS,
583 test_path,
584 &test_path.join("foo"),
585 &test_path.join("bar"),
586 &Some(test_path.join("foo")),
587 &common_settings(false, false),
588 false,
589 )
590 .await?;
591 assert_eq!(summary.hard_links_created, 5);
592 assert_eq!(summary.copy_summary.files_copied, 0);
593 assert_eq!(summary.copy_summary.symlinks_created, 2);
594 assert_eq!(summary.copy_summary.directories_created, 3);
595 testutils::check_dirs_identical(
596 &test_path.join("foo"),
597 &test_path.join("bar"),
598 testutils::FileEqualityCheck::Timestamp,
599 )
600 .await?;
601 Ok(())
602 }
603
604 #[tokio::test]
605 #[traced_test]
606 async fn test_basic_link_empty_src() -> Result<(), anyhow::Error> {
607 let tmp_dir = testutils::setup_test_dir().await?;
608 tokio::fs::create_dir(tmp_dir.join("baz")).await?;
609 let test_path = tmp_dir.as_path();
610 let summary = link(
611 &PROGRESS,
612 test_path,
613 &test_path.join("baz"), &test_path.join("bar"),
615 &Some(test_path.join("foo")),
616 &common_settings(false, false),
617 false,
618 )
619 .await?;
620 assert_eq!(summary.hard_links_created, 0);
621 assert_eq!(summary.copy_summary.files_copied, 5);
622 assert_eq!(summary.copy_summary.symlinks_created, 2);
623 assert_eq!(summary.copy_summary.directories_created, 3);
624 testutils::check_dirs_identical(
625 &test_path.join("foo"),
626 &test_path.join("bar"),
627 testutils::FileEqualityCheck::Timestamp,
628 )
629 .await?;
630 Ok(())
631 }
632
633 pub async fn setup_update_dir(tmp_dir: &std::path::Path) -> Result<(), anyhow::Error> {
634 let foo_path = tmp_dir.join("update");
640 tokio::fs::create_dir(&foo_path).await.unwrap();
641 tokio::fs::write(foo_path.join("0.txt"), "0-new")
642 .await
643 .unwrap();
644 let bar_path = foo_path.join("bar");
645 tokio::fs::create_dir(&bar_path).await.unwrap();
646 tokio::fs::write(bar_path.join("1.txt"), "1-new")
647 .await
648 .unwrap();
649 tokio::fs::symlink("../1.txt", bar_path.join("2.txt"))
650 .await
651 .unwrap();
652 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
653 Ok(())
654 }
655
656 #[tokio::test]
657 #[traced_test]
658 async fn test_link_update() -> Result<(), anyhow::Error> {
659 let tmp_dir = testutils::setup_test_dir().await?;
660 setup_update_dir(&tmp_dir).await?;
661 let test_path = tmp_dir.as_path();
662 let summary = link(
663 &PROGRESS,
664 test_path,
665 &test_path.join("foo"),
666 &test_path.join("bar"),
667 &Some(test_path.join("update")),
668 &common_settings(false, false),
669 false,
670 )
671 .await?;
672 assert_eq!(summary.hard_links_created, 2);
673 assert_eq!(summary.copy_summary.files_copied, 2);
674 assert_eq!(summary.copy_summary.symlinks_created, 3);
675 assert_eq!(summary.copy_summary.directories_created, 3);
676 testutils::check_dirs_identical(
678 &test_path.join("foo").join("baz"),
679 &test_path.join("bar").join("baz"),
680 testutils::FileEqualityCheck::HardLink,
681 )
682 .await?;
683 testutils::check_dirs_identical(
685 &test_path.join("update"),
686 &test_path.join("bar"),
687 testutils::FileEqualityCheck::Timestamp,
688 )
689 .await?;
690 Ok(())
691 }
692
693 #[tokio::test]
694 #[traced_test]
695 async fn test_link_update_exclusive() -> Result<(), anyhow::Error> {
696 let tmp_dir = testutils::setup_test_dir().await?;
697 setup_update_dir(&tmp_dir).await?;
698 let test_path = tmp_dir.as_path();
699 let mut settings = common_settings(false, false);
700 settings.update_exclusive = true;
701 let summary = link(
702 &PROGRESS,
703 test_path,
704 &test_path.join("foo"),
705 &test_path.join("bar"),
706 &Some(test_path.join("update")),
707 &settings,
708 false,
709 )
710 .await?;
711 assert_eq!(summary.hard_links_created, 0);
717 assert_eq!(summary.copy_summary.files_copied, 2);
718 assert_eq!(summary.copy_summary.symlinks_created, 1);
719 assert_eq!(summary.copy_summary.directories_created, 2);
720 testutils::check_dirs_identical(
722 &test_path.join("update"),
723 &test_path.join("bar"),
724 testutils::FileEqualityCheck::Timestamp,
725 )
726 .await?;
727 Ok(())
728 }
729
730 async fn setup_test_dir_and_link() -> Result<std::path::PathBuf, anyhow::Error> {
731 let tmp_dir = testutils::setup_test_dir().await?;
732 let test_path = tmp_dir.as_path();
733 let summary = link(
734 &PROGRESS,
735 test_path,
736 &test_path.join("foo"),
737 &test_path.join("bar"),
738 &None,
739 &common_settings(false, false),
740 false,
741 )
742 .await?;
743 assert_eq!(summary.hard_links_created, 5);
744 assert_eq!(summary.copy_summary.symlinks_created, 2);
745 assert_eq!(summary.copy_summary.directories_created, 3);
746 Ok(tmp_dir)
747 }
748
749 #[tokio::test]
750 #[traced_test]
751 async fn test_link_overwrite_basic() -> Result<(), anyhow::Error> {
752 let tmp_dir = setup_test_dir_and_link().await?;
753 let output_path = &tmp_dir.join("bar");
754 {
755 let summary = rm::rm(
766 &PROGRESS,
767 &output_path.join("bar"),
768 &rm::Settings { fail_early: false },
769 )
770 .await?
771 + rm::rm(
772 &PROGRESS,
773 &output_path.join("baz").join("5.txt"),
774 &rm::Settings { fail_early: false },
775 )
776 .await?;
777 assert_eq!(summary.files_removed, 3);
778 assert_eq!(summary.symlinks_removed, 1);
779 assert_eq!(summary.directories_removed, 1);
780 }
781 let summary = link(
782 &PROGRESS,
783 &tmp_dir,
784 &tmp_dir.join("foo"),
785 output_path,
786 &None,
787 &common_settings(false, true), false,
789 )
790 .await?;
791 assert_eq!(summary.hard_links_created, 3);
792 assert_eq!(summary.copy_summary.symlinks_created, 1);
793 assert_eq!(summary.copy_summary.directories_created, 1);
794 testutils::check_dirs_identical(
795 &tmp_dir.join("foo"),
796 output_path,
797 testutils::FileEqualityCheck::Timestamp,
798 )
799 .await?;
800 Ok(())
801 }
802
803 #[tokio::test]
804 #[traced_test]
805 async fn test_link_update_overwrite_basic() -> Result<(), anyhow::Error> {
806 let tmp_dir = setup_test_dir_and_link().await?;
807 let output_path = &tmp_dir.join("bar");
808 {
809 let summary = rm::rm(
820 &PROGRESS,
821 &output_path.join("bar"),
822 &rm::Settings { fail_early: false },
823 )
824 .await?
825 + rm::rm(
826 &PROGRESS,
827 &output_path.join("baz").join("5.txt"),
828 &rm::Settings { fail_early: false },
829 )
830 .await?;
831 assert_eq!(summary.files_removed, 3);
832 assert_eq!(summary.symlinks_removed, 1);
833 assert_eq!(summary.directories_removed, 1);
834 }
835 setup_update_dir(&tmp_dir).await?;
836 let summary = link(
842 &PROGRESS,
843 &tmp_dir,
844 &tmp_dir.join("foo"),
845 output_path,
846 &Some(tmp_dir.join("update")),
847 &common_settings(false, true), false,
849 )
850 .await?;
851 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);
855 testutils::check_dirs_identical(
857 &tmp_dir.join("foo").join("baz"),
858 &tmp_dir.join("bar").join("baz"),
859 testutils::FileEqualityCheck::HardLink,
860 )
861 .await?;
862 testutils::check_dirs_identical(
864 &tmp_dir.join("update"),
865 &tmp_dir.join("bar"),
866 testutils::FileEqualityCheck::Timestamp,
867 )
868 .await?;
869 Ok(())
870 }
871
872 #[tokio::test]
873 #[traced_test]
874 async fn test_link_overwrite_hardlink_file() -> Result<(), anyhow::Error> {
875 let tmp_dir = setup_test_dir_and_link().await?;
876 let output_path = &tmp_dir.join("bar");
877 {
878 let bar_path = output_path.join("bar");
887 let summary = rm::rm(
888 &PROGRESS,
889 &bar_path.join("1.txt"),
890 &rm::Settings { fail_early: false },
891 )
892 .await?
893 + rm::rm(
894 &PROGRESS,
895 &bar_path.join("2.txt"),
896 &rm::Settings { fail_early: false },
897 )
898 .await?
899 + rm::rm(
900 &PROGRESS,
901 &bar_path.join("3.txt"),
902 &rm::Settings { fail_early: false },
903 )
904 .await?
905 + rm::rm(
906 &PROGRESS,
907 &output_path.join("baz"),
908 &rm::Settings { fail_early: false },
909 )
910 .await?;
911 assert_eq!(summary.files_removed, 4);
912 assert_eq!(summary.symlinks_removed, 2);
913 assert_eq!(summary.directories_removed, 1);
914 tokio::fs::write(bar_path.join("1.txt"), "1-new")
916 .await
917 .unwrap();
918 tokio::fs::symlink("../0.txt", bar_path.join("2.txt"))
919 .await
920 .unwrap();
921 tokio::fs::create_dir(&bar_path.join("3.txt"))
922 .await
923 .unwrap();
924 tokio::fs::write(&output_path.join("baz"), "baz")
925 .await
926 .unwrap();
927 }
928 let summary = link(
929 &PROGRESS,
930 &tmp_dir,
931 &tmp_dir.join("foo"),
932 output_path,
933 &None,
934 &common_settings(false, true), false,
936 )
937 .await?;
938 assert_eq!(summary.hard_links_created, 4);
939 assert_eq!(summary.copy_summary.files_copied, 0);
940 assert_eq!(summary.copy_summary.symlinks_created, 2);
941 assert_eq!(summary.copy_summary.directories_created, 1);
942 testutils::check_dirs_identical(
943 &tmp_dir.join("foo"),
944 &tmp_dir.join("bar"),
945 testutils::FileEqualityCheck::HardLink,
946 )
947 .await?;
948 Ok(())
949 }
950
951 #[tokio::test]
952 #[traced_test]
953 async fn test_link_overwrite_error() -> Result<(), anyhow::Error> {
954 let tmp_dir = setup_test_dir_and_link().await?;
955 let output_path = &tmp_dir.join("bar");
956 {
957 let bar_path = output_path.join("bar");
966 let summary = rm::rm(
967 &PROGRESS,
968 &bar_path.join("1.txt"),
969 &rm::Settings { fail_early: false },
970 )
971 .await?
972 + rm::rm(
973 &PROGRESS,
974 &bar_path.join("2.txt"),
975 &rm::Settings { fail_early: false },
976 )
977 .await?
978 + rm::rm(
979 &PROGRESS,
980 &bar_path.join("3.txt"),
981 &rm::Settings { fail_early: false },
982 )
983 .await?
984 + rm::rm(
985 &PROGRESS,
986 &output_path.join("baz"),
987 &rm::Settings { fail_early: false },
988 )
989 .await?;
990 assert_eq!(summary.files_removed, 4);
991 assert_eq!(summary.symlinks_removed, 2);
992 assert_eq!(summary.directories_removed, 1);
993 tokio::fs::write(bar_path.join("1.txt"), "1-new")
995 .await
996 .unwrap();
997 tokio::fs::symlink("../0.txt", bar_path.join("2.txt"))
998 .await
999 .unwrap();
1000 tokio::fs::create_dir(&bar_path.join("3.txt"))
1001 .await
1002 .unwrap();
1003 tokio::fs::write(&output_path.join("baz"), "baz")
1004 .await
1005 .unwrap();
1006 }
1007 let source_path = &tmp_dir.join("foo");
1008 tokio::fs::set_permissions(
1010 &source_path.join("baz"),
1011 std::fs::Permissions::from_mode(0o000),
1012 )
1013 .await?;
1014 match link(
1018 &PROGRESS,
1019 &tmp_dir,
1020 &tmp_dir.join("foo"),
1021 output_path,
1022 &None,
1023 &common_settings(false, true), false,
1025 )
1026 .await
1027 {
1028 Ok(_) => panic!("Expected the link to error!"),
1029 Err(error) => {
1030 tracing::info!("{}", &error);
1031 assert_eq!(error.summary.hard_links_created, 3);
1032 assert_eq!(error.summary.copy_summary.files_copied, 0);
1033 assert_eq!(error.summary.copy_summary.symlinks_created, 0);
1034 assert_eq!(error.summary.copy_summary.directories_created, 0);
1035 assert_eq!(error.summary.copy_summary.rm_summary.files_removed, 1);
1036 assert_eq!(error.summary.copy_summary.rm_summary.directories_removed, 1);
1037 assert_eq!(error.summary.copy_summary.rm_summary.symlinks_removed, 1);
1038 }
1039 }
1040 Ok(())
1041 }
1042}