1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4use std::time::SystemTime;
5
6use crate::platform::common::MAX_MOUNT_RETRIES;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct MountInfo {
11 pub target: PathBuf,
13
14 pub sources: Vec<PathBuf>,
16
17 pub status: MountStatus,
19
20 pub fs_type: String,
22
23 pub options: Vec<String>,
25
26 pub mounted_at: Option<SystemTime>,
28
29 pub pid: Option<u32>,
31
32 pub metadata: MountMetadata,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub enum MountStatus {
39 Mounted,
41
42 Unmounted,
44
45 Degraded(String),
47
48 Error(String),
50
51 Unknown,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub enum MountMetadata {
58 Linux {
59 mount_id: Option<u32>,
60 parent_id: Option<u32>,
61 major_minor: Option<String>,
62 },
63 MacOS {
64 volume_name: Option<String>,
65 volume_uuid: Option<String>,
66 disk_identifier: Option<String>,
67 },
68 Unknown,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct MountOptions {
74 pub read_only: bool,
76
77 pub allow_other: bool,
79
80 pub volume_name: Option<String>,
82
83 pub extra_options: Vec<String>,
85
86 pub timeout: Option<std::time::Duration>,
88
89 pub retries: u32,
91}
92
93impl Default for MountOptions {
94 fn default() -> Self {
95 Self {
96 read_only: false,
97 allow_other: false,
98 volume_name: None,
99 extra_options: Vec::new(),
100 timeout: None,
101 retries: MAX_MOUNT_RETRIES,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct MountStateCache {
109 pub version: String,
110 pub mounts: HashMap<PathBuf, CachedMountInfo>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct CachedMountInfo {
116 pub target: PathBuf,
117 pub sources: Vec<PathBuf>,
118 pub mount_options: MountOptions,
119 pub created_at: SystemTime,
120 pub mount_command: String,
121 pub pid: Option<u32>,
122}
123
124use anyhow::Result;
125use std::fmt;
126
127#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134pub enum MountSpace {
135 Thoughts,
137
138 Context(String),
140
141 Reference {
143 org: String,
145 repo: String,
147 },
148}
149
150impl MountSpace {
151 pub fn parse(input: &str) -> Result<Self> {
153 if input == "thoughts" {
154 Ok(MountSpace::Thoughts)
155 } else if input.starts_with("references/") {
156 let parts: Vec<&str> = input.splitn(3, '/').collect();
157 if parts.len() == 3 && parts[0] == "references" {
158 Ok(MountSpace::Reference {
159 org: parts[1].to_string(),
160 repo: parts[2].to_string(),
161 })
162 } else {
163 anyhow::bail!("Invalid reference format: {}", input)
164 }
165 } else if let Some(rest) = input.strip_prefix("context/") {
166 if rest.is_empty() {
167 anyhow::bail!(
168 "Invalid context mount name '{}': missing mount path after 'context/'",
169 input
170 );
171 }
172 Ok(MountSpace::Context(rest.to_string()))
173 } else {
174 Ok(MountSpace::Context(input.to_string()))
176 }
177 }
178
179 pub fn as_str(&self) -> String {
181 match self {
182 MountSpace::Thoughts => "thoughts".to_string(),
183 MountSpace::Context(path) => path.clone(),
184 MountSpace::Reference { org, repo } => format!("references/{}/{}", org, repo),
185 }
186 }
187
188 pub fn relative_path(&self, mount_dirs: &crate::config::MountDirsV2) -> String {
190 match self {
191 MountSpace::Thoughts => mount_dirs.thoughts.clone(),
192 MountSpace::Context(path) => format!("{}/{}", mount_dirs.context, path),
193 MountSpace::Reference { org, repo } => {
194 format!("{}/{}/{}", mount_dirs.references, org, repo)
195 }
196 }
197 }
198
199 pub fn is_read_only(&self) -> bool {
201 matches!(self, MountSpace::Reference { .. })
202 }
203}
204
205impl fmt::Display for MountSpace {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 write!(f, "{}", self.as_str())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_mount_options_default() {
217 let options = MountOptions::default();
218
219 assert!(!options.read_only);
220 assert!(!options.allow_other);
221 assert_eq!(options.retries, MAX_MOUNT_RETRIES);
222 assert_eq!(options.volume_name, None);
223 }
224
225 #[test]
226 fn test_mount_status_serialization() {
227 let status = MountStatus::Mounted;
228 let json = serde_json::to_string(&status).unwrap();
229 let deserialized: MountStatus = serde_json::from_str(&json).unwrap();
230 assert_eq!(status, deserialized);
231 }
232
233 #[test]
234 fn test_mount_space_parse() {
235 let thoughts = MountSpace::parse("thoughts").unwrap();
237 assert_eq!(thoughts, MountSpace::Thoughts);
238
239 let context = MountSpace::parse("api-docs").unwrap();
241 assert_eq!(context, MountSpace::Context("api-docs".to_string()));
242
243 let reference = MountSpace::parse("references/github/example").unwrap();
245 assert_eq!(
246 reference,
247 MountSpace::Reference {
248 org: "github".to_string(),
249 repo: "example".to_string(),
250 }
251 );
252
253 assert!(MountSpace::parse("references/invalid").is_err());
255 }
256
257 #[test]
258 fn test_mount_space_as_str() {
259 assert_eq!(MountSpace::Thoughts.as_str(), "thoughts");
260 assert_eq!(MountSpace::Context("docs".to_string()).as_str(), "docs");
261 assert_eq!(
262 MountSpace::Reference {
263 org: "org".to_string(),
264 repo: "repo".to_string(),
265 }
266 .as_str(),
267 "references/org/repo"
268 );
269 }
270
271 #[test]
272 fn test_mount_space_round_trip() {
273 let cases = vec![
274 ("thoughts", MountSpace::Thoughts),
275 ("api-docs", MountSpace::Context("api-docs".to_string())),
276 (
277 "references/github/example",
278 MountSpace::Reference {
279 org: "github".to_string(),
280 repo: "example".to_string(),
281 },
282 ),
283 ];
284
285 for (input, expected) in cases {
286 let parsed = MountSpace::parse(input).unwrap();
287 assert_eq!(parsed, expected);
288 assert_eq!(parsed.as_str(), input);
289 }
290 }
291
292 #[test]
293 fn test_mount_space_is_read_only() {
294 assert!(!MountSpace::Thoughts.is_read_only());
295 assert!(!MountSpace::Context("test".to_string()).is_read_only());
296 assert!(
297 MountSpace::Reference {
298 org: "test".to_string(),
299 repo: "repo".to_string(),
300 }
301 .is_read_only()
302 );
303 }
304
305 #[test]
306 fn test_mount_space_parse_context_prefix_normalization() {
307 let ms_prefixed = MountSpace::parse("context/api-docs").unwrap();
308 assert_eq!(ms_prefixed, MountSpace::Context("api-docs".to_string()));
309
310 let ms_plain = MountSpace::parse("api-docs").unwrap();
311 assert_eq!(ms_plain, MountSpace::Context("api-docs".to_string()));
312
313 assert_eq!(ms_prefixed, ms_plain);
315
316 assert!(MountSpace::parse("context/").is_err());
318 }
319}