1use crate::errors::{Result, SandboxError};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum VolumeType {
11 Bind,
13 Tmpfs,
15 Named,
17 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#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct VolumeMount {
35 pub volume_type: VolumeType,
37 pub source: String,
39 pub destination: PathBuf,
41 pub read_only: bool,
43 pub size_limit: Option<u64>,
45}
46
47impl VolumeMount {
48 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 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 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 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 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 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
141pub struct VolumeManager {
143 mounts: Vec<VolumeMount>,
145 volume_root: PathBuf,
147}
148
149impl VolumeManager {
150 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 pub fn add_mount(&mut self, mount: VolumeMount) -> Result<()> {
160 mount.validate()?;
161 self.mounts.push(mount);
162 Ok(())
163 }
164
165 pub fn mounts(&self) -> &[VolumeMount] {
167 &self.mounts
168 }
169
170 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 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 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 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 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}