Skip to main content

vanta_store/
link.rs

1//! Link strategies for composing environment views (`docs/09-store.md`).
2//!
3//! Order of preference: hardlink → symlink → copy. (Reflink/CoW is a
4//! platform-specific syscall added later; `docs/17-cross-platform.md`.) On
5//! Windows, symlinks may require privilege, so copy is the reliable fallback.
6
7use std::fs;
8use std::path::Path;
9use vanta_core::{Area, VtaError, VtaResult};
10
11/// Link `src` to `dst` using the cheapest mechanism that succeeds. Returns the
12/// name of the strategy used.
13pub fn link_best(src: &Path, dst: &Path) -> VtaResult<&'static str> {
14    if let Some(parent) = dst.parent() {
15        fs::create_dir_all(parent).map_err(|e| err(dst, e))?;
16    }
17    let _ = fs::remove_file(dst); // replace an existing link/file
18
19    if fs::hard_link(src, dst).is_ok() {
20        return Ok("hardlink");
21    }
22    if symlink(src, dst).is_ok() {
23        return Ok("symlink");
24    }
25    fs::copy(src, dst).map_err(|e| err(dst, e))?;
26    Ok("copy")
27}
28
29#[cfg(unix)]
30fn symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
31    std::os::unix::fs::symlink(src, dst)
32}
33
34#[cfg(windows)]
35fn symlink(src: &Path, dst: &Path) -> std::io::Result<()> {
36    std::os::windows::fs::symlink_file(src, dst)
37}
38
39fn err(path: &Path, e: std::io::Error) -> VtaError {
40    VtaError::new(Area::Store, 3, format!("linking {}: {e}", path.display()))
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn links_a_file() {
49        let dir = std::env::temp_dir().join(format!("vanta-link-{}", std::process::id()));
50        let _ = fs::remove_dir_all(&dir);
51        fs::create_dir_all(&dir).unwrap();
52        let src = dir.join("src.bin");
53        let dst = dir.join("bin/tool");
54        fs::write(&src, b"#!/bin/true").unwrap();
55        let how = link_best(&src, &dst).unwrap();
56        assert!(["hardlink", "symlink", "copy"].contains(&how));
57        assert_eq!(fs::read(&dst).unwrap(), b"#!/bin/true");
58        let _ = fs::remove_dir_all(&dir);
59    }
60}