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
11pub 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
30pub 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 pub fn mountpoint_for(&self, env_id: &ContentHash) -> PathBuf {
47 self.mounts_dir.join(&env_id.to_hex()[..12])
48 }
49
50 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 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 pub fn is_mounted(&self, mountpoint: &Path) -> bool {
77 self.handles.iter().any(|h| h.mountpoint == mountpoint && h.state == MountState::Mounted)
78 }
79
80 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}