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        // Validate bind mounts exist
107        if self.volume_type == VolumeType::Bind || self.volume_type == VolumeType::ReadOnly {
108            let source_path = Path::new(&self.source);
109            if !source_path.exists() {
110                return Err(SandboxError::InvalidConfig(format!(
111                    "Bind mount source does not exist: {}",
112                    self.source
113                )));
114            }
115        }
116
117        Ok(())
118    }
119
120    /// Get mount options for mount command
121    pub fn get_mount_options(&self) -> String {
122        match self.volume_type {
123            VolumeType::Bind | VolumeType::ReadOnly => {
124                if self.read_only {
125                    "bind,ro".to_string()
126                } else {
127                    "bind".to_string()
128                }
129            }
130            VolumeType::Tmpfs => {
131                if let Some(size) = self.size_limit {
132                    format!("size={}", size)
133                } else {
134                    String::new()
135                }
136            }
137            VolumeType::Named => "named".to_string(),
138        }
139    }
140}
141
142/// Volume manager
143pub struct VolumeManager {
144    /// Mount points
145    mounts: Vec<VolumeMount>,
146    /// Volume storage directory
147    volume_root: PathBuf,
148}
149
150impl VolumeManager {
151    /// Create new volume manager
152    pub fn new(volume_root: impl AsRef<Path>) -> Self {
153        Self {
154            mounts: Vec::new(),
155            volume_root: volume_root.as_ref().to_path_buf(),
156        }
157    }
158
159    /// Add volume mount
160    pub fn add_mount(&mut self, mount: VolumeMount) -> Result<()> {
161        mount.validate()?;
162        self.mounts.push(mount);
163        Ok(())
164    }
165
166    /// Get all mounts
167    pub fn mounts(&self) -> &[VolumeMount] {
168        &self.mounts
169    }
170
171    /// Create named volume
172    pub fn create_volume(&self, name: &str) -> Result<PathBuf> {
173        let vol_path = self.volume_root.join(name);
174        fs::create_dir_all(&vol_path).map_err(|e| {
175            SandboxError::Syscall(format!("Failed to create volume {}: {}", name, e))
176        })?;
177        Ok(vol_path)
178    }
179
180    /// Delete named volume
181    pub fn delete_volume(&self, name: &str) -> Result<()> {
182        let vol_path = self.volume_root.join(name);
183        if vol_path.exists() {
184            fs::remove_dir_all(&vol_path).map_err(|e| {
185                SandboxError::Syscall(format!("Failed to delete volume {}: {}", name, e))
186            })?;
187        }
188        Ok(())
189    }
190
191    /// List named volumes
192    pub fn list_volumes(&self) -> Result<Vec<String>> {
193        let mut volumes = Vec::new();
194
195        if self.volume_root.exists() {
196            for entry in fs::read_dir(&self.volume_root)
197                .map_err(|e| SandboxError::Syscall(format!("Cannot list volumes: {}", e)))?
198            {
199                let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?;
200
201                if let Ok(name) = entry.file_name().into_string() {
202                    volumes.push(name);
203                }
204            }
205        }
206
207        Ok(volumes)
208    }
209
210    /// Get volume size
211    pub fn get_volume_size(&self, name: &str) -> Result<u64> {
212        let vol_path = self.volume_root.join(name);
213
214        if !vol_path.exists() {
215            return Err(SandboxError::Syscall(format!(
216                "Volume does not exist: {}",
217                name
218            )));
219        }
220
221        let mut total = 0u64;
222
223        for entry in fs::read_dir(&vol_path).map_err(|e| SandboxError::Syscall(e.to_string()))? {
224            let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?;
225            let path = entry.path();
226
227            if path.is_file() {
228                total += entry
229                    .metadata()
230                    .map_err(|e| SandboxError::Syscall(e.to_string()))?
231                    .len();
232            }
233        }
234
235        Ok(total)
236    }
237
238    /// Clear all mounts
239    pub fn clear_mounts(&mut self) {
240        self.mounts.clear();
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_volume_type_display() {
250        assert_eq!(VolumeType::Bind.to_string(), "bind");
251        assert_eq!(VolumeType::Tmpfs.to_string(), "tmpfs");
252    }
253
254    #[test]
255    fn test_volume_mount_bind() {
256        let mount = VolumeMount::bind("/tmp", "/mnt");
257        assert_eq!(mount.volume_type, VolumeType::Bind);
258        assert_eq!(mount.source, "/tmp");
259        assert_eq!(mount.destination, PathBuf::from("/mnt"));
260        assert!(!mount.read_only);
261    }
262
263    #[test]
264    fn test_volume_mount_readonly() {
265        let mount = VolumeMount::bind_readonly("/tmp", "/mnt");
266        assert_eq!(mount.volume_type, VolumeType::ReadOnly);
267        assert!(mount.read_only);
268    }
269
270    #[test]
271    fn test_volume_mount_tmpfs() {
272        let mount = VolumeMount::tmpfs("/tmp", Some(1024));
273        assert_eq!(mount.volume_type, VolumeType::Tmpfs);
274        assert_eq!(mount.size_limit, Some(1024));
275    }
276
277    #[test]
278    fn test_volume_mount_named() {
279        let mount = VolumeMount::named("mydata", "/data");
280        assert_eq!(mount.volume_type, VolumeType::Named);
281        assert_eq!(mount.source, "mydata");
282    }
283
284    #[test]
285    fn test_volume_mount_options() {
286        let bind_mount = VolumeMount::bind("/tmp", "/mnt");
287        assert_eq!(bind_mount.get_mount_options(), "bind");
288
289        let ro_mount = VolumeMount::bind_readonly("/tmp", "/mnt");
290        assert_eq!(ro_mount.get_mount_options(), "bind,ro");
291
292        let tmpfs_mount = VolumeMount::tmpfs("/tmp", Some(1024));
293        assert!(tmpfs_mount.get_mount_options().contains("size="));
294    }
295
296    #[test]
297    fn test_volume_manager_creation() {
298        let manager = VolumeManager::new("/tmp");
299        assert!(manager.mounts().is_empty());
300    }
301
302    #[test]
303    fn test_volume_manager_add_mount() {
304        let mut manager = VolumeManager::new("/tmp");
305        let mount = VolumeMount::tmpfs("/tmp", None);
306
307        assert!(manager.add_mount(mount).is_ok());
308        assert_eq!(manager.mounts().len(), 1);
309    }
310
311    #[test]
312    fn test_volume_manager_clear_mounts() {
313        let mut manager = VolumeManager::new("/tmp");
314        let mount = VolumeMount::tmpfs("/tmp", None);
315
316        manager.add_mount(mount).ok();
317        assert!(!manager.mounts().is_empty());
318
319        manager.clear_mounts();
320        assert!(manager.mounts().is_empty());
321    }
322}