1use sandbox_core::{Result, SandboxError};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[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#[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
122pub 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}