Skip to main content

pulith_fs/primitives/
hardlink.rs

1use crate::{Error, Result};
2use std::path::Path;
3
4#[derive(Clone, Copy, Debug, Default)]
5pub enum FallBack {
6    #[default]
7    Copy,
8    Error,
9}
10
11#[derive(Clone, Copy, Debug, Default)]
12pub struct Options {
13    pub fallback: FallBack,
14}
15
16impl Options {
17    pub fn new() -> Self {
18        Self::default()
19    }
20    pub fn fallback(mut self, fallback: FallBack) -> Self {
21        self.fallback = fallback;
22        self
23    }
24}
25
26pub fn hardlink_or_copy(
27    src: impl AsRef<Path>,
28    dest: impl AsRef<Path>,
29    options: Options,
30) -> Result<()> {
31    let src = src.as_ref();
32    let dest = dest.as_ref();
33
34    if src.is_dir() {
35        if matches!(options.fallback, FallBack::Copy) {
36            return crate::primitives::copy_dir::copy_dir_all(src, dest);
37        }
38
39        return Err(Error::Write {
40            path: dest.to_path_buf(),
41            source: std::io::Error::new(
42                std::io::ErrorKind::Unsupported,
43                "hard-linking directories is not supported",
44            ),
45        });
46    }
47
48    match std::fs::hard_link(src, dest) {
49        Ok(_) => Ok(()),
50        Err(e)
51            if e.raw_os_error() == Some(18) || e.kind() == std::io::ErrorKind::CrossesDevices =>
52        {
53            if matches!(options.fallback, FallBack::Copy) {
54                std::fs::copy(src, dest)
55                    .map(drop)
56                    .map_err(|e| Error::Write {
57                        path: dest.to_path_buf(),
58                        source: e,
59                    })
60            } else {
61                Err(Error::CrossDeviceHardlink)
62            }
63        }
64        Err(e) => Err(Error::Write {
65            path: dest.to_path_buf(),
66            source: e,
67        }),
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use tempfile::tempdir;
75
76    #[test]
77    fn test_hardlink_or_copy() {
78        let dir = tempdir().unwrap();
79        let src = dir.path().join("src.txt");
80        let dest = dir.path().join("dest.txt");
81        std::fs::write(&src, "data").unwrap();
82
83        hardlink_or_copy(&src, &dest, Options::new()).unwrap();
84        assert!(dest.exists());
85    }
86
87    #[test]
88    fn test_hardlink_or_copy_cross_device() {
89        let dir = tempdir().unwrap();
90        let src = dir.path().join("src.txt");
91        let dest = dir.path().join("dest.txt");
92        std::fs::write(&src, "data").unwrap();
93
94        let options = Options::new().fallback(FallBack::Copy);
95        hardlink_or_copy(&src, &dest, options).unwrap();
96        assert_eq!(std::fs::read(&dest).unwrap(), b"data");
97    }
98
99    #[test]
100    fn test_hardlink_or_copy_directory_with_copy_fallback() {
101        let dir = tempdir().unwrap();
102        let src = dir.path().join("src_dir");
103        let dest = dir.path().join("dest_dir");
104        std::fs::create_dir_all(&src).unwrap();
105        std::fs::write(src.join("file.txt"), "data").unwrap();
106
107        let options = Options::new().fallback(FallBack::Copy);
108        hardlink_or_copy(&src, &dest, options).unwrap();
109
110        assert!(dest.is_dir());
111        assert_eq!(std::fs::read(dest.join("file.txt")).unwrap(), b"data");
112    }
113}