1use crate::snapshot::Snapshot;
2use crate::{EnvVar, EnvVarManager};
3use color_eyre::Result;
4use color_eyre::eyre::eyre;
5use std::collections::HashMap;
6use std::fs;
7use std::path::PathBuf;
8
9pub struct SnapshotManager {
10 storage_dir: PathBuf,
11}
12
13impl SnapshotManager {
14 pub fn new() -> Result<Self> {
22 let storage_dir = if cfg!(windows) {
23 dirs::data_dir()
24 .ok_or_else(|| eyre!("Could not find data directory"))?
25 .join("envx")
26 .join("snapshots")
27 } else {
28 dirs::config_dir()
29 .ok_or_else(|| eyre!("Could not find config directory"))?
30 .join("envx")
31 .join("snapshots")
32 };
33
34 fs::create_dir_all(&storage_dir)?;
35 Ok(Self { storage_dir })
36 }
37
38 pub fn create(&self, name: String, description: Option<String>, vars: Vec<EnvVar>) -> Result<Snapshot> {
46 let snapshot = Snapshot::from_vars(name, description, vars);
47 self.save_snapshot(&snapshot)?;
48 Ok(snapshot)
49 }
50
51 pub fn list(&self) -> Result<Vec<Snapshot>> {
59 let mut snapshots = Vec::new();
60
61 for entry in fs::read_dir(&self.storage_dir)? {
62 let entry = entry?;
63 if entry.path().extension().and_then(|s| s.to_str()) == Some("json") {
64 let content = fs::read_to_string(entry.path())?;
65 if let Ok(snapshot) = serde_json::from_str::<Snapshot>(&content) {
66 snapshots.push(snapshot);
67 }
68 }
69 }
70
71 snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
73 Ok(snapshots)
74 }
75
76 pub fn get(&self, id_or_name: &str) -> Result<Snapshot> {
85 let id_path = self.storage_dir.join(format!("{id_or_name}.json"));
87 if id_path.exists() {
88 let content = fs::read_to_string(&id_path)?;
89 return Ok(serde_json::from_str(&content)?);
90 }
91
92 for snapshot in self.list()? {
94 if snapshot.name == id_or_name {
95 return Ok(snapshot);
96 }
97 }
98
99 Err(eyre!("Snapshot not found: {}", id_or_name))
100 }
101
102 pub fn delete(&self, id_or_name: &str) -> Result<()> {
110 let snapshot = self.get(id_or_name)?;
111 let path = self.storage_dir.join(format!("{}.json", snapshot.id));
112 fs::remove_file(path)?;
113 Ok(())
114 }
115
116 pub fn restore(&self, id_or_name: &str, manager: &mut EnvVarManager) -> Result<()> {
126 let snapshot = self.get(id_or_name)?;
127
128 manager.clear();
130
131 for (_, var) in snapshot.variables {
133 manager.set(&var.name, &var.value, true)?;
134 }
135
136 Ok(())
137 }
138
139 pub fn diff(&self, snapshot1: &str, snapshot2: &str) -> Result<SnapshotDiff> {
148 let snap1 = self.get(snapshot1)?;
149 let snap2 = self.get(snapshot2)?;
150
151 let mut diff = SnapshotDiff::default();
152
153 for (name, var2) in &snap2.variables {
155 match snap1.variables.get(name) {
156 Some(var1) => {
157 if var1.value != var2.value {
158 diff.modified.insert(name.clone(), (var1.clone(), var2.clone()));
159 }
160 }
161 None => {
162 diff.added.insert(name.clone(), var2.clone());
163 }
164 }
165 }
166
167 for (name, var1) in &snap1.variables {
169 if !snap2.variables.contains_key(name) {
170 diff.removed.insert(name.clone(), var1.clone());
171 }
172 }
173
174 Ok(diff)
175 }
176
177 fn save_snapshot(&self, snapshot: &Snapshot) -> color_eyre::Result<()> {
178 let path = self.storage_dir.join(format!("{}.json", snapshot.id));
179 let content = serde_json::to_string_pretty(snapshot)?;
180 fs::write(path, content)?;
181 Ok(())
182 }
183}
184
185#[derive(Debug, Default)]
186pub struct SnapshotDiff {
187 pub added: HashMap<String, EnvVar>,
188 pub removed: HashMap<String, EnvVar>,
189 pub modified: HashMap<String, (EnvVar, EnvVar)>, }
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::{EnvVar, EnvVarSource};
196 use chrono::Utc;
197 use tempfile::TempDir;
198
199 fn create_test_snapshot_manager() -> (SnapshotManager, TempDir) {
200 let temp_dir = TempDir::new().unwrap();
201 let storage_dir = temp_dir.path().join("snapshots");
202 fs::create_dir_all(&storage_dir).unwrap();
203
204 let manager = SnapshotManager { storage_dir };
205 (manager, temp_dir)
206 }
207
208 fn create_test_env_var(name: &str, value: &str) -> EnvVar {
209 EnvVar {
210 name: name.to_string(),
211 value: value.to_string(),
212 source: EnvVarSource::User,
213 modified: Utc::now(),
214 original_value: None,
215 }
216 }
217
218 fn create_test_env_manager() -> EnvVarManager {
219 let mut manager = EnvVarManager::new();
220 manager.set("VAR1", "value1", false).unwrap();
221 manager.set("VAR2", "value2", false).unwrap();
222 manager.set("VAR3", "value3", false).unwrap();
223 manager
224 }
225
226 #[test]
227 fn test_snapshot_manager_new() {
228 let temp_dir = TempDir::new().unwrap();
230 let storage_dir = temp_dir.path().join("envx").join("snapshots");
231
232 let manager = SnapshotManager {
234 storage_dir: storage_dir.clone(),
235 };
236
237 assert_eq!(manager.storage_dir, storage_dir);
239 }
240
241 #[test]
242 fn test_create_snapshot() {
243 let (manager, _temp) = create_test_snapshot_manager();
244
245 let vars = vec![
246 create_test_env_var("TEST_VAR1", "test_value1"),
247 create_test_env_var("TEST_VAR2", "test_value2"),
248 ];
249
250 let result = manager.create("test-snapshot".to_string(), Some("Test description".to_string()), vars);
251
252 assert!(result.is_ok());
253 let snapshot = result.unwrap();
254
255 assert_eq!(snapshot.name, "test-snapshot");
256 assert_eq!(snapshot.description, Some("Test description".to_string()));
257 assert_eq!(snapshot.variables.len(), 2);
258 assert!(snapshot.variables.contains_key("TEST_VAR1"));
259 assert!(snapshot.variables.contains_key("TEST_VAR2"));
260
261 let snapshot_path = manager.storage_dir.join(format!("{}.json", snapshot.id));
263 assert!(snapshot_path.exists());
264 }
265
266 #[test]
267 fn test_create_snapshot_without_description() {
268 let (manager, _temp) = create_test_snapshot_manager();
269
270 let vars = vec![create_test_env_var("TEST_VAR", "test_value")];
271 let result = manager.create("no-desc".to_string(), None, vars);
272
273 assert!(result.is_ok());
274 assert!(result.unwrap().description.is_none());
275 }
276
277 #[test]
278 fn test_list_snapshots_empty() {
279 let (manager, _temp) = create_test_snapshot_manager();
280
281 let result = manager.list();
282 assert!(result.is_ok());
283 assert!(result.unwrap().is_empty());
284 }
285
286 #[test]
287 fn test_list_snapshots_multiple() {
288 let (manager, _temp) = create_test_snapshot_manager();
289
290 let vars = vec![create_test_env_var("VAR", "value")];
292 manager.create("snap1".to_string(), None, vars.clone()).unwrap();
293
294 std::thread::sleep(std::time::Duration::from_millis(10));
296
297 manager.create("snap2".to_string(), None, vars.clone()).unwrap();
298 manager.create("snap3".to_string(), None, vars).unwrap();
299
300 let snapshots = manager.list().unwrap();
301 assert_eq!(snapshots.len(), 3);
302
303 assert_eq!(snapshots[0].name, "snap3");
305 assert_eq!(snapshots[1].name, "snap2");
306 assert_eq!(snapshots[2].name, "snap1");
307 }
308
309 #[test]
310 fn test_list_snapshots_handles_invalid_files() {
311 let (manager, _temp) = create_test_snapshot_manager();
312
313 let vars = vec![create_test_env_var("VAR", "value")];
315 manager.create("valid".to_string(), None, vars).unwrap();
316
317 let invalid_path = manager.storage_dir.join("invalid.json");
319 fs::write(invalid_path, "{ invalid json }").unwrap();
320
321 let non_json_path = manager.storage_dir.join("not-json.txt");
323 fs::write(non_json_path, "some content").unwrap();
324
325 let snapshots = manager.list().unwrap();
327 assert_eq!(snapshots.len(), 1);
328 assert_eq!(snapshots[0].name, "valid");
329 }
330
331 #[test]
332 fn test_get_snapshot_by_id() {
333 let (manager, _temp) = create_test_snapshot_manager();
334
335 let vars = vec![create_test_env_var("VAR", "value")];
336 let created = manager.create("test".to_string(), None, vars).unwrap();
337
338 let retrieved = manager.get(&created.id).unwrap();
339 assert_eq!(retrieved.id, created.id);
340 assert_eq!(retrieved.name, created.name);
341 }
342
343 #[test]
344 fn test_get_snapshot_by_name() {
345 let (manager, _temp) = create_test_snapshot_manager();
346
347 let vars = vec![create_test_env_var("VAR", "value")];
348 manager.create("test-name".to_string(), None, vars).unwrap();
349
350 let retrieved = manager.get("test-name").unwrap();
351 assert_eq!(retrieved.name, "test-name");
352 }
353
354 #[test]
355 fn test_get_snapshot_not_found() {
356 let (manager, _temp) = create_test_snapshot_manager();
357
358 let result = manager.get("nonexistent");
359 assert!(result.is_err());
360 assert!(result.unwrap_err().to_string().contains("Snapshot not found"));
361 }
362
363 #[test]
364 fn test_get_snapshot_prefers_id_over_name() {
365 let (manager, _temp) = create_test_snapshot_manager();
366
367 let vars = vec![create_test_env_var("VAR", "value")];
369 let snap1 = manager.create("first".to_string(), None, vars.clone()).unwrap();
370
371 manager.create(snap1.id.clone(), None, vars).unwrap();
373
374 let retrieved = manager.get(&snap1.id).unwrap();
376 assert_eq!(retrieved.name, "first");
377 }
378
379 #[test]
380 fn test_delete_snapshot() {
381 let (manager, _temp) = create_test_snapshot_manager();
382
383 let vars = vec![create_test_env_var("VAR", "value")];
384 let snapshot = manager.create("to-delete".to_string(), None, vars).unwrap();
385
386 assert!(manager.get(&snapshot.id).is_ok());
388
389 let result = manager.delete(&snapshot.id);
391 assert!(result.is_ok());
392
393 assert!(manager.get(&snapshot.id).is_err());
395
396 let snapshot_path = manager.storage_dir.join(format!("{}.json", snapshot.id));
398 assert!(!snapshot_path.exists());
399 }
400
401 #[test]
402 fn test_delete_snapshot_by_name() {
403 let (manager, _temp) = create_test_snapshot_manager();
404
405 let vars = vec![create_test_env_var("VAR", "value")];
406 manager.create("delete-by-name".to_string(), None, vars).unwrap();
407
408 let result = manager.delete("delete-by-name");
409 assert!(result.is_ok());
410 assert!(manager.get("delete-by-name").is_err());
411 }
412
413 #[test]
414 fn test_delete_nonexistent_snapshot() {
415 let (manager, _temp) = create_test_snapshot_manager();
416
417 let result = manager.delete("nonexistent");
418 assert!(result.is_err());
419 }
420
421 #[test]
422 fn test_restore_snapshot() {
423 let (manager, _temp) = create_test_snapshot_manager();
424 let mut env_manager = create_test_env_manager();
425
426 let vars = vec![
428 create_test_env_var("NEW_VAR1", "new_value1"),
429 create_test_env_var("NEW_VAR2", "new_value2"),
430 ];
431 let snapshot = manager.create("to-restore".to_string(), None, vars).unwrap();
432
433 let result = manager.restore(&snapshot.id, &mut env_manager);
435 assert!(result.is_ok());
436
437 assert!(env_manager.get("VAR1").is_none());
439 assert!(env_manager.get("VAR2").is_none());
440 assert!(env_manager.get("VAR3").is_none());
441
442 assert_eq!(env_manager.get("NEW_VAR1").unwrap().value, "new_value1");
443 assert_eq!(env_manager.get("NEW_VAR2").unwrap().value, "new_value2");
444 }
445
446 #[test]
447 fn test_restore_nonexistent_snapshot() {
448 let (manager, _temp) = create_test_snapshot_manager();
449 let mut env_manager = create_test_env_manager();
450
451 let result = manager.restore("nonexistent", &mut env_manager);
452 assert!(result.is_err());
453 }
454
455 #[test]
456 fn test_diff_snapshots_no_changes() {
457 let (manager, _temp) = create_test_snapshot_manager();
458
459 let vars = vec![
460 create_test_env_var("VAR1", "value1"),
461 create_test_env_var("VAR2", "value2"),
462 ];
463
464 let snap1 = manager.create("snap1".to_string(), None, vars.clone()).unwrap();
465 let snap2 = manager.create("snap2".to_string(), None, vars).unwrap();
466
467 let diff = manager.diff(&snap1.id, &snap2.id).unwrap();
468 assert!(diff.added.is_empty());
469 assert!(diff.removed.is_empty());
470 assert!(diff.modified.is_empty());
471 }
472
473 #[test]
474 fn test_diff_snapshots_with_changes() {
475 let (manager, _temp) = create_test_snapshot_manager();
476
477 let vars1 = vec![
478 create_test_env_var("VAR1", "value1"),
479 create_test_env_var("VAR2", "old_value"),
480 create_test_env_var("VAR3", "value3"),
481 ];
482
483 let vars2 = vec![
484 create_test_env_var("VAR1", "value1"), create_test_env_var("VAR2", "new_value"), create_test_env_var("VAR4", "value4"), ];
488
489 let snap1 = manager.create("snap1".to_string(), None, vars1).unwrap();
490 let snap2 = manager.create("snap2".to_string(), None, vars2).unwrap();
491
492 let diff = manager.diff(&snap1.id, &snap2.id).unwrap();
493
494 assert_eq!(diff.added.len(), 1);
496 assert!(diff.added.contains_key("VAR4"));
497 assert_eq!(diff.added.get("VAR4").unwrap().value, "value4");
498
499 assert_eq!(diff.removed.len(), 1);
501 assert!(diff.removed.contains_key("VAR3"));
502 assert_eq!(diff.removed.get("VAR3").unwrap().value, "value3");
503
504 assert_eq!(diff.modified.len(), 1);
506 assert!(diff.modified.contains_key("VAR2"));
507 let (old, new) = diff.modified.get("VAR2").unwrap();
508 assert_eq!(old.value, "old_value");
509 assert_eq!(new.value, "new_value");
510 }
511
512 #[test]
513 fn test_diff_nonexistent_snapshots() {
514 let (manager, _temp) = create_test_snapshot_manager();
515
516 let result = manager.diff("nonexistent1", "nonexistent2");
517 assert!(result.is_err());
518 }
519
520 #[test]
521 fn test_save_snapshot_creates_pretty_json() {
522 let (manager, _temp) = create_test_snapshot_manager();
523
524 let vars = vec![create_test_env_var("TEST_VAR", "test_value")];
525 let snapshot = manager
526 .create("pretty-test".to_string(), Some("Pretty JSON test".to_string()), vars)
527 .unwrap();
528
529 let snapshot_path = manager.storage_dir.join(format!("{}.json", snapshot.id));
531 let content = fs::read_to_string(snapshot_path).unwrap();
532
533 assert!(content.contains("\n "));
535 assert!(content.contains("\"name\": \"pretty-test\""));
536 assert!(content.contains("\"description\": \"Pretty JSON test\""));
537 }
538
539 #[test]
540 fn test_concurrent_operations() {
541 let (manager, _temp) = create_test_snapshot_manager();
542
543 let mut snapshot_ids = Vec::new();
545 for i in 0..5 {
546 let vars = vec![create_test_env_var(&format!("VAR{i}"), &format!("value{i}"))];
547 let snapshot = manager.create(format!("concurrent-{i}"), None, vars).unwrap();
548 snapshot_ids.push(snapshot.id);
549 }
550
551 for id in &snapshot_ids {
553 assert!(manager.get(id).is_ok());
554 }
555
556 let snapshots = manager.list().unwrap();
558 assert_eq!(snapshots.len(), 5);
559 }
560}