Skip to main content

hd_mount/
manager.rs

1use std::path::{Path, PathBuf};
2
3use hd_cas::ContentHash;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum MountState {
7    Unmounted,
8    Mounted,
9}
10
11/// Handle to a mounted filesystem.
12pub struct MountHandle {
13    pub mountpoint: PathBuf,
14    pub env_id: ContentHash,
15    pub state: MountState,
16}
17
18#[derive(Debug, thiserror::Error)]
19pub enum MountError {
20    #[error("mount failed: {0}")]
21    MountFailed(String),
22    #[error("already mounted at {0}")]
23    AlreadyMounted(String),
24    #[error("not mounted")]
25    NotMounted,
26    #[error("I/O error: {0}")]
27    Io(#[from] std::io::Error),
28}
29
30/// Manages mount/unmount lifecycle for projected filesystems.
31pub struct MountManager {
32    mounts_dir: PathBuf,
33    handles: Vec<MountHandle>,
34}
35
36impl MountManager {
37    pub fn new(mounts_dir: &Path) -> Result<Self, MountError> {
38        std::fs::create_dir_all(mounts_dir)?;
39        Ok(MountManager {
40            mounts_dir: mounts_dir.to_path_buf(),
41            handles: Vec::new(),
42        })
43    }
44
45    /// Get the mountpoint path for an environment.
46    pub fn mountpoint_for(&self, env_id: &ContentHash) -> PathBuf {
47        self.mounts_dir.join(&env_id.to_hex()[..12])
48    }
49
50    /// Register a mount (called after FUSE mount succeeds).
51    pub fn register_mount(&mut self, env_id: ContentHash) -> Result<&MountHandle, MountError> {
52        let mountpoint = self.mountpoint_for(&env_id);
53        if self.is_mounted(&mountpoint) {
54            return Err(MountError::AlreadyMounted(mountpoint.display().to_string()));
55        }
56        std::fs::create_dir_all(&mountpoint)?;
57        self.handles.push(MountHandle {
58            mountpoint,
59            env_id,
60            state: MountState::Mounted,
61        });
62        Ok(self.handles.last().unwrap())
63    }
64
65    /// Unregister a mount (called after FUSE unmount succeeds).
66    pub fn unregister_mount(&mut self, mountpoint: &Path) -> Result<(), MountError> {
67        if let Some(handle) = self.handles.iter_mut().find(|h| h.mountpoint == mountpoint) {
68            handle.state = MountState::Unmounted;
69            Ok(())
70        } else {
71            Err(MountError::NotMounted)
72        }
73    }
74
75    /// Check if a mountpoint is currently mounted.
76    pub fn is_mounted(&self, mountpoint: &Path) -> bool {
77        self.handles.iter().any(|h| h.mountpoint == mountpoint && h.state == MountState::Mounted)
78    }
79
80    /// Get all active mount handles.
81    pub fn active_mounts(&self) -> Vec<&MountHandle> {
82        self.handles.iter().filter(|h| h.state == MountState::Mounted).collect()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn register_and_unregister_mount() {
92        let dir = tempfile::TempDir::new().unwrap();
93        let mut manager = MountManager::new(dir.path()).unwrap();
94        let env_id = ContentHash::from_bytes(b"test-env");
95
96        let handle = manager.register_mount(env_id).unwrap();
97        assert_eq!(handle.state, MountState::Mounted);
98
99        let mountpoint = manager.mountpoint_for(&env_id);
100        assert!(manager.is_mounted(&mountpoint));
101
102        manager.unregister_mount(&mountpoint).unwrap();
103        assert!(!manager.is_mounted(&mountpoint));
104    }
105
106    #[test]
107    fn double_mount_errors() {
108        let dir = tempfile::TempDir::new().unwrap();
109        let mut manager = MountManager::new(dir.path()).unwrap();
110        let env_id = ContentHash::from_bytes(b"test-env");
111
112        manager.register_mount(env_id).unwrap();
113        assert!(manager.register_mount(env_id).is_err());
114    }
115
116    #[test]
117    fn active_mounts_filtering() {
118        let dir = tempfile::TempDir::new().unwrap();
119        let mut manager = MountManager::new(dir.path()).unwrap();
120
121        let e1 = ContentHash::from_bytes(b"env1");
122        let e2 = ContentHash::from_bytes(b"env2");
123        manager.register_mount(e1).unwrap();
124        manager.register_mount(e2).unwrap();
125        assert_eq!(manager.active_mounts().len(), 2);
126
127        let mp1 = manager.mountpoint_for(&e1);
128        manager.unregister_mount(&mp1).unwrap();
129        assert_eq!(manager.active_mounts().len(), 1);
130    }
131}