pulith_fs/primitives/
hardlink.rs1use 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}