1use a3s_box_core::error::{BoxError, Result};
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
8pub struct MountPoint {
9 pub host_path: PathBuf,
11
12 pub guest_path: PathBuf,
14
15 pub readonly: bool,
17}
18
19pub struct FsManager {
21 mounts: Vec<MountPoint>,
23}
24
25impl FsManager {
26 pub fn new() -> Self {
28 Self { mounts: Vec::new() }
29 }
30
31 pub fn add_mount(
33 &mut self,
34 host_path: impl AsRef<Path>,
35 guest_path: impl AsRef<Path>,
36 readonly: bool,
37 ) {
38 self.mounts.push(MountPoint {
39 host_path: host_path.as_ref().to_path_buf(),
40 guest_path: guest_path.as_ref().to_path_buf(),
41 readonly,
42 });
43 }
44
45 pub fn setup_default_mounts(
47 &mut self,
48 workspace: impl AsRef<Path>,
49 cache: impl AsRef<Path>,
50 ) -> Result<()> {
51 self.add_mount(workspace, "/workspace", false);
53
54 self.add_mount(cache, "/cache", false);
56
57 Ok(())
58 }
59
60 pub fn mounts(&self) -> &[MountPoint] {
62 &self.mounts
63 }
64
65 pub async fn apply_mounts(&self) -> Result<()> {
70 for mount in &self.mounts {
71 if !mount.host_path.exists() {
72 return Err(BoxError::ConfigError(format!(
73 "Mount source does not exist: {}",
74 mount.host_path.display()
75 )));
76 }
77
78 tracing::info!(
79 host = %mount.host_path.display(),
80 guest = %mount.guest_path.display(),
81 readonly = mount.readonly,
82 "Configured virtio-fs mount"
83 );
84 }
85
86 Ok(())
87 }
88}
89
90impl Default for FsManager {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96pub async fn ensure_cache_dir() -> Result<PathBuf> {
98 let cache_dir = dirs::cache_dir()
99 .ok_or_else(|| BoxError::ConfigError("Cannot determine cache directory".to_string()))?
100 .join("a3s-box");
101
102 tokio::fs::create_dir_all(&cache_dir).await?;
103
104 Ok(cache_dir)
105}
106
107mod dirs {
109 use std::path::PathBuf;
110
111 pub fn cache_dir() -> Option<PathBuf> {
112 #[cfg(target_os = "macos")]
113 {
114 std::env::var_os("HOME").map(|home| PathBuf::from(home).join("Library/Caches"))
115 }
116
117 #[cfg(target_os = "linux")]
118 {
119 std::env::var_os("XDG_CACHE_HOME")
120 .map(PathBuf::from)
121 .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))
122 }
123
124 #[cfg(target_os = "windows")]
125 {
126 std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use tempfile::TempDir;
135
136 #[test]
137 fn test_fs_manager_new_empty() {
138 let mgr = FsManager::new();
139 assert!(mgr.mounts().is_empty());
140 }
141
142 #[test]
143 fn test_fs_manager_default() {
144 let mgr = FsManager::default();
145 assert!(mgr.mounts().is_empty());
146 }
147
148 #[test]
149 fn test_add_mount() {
150 let mut mgr = FsManager::new();
151 mgr.add_mount("/host/path", "/guest/path", false);
152
153 assert_eq!(mgr.mounts().len(), 1);
154 assert_eq!(mgr.mounts()[0].host_path, PathBuf::from("/host/path"));
155 assert_eq!(mgr.mounts()[0].guest_path, PathBuf::from("/guest/path"));
156 assert!(!mgr.mounts()[0].readonly);
157 }
158
159 #[test]
160 fn test_add_mount_readonly() {
161 let mut mgr = FsManager::new();
162 mgr.add_mount("/data", "/mnt/data", true);
163
164 assert!(mgr.mounts()[0].readonly);
165 }
166
167 #[test]
168 fn test_add_multiple_mounts() {
169 let mut mgr = FsManager::new();
170 mgr.add_mount("/a", "/ga", false);
171 mgr.add_mount("/b", "/gb", true);
172 mgr.add_mount("/c", "/gc", false);
173
174 assert_eq!(mgr.mounts().len(), 3);
175 }
176
177 #[test]
178 fn test_setup_default_mounts() {
179 let tmp = TempDir::new().unwrap();
180 let workspace = tmp.path().join("workspace");
181 let cache = tmp.path().join("cache");
182
183 std::fs::create_dir_all(&workspace).unwrap();
184 std::fs::create_dir_all(&cache).unwrap();
185
186 let mut mgr = FsManager::new();
187 mgr.setup_default_mounts(&workspace, &cache).unwrap();
188
189 assert_eq!(mgr.mounts().len(), 2);
191
192 assert!(!mgr.mounts()[0].readonly);
194 assert_eq!(mgr.mounts()[0].guest_path, PathBuf::from("/workspace"));
195
196 assert!(!mgr.mounts()[1].readonly);
198 assert_eq!(mgr.mounts()[1].guest_path, PathBuf::from("/cache"));
199 }
200
201 #[tokio::test]
202 async fn test_apply_mounts_valid() {
203 let tmp = TempDir::new().unwrap();
204 let host_dir = tmp.path().join("data");
205 std::fs::create_dir_all(&host_dir).unwrap();
206
207 let mut mgr = FsManager::new();
208 mgr.add_mount(&host_dir, "/guest/data", false);
209
210 assert!(mgr.apply_mounts().await.is_ok());
211 }
212
213 #[tokio::test]
214 async fn test_apply_mounts_missing_host_path() {
215 let mut mgr = FsManager::new();
216 mgr.add_mount("/nonexistent/path/12345", "/guest", false);
217
218 let result = mgr.apply_mounts().await;
219 assert!(result.is_err());
220 let err = format!("{}", result.unwrap_err());
221 assert!(err.contains("Mount source does not exist"));
222 }
223
224 #[tokio::test]
225 async fn test_ensure_cache_dir() {
226 let dir = ensure_cache_dir().await.unwrap();
227 assert!(dir.exists());
228 assert!(dir.to_string_lossy().contains("a3s-box"));
229 }
230
231 #[test]
232 fn test_mount_point_debug() {
233 let mp = MountPoint {
234 host_path: PathBuf::from("/host"),
235 guest_path: PathBuf::from("/guest"),
236 readonly: true,
237 };
238 let debug = format!("{:?}", mp);
239 assert!(debug.contains("host"));
240 assert!(debug.contains("guest"));
241 assert!(debug.contains("true"));
242 }
243}