sandbox_rs/storage/
volumes.rs

1//! Volume management for persistent storage in sandbox
2
3use crate::errors::{Result, SandboxError};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::{Path, PathBuf};
7
8/// Volume mount type
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum VolumeType {
11    /// Bind mount (host directory)
12    Bind,
13    /// Tmpfs mount
14    Tmpfs,
15    /// Named volume
16    Named,
17    /// Read-only mount
18    ReadOnly,
19}
20
21impl std::fmt::Display for VolumeType {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            VolumeType::Bind => write!(f, "bind"),
25            VolumeType::Tmpfs => write!(f, "tmpfs"),
26            VolumeType::Named => write!(f, "named"),
27            VolumeType::ReadOnly => write!(f, "readonly"),
28        }
29    }
30}
31
32/// Volume mount configuration
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct VolumeMount {
35    /// Volume type
36    pub volume_type: VolumeType,
37    /// Source (host path or volume name)
38    pub source: String,
39    /// Destination (container path)
40    pub destination: PathBuf,
41    /// Read-only flag
42    pub read_only: bool,
43    /// Optional size limit (for tmpfs)
44    pub size_limit: Option<u64>,
45}
46
47impl VolumeMount {
48    /// Create bind mount
49    pub fn bind(source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Self {
50        Self {
51            volume_type: VolumeType::Bind,
52            source: source.as_ref().display().to_string(),
53            destination: destination.as_ref().to_path_buf(),
54            read_only: false,
55            size_limit: None,
56        }
57    }
58
59    /// Create read-only bind mount
60    pub fn bind_readonly(source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Self {
61        Self {
62            volume_type: VolumeType::ReadOnly,
63            source: source.as_ref().display().to_string(),
64            destination: destination.as_ref().to_path_buf(),
65            read_only: true,
66            size_limit: None,
67        }
68    }
69
70    /// Create tmpfs mount
71    pub fn tmpfs(destination: impl AsRef<Path>, size_limit: Option<u64>) -> Self {
72        Self {
73            volume_type: VolumeType::Tmpfs,
74            source: "tmpfs".to_string(),
75            destination: destination.as_ref().to_path_buf(),
76            read_only: false,
77            size_limit,
78        }
79    }
80
81    /// Create named volume
82    pub fn named(name: &str, destination: impl AsRef<Path>) -> Self {
83        Self {
84            volume_type: VolumeType::Named,
85            source: name.to_string(),
86            destination: destination.as_ref().to_path_buf(),
87            read_only: false,
88            size_limit: None,
89        }
90    }
91
92    /// Validate volume mount
93    pub fn validate(&self) -> Result<()> {
94        if self.source.is_empty() {
95            return Err(SandboxError::InvalidConfig(
96                "Volume source cannot be empty".to_string(),
97            ));
98        }
99
100        if self.destination.as_os_str().is_empty() {
101            return Err(SandboxError::InvalidConfig(
102                "Volume destination cannot be empty".to_string(),
103            ));
104        }
105
106        if self.volume_type == VolumeType::Bind || self.volume_type == VolumeType::ReadOnly {
107            let source_path = Path::new(&self.source);
108            if !source_path.exists() {
109                return Err(SandboxError::InvalidConfig(format!(
110                    "Bind mount source does not exist: {}",
111                    self.source
112                )));
113            }
114        }
115
116        Ok(())
117    }
118
119    /// Get mount options for mount command
120    pub fn get_mount_options(&self) -> String {
121        match self.volume_type {
122            VolumeType::Bind | VolumeType::ReadOnly => {
123                if self.read_only {
124                    "bind,ro".to_string()
125                } else {
126                    "bind".to_string()
127                }
128            }
129            VolumeType::Tmpfs => {
130                if let Some(size) = self.size_limit {
131                    format!("size={}", size)
132                } else {
133                    String::new()
134                }
135            }
136            VolumeType::Named => "named".to_string(),
137        }
138    }
139}
140
141/// Volume manager
142pub struct VolumeManager {
143    /// Mount points
144    mounts: Vec<VolumeMount>,
145    /// Volume storage directory
146    volume_root: PathBuf,
147}
148
149impl VolumeManager {
150    /// Create new volume manager
151    pub fn new(volume_root: impl AsRef<Path>) -> Self {
152        Self {
153            mounts: Vec::new(),
154            volume_root: volume_root.as_ref().to_path_buf(),
155        }
156    }
157
158    /// Add volume mount
159    pub fn add_mount(&mut self, mount: VolumeMount) -> Result<()> {
160        mount.validate()?;
161        self.mounts.push(mount);
162        Ok(())
163    }
164
165    /// Get all mounts
166    pub fn mounts(&self) -> &[VolumeMount] {
167        &self.mounts
168    }
169
170    /// Create named volume
171    pub fn create_volume(&self, name: &str) -> Result<PathBuf> {
172        let vol_path = self.volume_root.join(name);
173        fs::create_dir_all(&vol_path).map_err(|e| {
174            SandboxError::Syscall(format!("Failed to create volume {}: {}", name, e))
175        })?;
176        Ok(vol_path)
177    }
178
179    /// Delete named volume
180    pub fn delete_volume(&self, name: &str) -> Result<()> {
181        let vol_path = self.volume_root.join(name);
182        if vol_path.exists() {
183            fs::remove_dir_all(&vol_path).map_err(|e| {
184                SandboxError::Syscall(format!("Failed to delete volume {}: {}", name, e))
185            })?;
186        }
187        Ok(())
188    }
189
190    /// List named volumes
191    pub fn list_volumes(&self) -> Result<Vec<String>> {
192        let mut volumes = Vec::new();
193
194        if self.volume_root.exists() {
195            for entry in fs::read_dir(&self.volume_root)
196                .map_err(|e| SandboxError::Syscall(format!("Cannot list volumes: {}", e)))?
197            {
198                let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?;
199
200                if let Ok(name) = entry.file_name().into_string() {
201                    volumes.push(name);
202                }
203            }
204        }
205
206        Ok(volumes)
207    }
208
209    /// Get volume size (recursive)
210    pub fn get_volume_size(&self, name: &str) -> Result<u64> {
211        use walkdir::WalkDir;
212
213        let vol_path = self.volume_root.join(name);
214
215        if !vol_path.exists() {
216            return Err(SandboxError::Syscall(format!(
217                "Volume does not exist: {}",
218                name
219            )));
220        }
221
222        let mut total = 0u64;
223
224        for entry in WalkDir::new(&vol_path).into_iter().filter_map(|e| e.ok()) {
225            if entry.file_type().is_file() {
226                total += entry
227                    .metadata()
228                    .map_err(|e| SandboxError::Syscall(e.to_string()))?
229                    .len();
230            }
231        }
232
233        Ok(total)
234    }
235
236    /// Clear all mounts
237    pub fn clear_mounts(&mut self) {
238        self.mounts.clear();
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_volume_type_display() {
248        assert_eq!(VolumeType::Bind.to_string(), "bind");
249        assert_eq!(VolumeType::Tmpfs.to_string(), "tmpfs");
250    }
251
252    #[test]
253    fn test_volume_mount_bind() {
254        let mount = VolumeMount::bind("/tmp", "/mnt");
255        assert_eq!(mount.volume_type, VolumeType::Bind);
256        assert_eq!(mount.source, "/tmp");
257        assert_eq!(mount.destination, PathBuf::from("/mnt"));
258        assert!(!mount.read_only);
259    }
260
261    #[test]
262    fn test_volume_mount_readonly() {
263        let mount = VolumeMount::bind_readonly("/tmp", "/mnt");
264        assert_eq!(mount.volume_type, VolumeType::ReadOnly);
265        assert!(mount.read_only);
266    }
267
268    #[test]
269    fn test_volume_mount_tmpfs() {
270        let mount = VolumeMount::tmpfs("/tmp", Some(1024));
271        assert_eq!(mount.volume_type, VolumeType::Tmpfs);
272        assert_eq!(mount.size_limit, Some(1024));
273    }
274
275    #[test]
276    fn test_volume_mount_named() {
277        let mount = VolumeMount::named("mydata", "/data");
278        assert_eq!(mount.volume_type, VolumeType::Named);
279        assert_eq!(mount.source, "mydata");
280    }
281
282    #[test]
283    fn test_volume_mount_options() {
284        let bind_mount = VolumeMount::bind("/tmp", "/mnt");
285        assert_eq!(bind_mount.get_mount_options(), "bind");
286
287        let ro_mount = VolumeMount::bind_readonly("/tmp", "/mnt");
288        assert_eq!(ro_mount.get_mount_options(), "bind,ro");
289
290        let tmpfs_mount = VolumeMount::tmpfs("/tmp", Some(1024));
291        assert!(tmpfs_mount.get_mount_options().contains("size="));
292    }
293
294    #[test]
295    fn test_volume_manager_creation() {
296        let manager = VolumeManager::new("/tmp");
297        assert!(manager.mounts().is_empty());
298    }
299
300    #[test]
301    fn test_volume_manager_add_mount() {
302        let mut manager = VolumeManager::new("/tmp");
303        let mount = VolumeMount::tmpfs("/tmp", None);
304
305        assert!(manager.add_mount(mount).is_ok());
306        assert_eq!(manager.mounts().len(), 1);
307    }
308
309    #[test]
310    fn test_volume_manager_clear_mounts() {
311        let mut manager = VolumeManager::new("/tmp");
312        let mount = VolumeMount::tmpfs("/tmp", None);
313
314        manager.add_mount(mount).ok();
315        assert!(!manager.mounts().is_empty());
316
317        manager.clear_mounts();
318        assert!(manager.mounts().is_empty());
319    }
320}