1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct VolumeConfig {
12 pub name: String,
14
15 #[serde(default = "default_driver")]
17 pub driver: String,
18
19 pub mount_point: String,
21
22 #[serde(default)]
24 pub labels: HashMap<String, String>,
25
26 #[serde(default)]
28 pub in_use_by: Vec<String>,
29
30 #[serde(default)]
32 pub size_limit: u64,
33
34 pub created_at: String,
36}
37
38fn default_driver() -> String {
39 "local".to_string()
40}
41
42impl VolumeConfig {
43 pub fn new(name: &str, mount_point: &str) -> Self {
45 Self {
46 name: name.to_string(),
47 driver: "local".to_string(),
48 mount_point: mount_point.to_string(),
49 labels: HashMap::new(),
50 in_use_by: Vec::new(),
51 size_limit: 0,
52 created_at: chrono::Utc::now().to_rfc3339(),
53 }
54 }
55
56 pub fn with_size_limit(name: &str, mount_point: &str, size_limit: u64) -> Self {
58 let mut vol = Self::new(name, mount_point);
59 vol.size_limit = size_limit;
60 vol
61 }
62
63 pub fn check_quota(&self) -> Result<u64, String> {
68 if self.size_limit == 0 {
69 return Ok(0);
70 }
71 let path = std::path::Path::new(&self.mount_point);
72 if !path.exists() {
73 return Ok(0);
74 }
75 let size = dir_size(path);
76 if size > self.size_limit {
77 Err(format!(
78 "volume '{}' exceeds size limit: {} > {} bytes",
79 self.name, size, self.size_limit
80 ))
81 } else {
82 Ok(size)
83 }
84 }
85
86 pub fn attach(&mut self, box_id: &str) {
88 if !self.in_use_by.contains(&box_id.to_string()) {
89 self.in_use_by.push(box_id.to_string());
90 }
91 }
92
93 pub fn detach(&mut self, box_id: &str) {
95 self.in_use_by.retain(|id| id != box_id);
96 }
97
98 pub fn is_in_use(&self) -> bool {
100 !self.in_use_by.is_empty()
101 }
102}
103
104fn dir_size(path: &std::path::Path) -> u64 {
106 let mut total = 0u64;
107 if let Ok(entries) = std::fs::read_dir(path) {
108 for entry in entries.flatten() {
109 let p = entry.path();
110 if p.is_dir() {
111 total += dir_size(&p);
112 } else if let Ok(meta) = p.metadata() {
113 total += meta.len();
114 }
115 }
116 }
117 total
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_volume_config_new() {
126 let vol = VolumeConfig::new("mydata", "/home/user/.a3s/volumes/mydata");
127 assert_eq!(vol.name, "mydata");
128 assert_eq!(vol.driver, "local");
129 assert!(vol.in_use_by.is_empty());
130 assert!(vol.labels.is_empty());
131 }
132
133 #[test]
134 fn test_volume_attach_detach() {
135 let mut vol = VolumeConfig::new("mydata", "/tmp/vol");
136 vol.attach("box-1");
137 vol.attach("box-2");
138 assert_eq!(vol.in_use_by.len(), 2);
139 assert!(vol.is_in_use());
140
141 vol.detach("box-1");
142 assert_eq!(vol.in_use_by.len(), 1);
143 assert!(vol.is_in_use());
144
145 vol.detach("box-2");
146 assert!(!vol.is_in_use());
147 }
148
149 #[test]
150 fn test_volume_attach_idempotent() {
151 let mut vol = VolumeConfig::new("mydata", "/tmp/vol");
152 vol.attach("box-1");
153 vol.attach("box-1");
154 assert_eq!(vol.in_use_by.len(), 1);
155 }
156
157 #[test]
158 fn test_volume_detach_nonexistent() {
159 let mut vol = VolumeConfig::new("mydata", "/tmp/vol");
160 vol.detach("nonexistent"); assert!(vol.in_use_by.is_empty());
162 }
163
164 #[test]
165 fn test_volume_with_labels() {
166 let mut vol = VolumeConfig::new("mydata", "/tmp/vol");
167 vol.labels.insert("env".to_string(), "prod".to_string());
168 assert_eq!(vol.labels.get("env").unwrap(), "prod");
169 }
170
171 #[test]
172 fn test_volume_serialization() {
173 let mut vol = VolumeConfig::new("mydata", "/tmp/vol");
174 vol.attach("box-1");
175 vol.labels.insert("env".to_string(), "test".to_string());
176
177 let json = serde_json::to_string(&vol).unwrap();
178 let parsed: VolumeConfig = serde_json::from_str(&json).unwrap();
179
180 assert_eq!(parsed.name, "mydata");
181 assert_eq!(parsed.in_use_by, vec!["box-1"]);
182 assert_eq!(parsed.labels.get("env").unwrap(), "test");
183 }
184
185 #[test]
186 fn test_volume_default_driver() {
187 let json = r#"{"name":"test","mount_point":"/tmp","created_at":"2024-01-01T00:00:00Z"}"#;
188 let vol: VolumeConfig = serde_json::from_str(json).unwrap();
189 assert_eq!(vol.driver, "local");
190 }
191
192 #[test]
193 fn test_volume_size_limit_default_zero() {
194 let vol = VolumeConfig::new("test", "/tmp/vol");
195 assert_eq!(vol.size_limit, 0);
196 }
197
198 #[test]
199 fn test_volume_with_size_limit() {
200 let vol = VolumeConfig::with_size_limit("test", "/tmp/vol", 1024 * 1024);
201 assert_eq!(vol.size_limit, 1024 * 1024);
202 assert_eq!(vol.name, "test");
203 }
204
205 #[test]
206 fn test_volume_check_quota_no_limit() {
207 let vol = VolumeConfig::new("test", "/tmp/nonexistent");
208 assert!(vol.check_quota().is_ok());
209 assert_eq!(vol.check_quota().unwrap(), 0);
210 }
211
212 #[test]
213 fn test_volume_check_quota_within_limit() {
214 let dir = std::env::temp_dir().join("a3s_test_vol_quota_ok");
215 let _ = std::fs::remove_dir_all(&dir);
216 std::fs::create_dir_all(&dir).unwrap();
217 std::fs::write(dir.join("data.txt"), "hello").unwrap();
218
219 let vol = VolumeConfig::with_size_limit(
220 "test",
221 dir.to_str().unwrap(),
222 1024 * 1024, );
224 let result = vol.check_quota();
225 assert!(result.is_ok());
226 assert_eq!(result.unwrap(), 5); let _ = std::fs::remove_dir_all(&dir);
229 }
230
231 #[test]
232 fn test_volume_check_quota_exceeded() {
233 let dir = std::env::temp_dir().join("a3s_test_vol_quota_exceed");
234 let _ = std::fs::remove_dir_all(&dir);
235 std::fs::create_dir_all(&dir).unwrap();
236 std::fs::write(dir.join("data.txt"), "hello world!").unwrap();
237
238 let vol = VolumeConfig::with_size_limit(
239 "test",
240 dir.to_str().unwrap(),
241 5, );
243 let result = vol.check_quota();
244 assert!(result.is_err());
245 assert!(result.unwrap_err().contains("exceeds size limit"));
246
247 let _ = std::fs::remove_dir_all(&dir);
248 }
249
250 #[test]
251 fn test_volume_size_limit_serde() {
252 let vol = VolumeConfig::with_size_limit("test", "/tmp/vol", 1024);
253 let json = serde_json::to_string(&vol).unwrap();
254 let parsed: VolumeConfig = serde_json::from_str(&json).unwrap();
255 assert_eq!(parsed.size_limit, 1024);
256 }
257
258 #[test]
259 fn test_dir_size_empty() {
260 let dir = std::env::temp_dir().join("a3s_test_dir_size_empty");
261 let _ = std::fs::remove_dir_all(&dir);
262 std::fs::create_dir_all(&dir).unwrap();
263 assert_eq!(dir_size(&dir), 0);
264 let _ = std::fs::remove_dir_all(&dir);
265 }
266
267 #[test]
268 fn test_dir_size_with_files() {
269 let dir = std::env::temp_dir().join("a3s_test_dir_size_files");
270 let _ = std::fs::remove_dir_all(&dir);
271 std::fs::create_dir_all(&dir).unwrap();
272 std::fs::write(dir.join("a.txt"), "aaa").unwrap();
273 std::fs::write(dir.join("b.txt"), "bb").unwrap();
274 assert_eq!(dir_size(&dir), 5);
275 let _ = std::fs::remove_dir_all(&dir);
276 }
277
278 #[test]
279 fn test_dir_size_recursive() {
280 let dir = std::env::temp_dir().join("a3s_test_dir_size_recursive");
281 let _ = std::fs::remove_dir_all(&dir);
282 std::fs::create_dir_all(dir.join("sub")).unwrap();
283 std::fs::write(dir.join("a.txt"), "aaa").unwrap();
284 std::fs::write(dir.join("sub").join("b.txt"), "bb").unwrap();
285 assert_eq!(dir_size(&dir), 5);
286 let _ = std::fs::remove_dir_all(&dir);
287 }
288}