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 {
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 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
142pub struct VolumeManager {
144 mounts: Vec<VolumeMount>,
146 volume_root: PathBuf,
148}
149
150impl VolumeManager {
151 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 pub fn add_mount(&mut self, mount: VolumeMount) -> Result<()> {
161 mount.validate()?;
162 self.mounts.push(mount);
163 Ok(())
164 }
165
166 pub fn mounts(&self) -> &[VolumeMount] {
168 &self.mounts
169 }
170
171 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 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 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 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 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}