1use camino::Utf8Path;
11
12use crate::config::{DirLinkMode, FileLinkMode};
13use crate::{Error, Result};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum EffectiveFileMode {
17 Symlink,
18 Hardlink,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum EffectiveDirMode {
23 Symlink,
24 Junction,
25}
26
27pub fn resolve_file_mode(mode: FileLinkMode) -> EffectiveFileMode {
28 match mode {
29 FileLinkMode::Symlink => EffectiveFileMode::Symlink,
30 FileLinkMode::Hardlink => EffectiveFileMode::Hardlink,
31 FileLinkMode::Auto => {
32 if cfg!(windows) {
33 EffectiveFileMode::Hardlink
34 } else {
35 EffectiveFileMode::Symlink
36 }
37 }
38 }
39}
40
41pub fn resolve_dir_mode(mode: DirLinkMode) -> EffectiveDirMode {
42 match mode {
43 DirLinkMode::Symlink => EffectiveDirMode::Symlink,
44 DirLinkMode::Junction => EffectiveDirMode::Junction,
45 DirLinkMode::Auto => {
46 if cfg!(windows) {
47 EffectiveDirMode::Junction
48 } else {
49 EffectiveDirMode::Symlink
50 }
51 }
52 }
53}
54
55pub fn link_file(src: &Utf8Path, dst: &Utf8Path, mode: EffectiveFileMode) -> Result<()> {
56 if let Some(parent) = dst.parent() {
57 std::fs::create_dir_all(parent)?;
58 }
59 match mode {
60 EffectiveFileMode::Hardlink => std::fs::hard_link(src, dst)?,
61 EffectiveFileMode::Symlink => create_file_symlink(src, dst)?,
62 }
63 Ok(())
64}
65
66pub fn link_dir(src: &Utf8Path, dst: &Utf8Path, mode: EffectiveDirMode) -> Result<()> {
67 if let Some(parent) = dst.parent() {
68 std::fs::create_dir_all(parent)?;
69 }
70 match mode {
71 EffectiveDirMode::Junction => create_junction(src, dst)?,
72 EffectiveDirMode::Symlink => create_dir_symlink(src, dst)?,
73 }
74 Ok(())
75}
76
77pub fn unlink(dst: &Utf8Path) -> Result<()> {
80 let meta = match std::fs::symlink_metadata(dst) {
81 Ok(m) => m,
82 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
83 Err(e) => return Err(Error::Io(e)),
84 };
85 let ft = meta.file_type();
86
87 if ft.is_symlink() {
88 #[cfg(windows)]
89 {
90 if junction::delete(dst.as_std_path()).is_ok() {
96 let _ = std::fs::remove_dir(dst);
97 return Ok(());
98 }
99 if std::fs::remove_file(dst).is_ok() {
100 return Ok(());
101 }
102 std::fs::remove_dir(dst)?;
103 return Ok(());
104 }
105 #[cfg(unix)]
106 {
107 std::fs::remove_file(dst)?;
108 return Ok(());
109 }
110 }
111
112 if ft.is_dir() {
113 #[cfg(windows)]
114 {
115 return remove_link_dir_windows(dst);
116 }
117 #[cfg(unix)]
118 return std::fs::remove_dir(dst).map_err(|e| {
119 Error::Other(anyhow::anyhow!(
120 "unlink: {dst} not removed as a directory link (regular dir with content?): {e}"
121 ))
122 });
123 }
124
125 std::fs::remove_file(dst)?;
128 Ok(())
129}
130
131#[cfg(unix)]
132fn create_file_symlink(src: &Utf8Path, dst: &Utf8Path) -> Result<()> {
133 std::os::unix::fs::symlink(src, dst)?;
134 Ok(())
135}
136
137#[cfg(unix)]
138fn create_dir_symlink(src: &Utf8Path, dst: &Utf8Path) -> Result<()> {
139 std::os::unix::fs::symlink(src, dst)?;
140 Ok(())
141}
142
143#[cfg(unix)]
144fn create_junction(_src: &Utf8Path, _dst: &Utf8Path) -> Result<()> {
145 Err(Error::Other(anyhow::anyhow!(
146 "junctions are Windows-only; use symlink mode on Unix"
147 )))
148}
149
150#[cfg(windows)]
151fn create_file_symlink(src: &Utf8Path, dst: &Utf8Path) -> Result<()> {
152 std::os::windows::fs::symlink_file(src, dst)?;
153 Ok(())
154}
155
156#[cfg(windows)]
157fn create_dir_symlink(src: &Utf8Path, dst: &Utf8Path) -> Result<()> {
158 std::os::windows::fs::symlink_dir(src, dst)?;
159 Ok(())
160}
161
162#[cfg(windows)]
163fn create_junction(src: &Utf8Path, dst: &Utf8Path) -> Result<()> {
164 junction::create(src.as_std_path(), dst.as_std_path())?;
165 Ok(())
166}
167
168#[cfg(windows)]
173fn remove_link_dir_windows(dst: &Utf8Path) -> Result<()> {
174 if junction::delete(dst.as_std_path()).is_ok() {
175 let _ = std::fs::remove_dir(dst);
176 return Ok(());
177 }
178 std::fs::remove_dir(dst).map_err(|e| {
179 Error::Other(anyhow::anyhow!(
180 "unlink: {dst} not removed as a directory link: {e}"
181 ))
182 })
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use camino::Utf8PathBuf;
189 use tempfile::TempDir;
190
191 fn utf8(p: std::path::PathBuf) -> Utf8PathBuf {
192 Utf8PathBuf::from_path_buf(p).unwrap()
193 }
194
195 #[test]
196 fn auto_resolves_per_platform() {
197 let f = resolve_file_mode(FileLinkMode::Auto);
198 let d = resolve_dir_mode(DirLinkMode::Auto);
199 if cfg!(windows) {
200 assert_eq!(f, EffectiveFileMode::Hardlink);
201 assert_eq!(d, EffectiveDirMode::Junction);
202 } else {
203 assert_eq!(f, EffectiveFileMode::Symlink);
204 assert_eq!(d, EffectiveDirMode::Symlink);
205 }
206 }
207
208 #[test]
209 fn explicit_overrides_auto() {
210 assert_eq!(
211 resolve_file_mode(FileLinkMode::Symlink),
212 EffectiveFileMode::Symlink
213 );
214 assert_eq!(
215 resolve_dir_mode(DirLinkMode::Junction),
216 EffectiveDirMode::Junction
217 );
218 }
219
220 #[test]
221 fn hardlink_file_and_unlink() {
222 let tmp = TempDir::new().unwrap();
223 let src = utf8(tmp.path().join("src.txt"));
224 std::fs::write(&src, "hello").unwrap();
225
226 let dst = utf8(tmp.path().join("nested/dst.txt"));
227 link_file(&src, &dst, EffectiveFileMode::Hardlink).unwrap();
228
229 assert_eq!(std::fs::read_to_string(&dst).unwrap(), "hello");
230
231 std::fs::write(&dst, "updated").unwrap();
233 assert_eq!(std::fs::read_to_string(&src).unwrap(), "updated");
234
235 unlink(&dst).unwrap();
236 assert!(!dst.exists());
237 assert!(src.exists());
238 }
239
240 #[test]
241 fn unlink_missing_is_noop() {
242 let tmp = TempDir::new().unwrap();
243 let dst = utf8(tmp.path().join("nonexistent"));
244 unlink(&dst).unwrap();
245 }
246
247 #[cfg(windows)]
248 #[test]
249 fn junction_dir_and_unlink() {
250 let tmp = TempDir::new().unwrap();
251 std::fs::create_dir_all(tmp.path().join("src_dir/sub")).unwrap();
252 std::fs::write(tmp.path().join("src_dir/a.txt"), "A").unwrap();
253 let src = utf8(std::fs::canonicalize(tmp.path().join("src_dir")).unwrap());
254
255 let dst = utf8(tmp.path().join("nested/dst_dir"));
256 link_dir(&src, &dst, EffectiveDirMode::Junction).unwrap();
257
258 assert_eq!(std::fs::read_to_string(dst.join("a.txt")).unwrap(), "A");
259 assert!(dst.join("sub").is_dir());
260
261 unlink(&dst).unwrap();
262 assert!(!dst.exists());
263 assert!(src.join("a.txt").exists());
264 }
265
266 #[cfg(unix)]
267 #[test]
268 fn symlink_file_and_unlink() {
269 let tmp = TempDir::new().unwrap();
270 let src = utf8(tmp.path().join("src.txt"));
271 std::fs::write(&src, "hello").unwrap();
272
273 let dst = utf8(tmp.path().join("nested/dst.txt"));
274 link_file(&src, &dst, EffectiveFileMode::Symlink).unwrap();
275
276 assert_eq!(std::fs::read_to_string(&dst).unwrap(), "hello");
277
278 unlink(&dst).unwrap();
279 assert!(!dst.exists());
280 assert!(src.exists());
281 }
282
283 #[cfg(unix)]
284 #[test]
285 fn symlink_dir_and_unlink() {
286 let tmp = TempDir::new().unwrap();
287 std::fs::create_dir_all(tmp.path().join("src_dir")).unwrap();
288 std::fs::write(tmp.path().join("src_dir/a.txt"), "A").unwrap();
289 let src = utf8(tmp.path().join("src_dir"));
290
291 let dst = utf8(tmp.path().join("nested/dst_dir"));
292 link_dir(&src, &dst, EffectiveDirMode::Symlink).unwrap();
293
294 assert_eq!(std::fs::read_to_string(dst.join("a.txt")).unwrap(), "A");
295
296 unlink(&dst).unwrap();
297 assert!(!dst.exists());
298 assert!(src.join("a.txt").exists());
299 }
300}