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
210    pub fn get_volume_size(&self, name: &str) -> Result<u64> {
211        let vol_path = self.volume_root.join(name);
212
213        if !vol_path.exists() {
214            return Err(SandboxError::Syscall(format!(
215                "Volume does not exist: {}",
216                name
217            )));
218        }
219
220        let mut total = 0u64;
221
222        for entry in fs::read_dir(&vol_path).map_err(|e| SandboxError::Syscall(e.to_string()))? {
223            let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?;
224            let path = entry.path();
225
226            if path.is_file() {
227                total += entry
228                    .metadata()
229                    .map_err(|e| SandboxError::Syscall(e.to_string()))?
230                    .len();
231            }
232        }
233
234        Ok(total)
235    }
236
237    /// Clear all mounts
238    pub fn clear_mounts(&mut self) {
239        self.mounts.clear();
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_volume_type_display() {
249        assert_eq!(VolumeType::Bind.to_string(), "bind");
250        assert_eq!(VolumeType::Tmpfs.to_string(), "tmpfs");
251    }
252
253    #[test]
254    fn test_volume_mount_bind() {
255        let mount = VolumeMount::bind("/tmp", "/mnt");
256        assert_eq!(mount.volume_type, VolumeType::Bind);
257        assert_eq!(mount.source, "/tmp");
258        assert_eq!(mount.destination, PathBuf::from("/mnt"));
259        assert!(!mount.read_only);
260    }
261
262    #[test]
263    fn test_volume_mount_readonly() {
264        let mount = VolumeMount::bind_readonly("/tmp", "/mnt");
265        assert_eq!(mount.volume_type, VolumeType::ReadOnly);
266        assert!(mount.read_only);
267    }
268
269    #[test]
270    fn test_volume_mount_tmpfs() {
271        let mount = VolumeMount::tmpfs("/tmp", Some(1024));
272        assert_eq!(mount.volume_type, VolumeType::Tmpfs);
273        assert_eq!(mount.size_limit, Some(1024));
274    }
275
276    #[test]
277    fn test_volume_mount_named() {
278        let mount = VolumeMount::named("mydata", "/data");
279        assert_eq!(mount.volume_type, VolumeType::Named);
280        assert_eq!(mount.source, "mydata");
281    }
282
283    #[test]
284    fn test_volume_mount_options() {
285        let bind_mount = VolumeMount::bind("/tmp", "/mnt");
286        assert_eq!(bind_mount.get_mount_options(), "bind");
287
288        let ro_mount = VolumeMount::bind_readonly("/tmp", "/mnt");
289        assert_eq!(ro_mount.get_mount_options(), "bind,ro");
290
291        let tmpfs_mount = VolumeMount::tmpfs("/tmp", Some(1024));
292        assert!(tmpfs_mount.get_mount_options().contains("size="));
293    }
294
295    #[test]
296    fn test_volume_manager_creation() {
297        let manager = VolumeManager::new("/tmp");
298        assert!(manager.mounts().is_empty());
299    }
300
301    #[test]
302    fn test_volume_manager_add_mount() {
303        let mut manager = VolumeManager::new("/tmp");
304        let mount = VolumeMount::tmpfs("/tmp", None);
305
306        assert!(manager.add_mount(mount).is_ok());
307        assert_eq!(manager.mounts().len(), 1);
308    }
309
310    #[test]
311    fn test_volume_manager_clear_mounts() {
312        let mut manager = VolumeManager::new("/tmp");
313        let mount = VolumeMount::tmpfs("/tmp", None);
314
315        manager.add_mount(mount).ok();
316        assert!(!manager.mounts().is_empty());
317
318        manager.clear_mounts();
319        assert!(manager.mounts().is_empty());
320    }
321}