Skip to main content

sandbox_fs/
volumes.rs

1//! Volume management for persistent storage in sandbox
2
3use sandbox_core::{Result, SandboxError};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Volume mount type
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VolumeType {
10    Bind,
11    Tmpfs,
12    Named,
13    ReadOnly,
14}
15
16impl std::fmt::Display for VolumeType {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            VolumeType::Bind => write!(f, "bind"),
20            VolumeType::Tmpfs => write!(f, "tmpfs"),
21            VolumeType::Named => write!(f, "named"),
22            VolumeType::ReadOnly => write!(f, "readonly"),
23        }
24    }
25}
26
27/// Volume mount configuration
28#[derive(Debug, Clone)]
29pub struct VolumeMount {
30    pub volume_type: VolumeType,
31    pub source: String,
32    pub destination: PathBuf,
33    pub read_only: bool,
34    pub size_limit: Option<u64>,
35}
36
37impl VolumeMount {
38    pub fn bind(source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Self {
39        Self {
40            volume_type: VolumeType::Bind,
41            source: source.as_ref().display().to_string(),
42            destination: destination.as_ref().to_path_buf(),
43            read_only: false,
44            size_limit: None,
45        }
46    }
47
48    pub fn bind_readonly(source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Self {
49        Self {
50            volume_type: VolumeType::ReadOnly,
51            source: source.as_ref().display().to_string(),
52            destination: destination.as_ref().to_path_buf(),
53            read_only: true,
54            size_limit: None,
55        }
56    }
57
58    pub fn tmpfs(destination: impl AsRef<Path>, size_limit: Option<u64>) -> Self {
59        Self {
60            volume_type: VolumeType::Tmpfs,
61            source: "tmpfs".to_string(),
62            destination: destination.as_ref().to_path_buf(),
63            read_only: false,
64            size_limit,
65        }
66    }
67
68    pub fn named(name: &str, destination: impl AsRef<Path>) -> Self {
69        Self {
70            volume_type: VolumeType::Named,
71            source: name.to_string(),
72            destination: destination.as_ref().to_path_buf(),
73            read_only: false,
74            size_limit: None,
75        }
76    }
77
78    pub fn validate(&self) -> Result<()> {
79        if self.source.is_empty() {
80            return Err(SandboxError::InvalidConfig(
81                "Volume source cannot be empty".to_string(),
82            ));
83        }
84        if self.destination.as_os_str().is_empty() {
85            return Err(SandboxError::InvalidConfig(
86                "Volume destination cannot be empty".to_string(),
87            ));
88        }
89        if self.volume_type == VolumeType::Bind || self.volume_type == VolumeType::ReadOnly {
90            let source_path = Path::new(&self.source);
91            if !source_path.exists() {
92                return Err(SandboxError::InvalidConfig(format!(
93                    "Bind mount source does not exist: {}",
94                    self.source
95                )));
96            }
97        }
98        Ok(())
99    }
100
101    pub fn get_mount_options(&self) -> String {
102        match self.volume_type {
103            VolumeType::Bind | VolumeType::ReadOnly => {
104                if self.read_only {
105                    "bind,ro".to_string()
106                } else {
107                    "bind".to_string()
108                }
109            }
110            VolumeType::Tmpfs => {
111                if let Some(size) = self.size_limit {
112                    format!("size={}", size)
113                } else {
114                    String::new()
115                }
116            }
117            VolumeType::Named => "named".to_string(),
118        }
119    }
120}
121
122/// Volume manager
123pub struct VolumeManager {
124    mounts: Vec<VolumeMount>,
125    volume_root: PathBuf,
126}
127
128impl VolumeManager {
129    pub fn new(volume_root: impl AsRef<Path>) -> Self {
130        Self {
131            mounts: Vec::new(),
132            volume_root: volume_root.as_ref().to_path_buf(),
133        }
134    }
135
136    pub fn add_mount(&mut self, mount: VolumeMount) -> Result<()> {
137        mount.validate()?;
138        self.mounts.push(mount);
139        Ok(())
140    }
141
142    pub fn mounts(&self) -> &[VolumeMount] {
143        &self.mounts
144    }
145
146    pub fn create_volume(&self, name: &str) -> Result<PathBuf> {
147        let vol_path = self.volume_root.join(name);
148        fs::create_dir_all(&vol_path).map_err(|e| {
149            SandboxError::Syscall(format!("Failed to create volume {}: {}", name, e))
150        })?;
151        Ok(vol_path)
152    }
153
154    pub fn delete_volume(&self, name: &str) -> Result<()> {
155        let vol_path = self.volume_root.join(name);
156        if vol_path.exists() {
157            fs::remove_dir_all(&vol_path).map_err(|e| {
158                SandboxError::Syscall(format!("Failed to delete volume {}: {}", name, e))
159            })?;
160        }
161        Ok(())
162    }
163
164    pub fn list_volumes(&self) -> Result<Vec<String>> {
165        let mut volumes = Vec::new();
166        if self.volume_root.exists() {
167            for entry in fs::read_dir(&self.volume_root)
168                .map_err(|e| SandboxError::Syscall(format!("Cannot list volumes: {}", e)))?
169            {
170                let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?;
171                if let Ok(name) = entry.file_name().into_string() {
172                    volumes.push(name);
173                }
174            }
175        }
176        Ok(volumes)
177    }
178
179    pub fn get_volume_size(&self, name: &str) -> Result<u64> {
180        use walkdir::WalkDir;
181        let vol_path = self.volume_root.join(name);
182        if !vol_path.exists() {
183            return Err(SandboxError::Syscall(format!(
184                "Volume does not exist: {}",
185                name
186            )));
187        }
188        let mut total = 0u64;
189        for entry in WalkDir::new(&vol_path).into_iter().filter_map(|e| e.ok()) {
190            if entry.file_type().is_file() {
191                total += entry
192                    .metadata()
193                    .map_err(|e| SandboxError::Syscall(e.to_string()))?
194                    .len();
195            }
196        }
197        Ok(total)
198    }
199
200    pub fn clear_mounts(&mut self) {
201        self.mounts.clear();
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_volume_type_display() {
211        assert_eq!(VolumeType::Bind.to_string(), "bind");
212        assert_eq!(VolumeType::Tmpfs.to_string(), "tmpfs");
213    }
214
215    #[test]
216    fn test_volume_mount_bind() {
217        let mount = VolumeMount::bind("/tmp", "/mnt");
218        assert_eq!(mount.volume_type, VolumeType::Bind);
219        assert!(!mount.read_only);
220    }
221
222    #[test]
223    fn test_volume_mount_options() {
224        let bind_mount = VolumeMount::bind("/tmp", "/mnt");
225        assert_eq!(bind_mount.get_mount_options(), "bind");
226        let ro_mount = VolumeMount::bind_readonly("/tmp", "/mnt");
227        assert_eq!(ro_mount.get_mount_options(), "bind,ro");
228    }
229
230    #[test]
231    fn test_volume_manager_creation() {
232        let manager = VolumeManager::new("/tmp");
233        assert!(manager.mounts().is_empty());
234    }
235}