common/
link.rs

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                            // the path is missing from update, we're done
149                            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            // file type changed, just copy the updated one
167            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            // check if the file is unchanged and if so hard-link, otherwise copy from the updated one
198            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            // use "copy" function to handle the overwrite logic
231            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        // update hasn't been specified, if this is a file just hard-link the source or symlink if it's a symlink
255        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            // use "copy" function to handle the overwrite logic
262            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                // check if the destination is a directory - if so, leave it
308                //
309                // N.B. the permissions may prevent us from writing to it but the alternative is to open up the directory
310                // while we're writing to it which isn't safe
311                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                    // anything copied into dst may assume they don't need to check for conflicts
357                    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            // new directory created, anything copied into dst may assume they don't need to check for conflicts
371            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    // create a set of all the files we already processed
385    let mut processed_files = std::collections::HashSet::new();
386    // iterate through src entries and recursively call "link" on each one
387    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        // it's better to await the token here so that we throttle the syscalls generated by the
394        // DirEntry call. the ops-throttle will never cause a deadlock (unlike max-open-files limit)
395        // so it's safe to do here.
396        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    // unfortunately ReadDir is opening file-descriptors and there's not a good way to limit this,
419    // one thing we CAN do however is to drop it as soon as we're done with it
420    drop(src_entries);
421    // only process update if the path was provided and the directory is present
422    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        // iterate through update entries and for each one that's not present in src call "copy"
430        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                // we already must have considered this file, skip it
440                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        // unfortunately ReadDir is opening file-descriptors and there's not a good way to limit this,
468        // one thing we CAN do however is to drop it as soon as we're done with it
469        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"), // empty source
614            &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        // update
635        // |- 0.txt
636        // |- bar
637        //    |- 1.txt
638        //    |- 2.txt -> ../0.txt
639        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        // compare subset of src and dst
677        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        // compare update and dst
684        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        // we should end up with same directory as the update
712        // |- 0.txt
713        // |- bar
714        //    |- 1.txt
715        //    |- 2.txt -> ../0.txt
716        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        // compare update and dst
721        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            // bar
756            // |- 0.txt
757            // |- bar  <---------------------------------------- REMOVE
758            //    |- 1.txt  <----------------------------------- REMOVE
759            //    |- 2.txt  <----------------------------------- REMOVE
760            //    |- 3.txt  <----------------------------------- REMOVE
761            // |- baz
762            //    |- 4.txt
763            //    |- 5.txt -> ../bar/2.txt <-------------------- REMOVE
764            //    |- 6.txt -> (absolute path) .../foo/bar/3.txt
765            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), // overwrite!
788            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            // bar
810            // |- 0.txt
811            // |- bar  <---------------------------------------- REMOVE
812            //    |- 1.txt  <----------------------------------- REMOVE
813            //    |- 2.txt  <----------------------------------- REMOVE
814            //    |- 3.txt  <----------------------------------- REMOVE
815            // |- baz
816            //    |- 4.txt
817            //    |- 5.txt -> ../bar/2.txt <-------------------- REMOVE
818            //    |- 6.txt -> (absolute path) .../foo/bar/3.txt
819            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        // update
837        // |- 0.txt
838        // |- bar
839        //    |- 1.txt
840        //    |- 2.txt -> ../0.txt
841        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), // overwrite!
848            false,
849        )
850        .await?;
851        assert_eq!(summary.hard_links_created, 1); // 3.txt
852        assert_eq!(summary.copy_summary.files_copied, 2); // 0.txt, 1.txt
853        assert_eq!(summary.copy_summary.symlinks_created, 2); // 2.txt, 5.txt
854        assert_eq!(summary.copy_summary.directories_created, 1);
855        // compare subset of src and dst
856        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        // compare update and dst
863        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            // bar
879            // |- 0.txt
880            // |- bar
881            //    |- 1.txt  <----------------------------------- REPLACE W/ FILE
882            //    |- 2.txt  <----------------------------------- REPLACE W/ SYMLINK
883            //    |- 3.txt  <----------------------------------- REPLACE W/ DIRECTORY
884            // |- baz    <-------------------------------------- REPLACE W/ FILE
885            //    |- ...
886            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            // REPLACE with a file, a symlink, a directory and a file
915            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), // overwrite!
935            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            // bar
958            // |- 0.txt
959            // |- bar
960            //    |- 1.txt  <----------------------------------- REPLACE W/ FILE
961            //    |- 2.txt  <----------------------------------- REPLACE W/ SYMLINK
962            //    |- 3.txt  <----------------------------------- REPLACE W/ DIRECTORY
963            // |- baz    <-------------------------------------- REPLACE W/ FILE
964            //    |- ...
965            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            // REPLACE with a file, a symlink, a directory and a file
994            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        // unreadable
1009        tokio::fs::set_permissions(
1010            &source_path.join("baz"),
1011            std::fs::Permissions::from_mode(0o000),
1012        )
1013        .await?;
1014        // bar
1015        // |- ...
1016        // |- baz <- NON READABLE
1017        match link(
1018            &PROGRESS,
1019            &tmp_dir,
1020            &tmp_dir.join("foo"),
1021            output_path,
1022            &None,
1023            &common_settings(false, true), // overwrite!
1024            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}