Skip to main content

a3s_box_runtime/
fs.rs

1//! Filesystem mount management for virtio-fs
2
3use a3s_box_core::error::{BoxError, Result};
4use std::path::{Path, PathBuf};
5
6/// Mount point configuration
7#[derive(Debug, Clone)]
8pub struct MountPoint {
9    /// Host path
10    pub host_path: PathBuf,
11
12    /// Guest path
13    pub guest_path: PathBuf,
14
15    /// Read-only
16    pub readonly: bool,
17}
18
19/// Filesystem manager
20pub struct FsManager {
21    /// Mount points
22    mounts: Vec<MountPoint>,
23}
24
25impl FsManager {
26    /// Create a new filesystem manager
27    pub fn new() -> Self {
28        Self { mounts: Vec::new() }
29    }
30
31    /// Add a mount point
32    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    /// Setup default mounts for A3S Box
46    pub fn setup_default_mounts(
47        &mut self,
48        workspace: impl AsRef<Path>,
49        cache: impl AsRef<Path>,
50    ) -> Result<()> {
51        // Workspace mount (read-write)
52        self.add_mount(workspace, "/workspace", false);
53
54        // Cache mount (read-write, persistent)
55        self.add_mount(cache, "/cache", false);
56
57        Ok(())
58    }
59
60    /// Get all mount points
61    pub fn mounts(&self) -> &[MountPoint] {
62        &self.mounts
63    }
64
65    /// Validate and log all configured mounts.
66    ///
67    /// The actual virtio-fs device setup is handled by the shim via `InstanceSpec`.
68    /// This method validates that mount source paths exist on the host.
69    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
96/// Ensure cache directory exists
97pub 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
107// External dependency for cache directory
108mod 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        // workspace + cache = 2
190        assert_eq!(mgr.mounts().len(), 2);
191
192        // Workspace is read-write
193        assert!(!mgr.mounts()[0].readonly);
194        assert_eq!(mgr.mounts()[0].guest_path, PathBuf::from("/workspace"));
195
196        // Cache is read-write
197        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}