1use a3s_box_core::error::{BoxError, Result};
7use a3s_box_core::network::NetworkConfig;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug)]
13pub struct NetworkStore {
14 path: PathBuf,
16}
17
18#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
20struct NetworksFile {
21 networks: HashMap<String, NetworkConfig>,
22}
23
24impl NetworkStore {
25 pub fn new(path: impl Into<PathBuf>) -> Self {
27 Self { path: path.into() }
28 }
29
30 pub fn default_path() -> Result<Self> {
32 let home = a3s_box_core::dirs_home();
33 Ok(Self::new(home.join("networks.json")))
34 }
35
36 pub fn load(&self) -> Result<HashMap<String, NetworkConfig>> {
38 if !self.path.exists() {
39 return Ok(HashMap::new());
40 }
41
42 let data = std::fs::read_to_string(&self.path).map_err(|e| {
43 BoxError::NetworkError(format!(
44 "failed to read networks file {}: {}",
45 self.path.display(),
46 e
47 ))
48 })?;
49
50 let file: NetworksFile = serde_json::from_str(&data)
51 .map_err(|e| BoxError::NetworkError(format!("failed to parse networks file: {}", e)))?;
52
53 Ok(file.networks)
54 }
55
56 pub fn save(&self, networks: &HashMap<String, NetworkConfig>) -> Result<()> {
58 if let Some(parent) = self.path.parent() {
60 std::fs::create_dir_all(parent).map_err(|e| {
61 BoxError::NetworkError(format!(
62 "failed to create directory {}: {}",
63 parent.display(),
64 e
65 ))
66 })?;
67 }
68
69 let file = NetworksFile {
70 networks: networks.clone(),
71 };
72
73 let json = serde_json::to_string_pretty(&file)
74 .map_err(|e| BoxError::NetworkError(format!("failed to serialize networks: {}", e)))?;
75
76 let tmp_path = self.path.with_extension("json.tmp");
78 std::fs::write(&tmp_path, &json).map_err(|e| {
79 BoxError::NetworkError(format!(
80 "failed to write tmp file {}: {}",
81 tmp_path.display(),
82 e
83 ))
84 })?;
85
86 std::fs::rename(&tmp_path, &self.path).map_err(|e| {
87 BoxError::NetworkError(format!(
88 "failed to rename {} → {}: {}",
89 tmp_path.display(),
90 self.path.display(),
91 e
92 ))
93 })?;
94
95 Ok(())
96 }
97
98 pub fn get(&self, name: &str) -> Result<Option<NetworkConfig>> {
100 let networks = self.load()?;
101 Ok(networks.get(name).cloned())
102 }
103
104 pub fn with_write_lock<F, R, E>(&self, f: F) -> std::result::Result<R, E>
113 where
114 F: FnOnce(&mut HashMap<String, NetworkConfig>) -> std::result::Result<R, E>,
115 E: From<BoxError>,
116 {
117 let _lock = crate::file_lock::FileLock::acquire(&self.path).map_err(|e| {
118 E::from(BoxError::NetworkError(format!(
119 "failed to lock networks file {}: {e}",
120 self.path.display()
121 )))
122 })?;
123 let mut networks = self.load().map_err(E::from)?;
124 let r = f(&mut networks)?;
125 self.save(&networks).map_err(E::from)?;
126 Ok(r)
127 }
128
129 pub fn create(&self, config: NetworkConfig) -> Result<()> {
131 self.with_write_lock(|networks| {
132 if networks.contains_key(&config.name) {
133 return Err(BoxError::NetworkError(format!(
134 "network '{}' already exists",
135 config.name
136 )));
137 }
138 networks.insert(config.name.clone(), config);
139 Ok(())
140 })
141 }
142
143 pub fn remove(&self, name: &str) -> Result<NetworkConfig> {
145 self.with_write_lock(|networks| {
146 let config = networks
147 .remove(name)
148 .ok_or_else(|| BoxError::NetworkError(format!("network '{}' not found", name)))?;
149
150 if !config.endpoints.is_empty() {
151 return Err(BoxError::NetworkError(format!(
154 "network '{}' has {} connected endpoint(s); disconnect them first or use --force",
155 name,
156 config.endpoints.len()
157 )));
158 }
159 Ok(config)
160 })
161 }
162
163 pub fn list(&self) -> Result<Vec<NetworkConfig>> {
165 let networks = self.load()?;
166 Ok(networks.into_values().collect())
167 }
168
169 pub fn update(&self, config: &NetworkConfig) -> Result<()> {
176 self.with_write_lock(|networks| {
177 if !networks.contains_key(&config.name) {
178 return Err(BoxError::NetworkError(format!(
179 "network '{}' not found",
180 config.name
181 )));
182 }
183 networks.insert(config.name.clone(), config.clone());
184 Ok(())
185 })
186 }
187
188 pub fn path(&self) -> &Path {
190 &self.path
191 }
192}
193
194impl a3s_box_core::traits::NetworkStoreBackend for NetworkStore {
195 fn get(&self, name: &str) -> Result<Option<NetworkConfig>> {
196 self.get(name)
197 }
198
199 fn create(&self, config: NetworkConfig) -> Result<()> {
200 self.create(config)
201 }
202
203 fn remove(&self, name: &str) -> Result<NetworkConfig> {
204 self.remove(name)
205 }
206
207 fn list(&self) -> Result<Vec<NetworkConfig>> {
208 self.list()
209 }
210
211 fn update(&self, config: &NetworkConfig) -> Result<()> {
212 self.update(config)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn temp_store() -> (tempfile::TempDir, NetworkStore) {
221 let dir = tempfile::tempdir().unwrap();
222 let store = NetworkStore::new(dir.path().join("networks.json"));
223 (dir, store)
224 }
225
226 #[test]
227 fn test_load_empty() {
228 let (_dir, store) = temp_store();
229 let networks = store.load().unwrap();
230 assert!(networks.is_empty());
231 }
232
233 #[test]
234 fn test_create_and_load() {
235 let (_dir, store) = temp_store();
236 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
237 store.create(net).unwrap();
238
239 let networks = store.load().unwrap();
240 assert_eq!(networks.len(), 1);
241 assert!(networks.contains_key("mynet"));
242 }
243
244 #[test]
245 fn test_create_duplicate() {
246 let (_dir, store) = temp_store();
247 let net1 = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
248 let net2 = NetworkConfig::new("mynet", "10.89.0.0/24").unwrap();
249
250 store.create(net1).unwrap();
251 assert!(store.create(net2).is_err());
252 }
253
254 #[test]
255 fn test_get_existing() {
256 let (_dir, store) = temp_store();
257 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
258 store.create(net).unwrap();
259
260 let found = store.get("mynet").unwrap();
261 assert!(found.is_some());
262 assert_eq!(found.unwrap().name, "mynet");
263 }
264
265 #[test]
266 fn test_get_nonexistent() {
267 let (_dir, store) = temp_store();
268 let found = store.get("nope").unwrap();
269 assert!(found.is_none());
270 }
271
272 #[test]
273 fn test_remove() {
274 let (_dir, store) = temp_store();
275 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
276 store.create(net).unwrap();
277
278 let removed = store.remove("mynet").unwrap();
279 assert_eq!(removed.name, "mynet");
280
281 let networks = store.load().unwrap();
282 assert!(networks.is_empty());
283 }
284
285 #[test]
286 fn test_remove_nonexistent() {
287 let (_dir, store) = temp_store();
288 assert!(store.remove("nope").is_err());
289 }
290
291 #[test]
292 fn test_remove_with_endpoints() {
293 let (_dir, store) = temp_store();
294 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
295 net.connect("box-1", "web").unwrap();
296 store.create(net).unwrap();
297
298 assert!(store.remove("mynet").is_err());
300 }
301
302 #[test]
303 fn test_list() {
304 let (_dir, store) = temp_store();
305 store
306 .create(NetworkConfig::new("net1", "10.88.0.0/24").unwrap())
307 .unwrap();
308 store
309 .create(NetworkConfig::new("net2", "10.89.0.0/24").unwrap())
310 .unwrap();
311
312 let list = store.list().unwrap();
313 assert_eq!(list.len(), 2);
314 }
315
316 #[test]
317 fn test_update() {
318 let (_dir, store) = temp_store();
319 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
320 store.create(net).unwrap();
321
322 let mut net = store.get("mynet").unwrap().unwrap();
324 net.connect("box-1", "web").unwrap();
325 store.update(&net).unwrap();
326
327 let loaded = store.get("mynet").unwrap().unwrap();
329 assert_eq!(loaded.endpoints.len(), 1);
330 }
331
332 #[test]
333 fn test_update_nonexistent() {
334 let (_dir, store) = temp_store();
335 let net = NetworkConfig::new("nope", "10.88.0.0/24").unwrap();
336 assert!(store.update(&net).is_err());
337 }
338
339 #[test]
340 fn test_atomic_write() {
341 let (_dir, store) = temp_store();
342 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
343 store.create(net).unwrap();
344
345 let data = std::fs::read_to_string(store.path()).unwrap();
347 let _: serde_json::Value = serde_json::from_str(&data).unwrap();
348
349 let tmp = store.path().with_extension("json.tmp");
351 assert!(!tmp.exists());
352 }
353
354 #[test]
355 fn test_creates_parent_directory() {
356 let dir = tempfile::tempdir().unwrap();
357 let store = NetworkStore::new(dir.path().join("subdir").join("networks.json"));
358
359 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
360 store.create(net).unwrap();
361
362 assert!(store.path().exists());
363 }
364
365 #[test]
366 fn concurrent_connects_allocate_distinct_ips() {
367 use std::collections::HashSet;
368 use std::sync::Arc;
369
370 let dir = tempfile::tempdir().unwrap();
371 let store = Arc::new(NetworkStore::new(dir.path().join("networks.json")));
372 store
373 .create(NetworkConfig::new("dev", "10.88.0.0/24").unwrap())
374 .unwrap();
375
376 let handles: Vec<_> = (0..16)
380 .map(|i| {
381 let store = Arc::clone(&store);
382 std::thread::spawn(move || {
383 store
384 .with_write_lock(|nets| {
385 nets.get_mut("dev")
386 .unwrap()
387 .connect(&format!("box-{i}"), &format!("name-{i}"))
388 .map_err(BoxError::NetworkError)
389 })
390 .unwrap()
391 .ip_address
392 })
393 })
394 .collect();
395
396 let ips: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
397 let unique: HashSet<_> = ips.iter().collect();
398 assert_eq!(
399 unique.len(),
400 ips.len(),
401 "concurrent connects must allocate distinct IPs (got {ips:?})"
402 );
403 assert_eq!(
404 store.get("dev").unwrap().unwrap().endpoints.len(),
405 16,
406 "every concurrent endpoint must be persisted (no lost writes)"
407 );
408 }
409}