1use crate::change::Change;
4use crate::checkpoint::Checkpoint;
5use crate::error::UndoRedoError;
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HistorySnapshot {
15 pub changes: Vec<Change>,
17 pub checkpoints: HashMap<String, Checkpoint>,
19 pub snapshot_time: DateTime<Utc>,
21}
22
23impl HistorySnapshot {
24 pub fn new(
26 changes: Vec<Change>,
27 checkpoints: HashMap<String, Checkpoint>,
28 ) -> Self {
29 HistorySnapshot {
30 changes,
31 checkpoints,
32 snapshot_time: Utc::now(),
33 }
34 }
35
36 pub fn validate(&self) -> Result<(), UndoRedoError> {
38 for change in &self.changes {
40 change.validate()?;
41 }
42
43 for checkpoint in self.checkpoints.values() {
45 checkpoint.validate()?;
46 }
47
48 Ok(())
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct StorageStats {
55 pub size_bytes: u64,
57 pub entry_count: usize,
59 pub oldest_entry: Option<DateTime<Utc>>,
61 pub newest_entry: Option<DateTime<Utc>>,
63}
64
65pub struct HistoryStore {
67 storage_path: PathBuf,
68 history_data: Option<HistorySnapshot>,
69 max_retries: usize,
70}
71
72impl HistoryStore {
73 pub fn new(storage_path: impl AsRef<Path>) -> Result<Self, UndoRedoError> {
75 let storage_path = storage_path.as_ref().to_path_buf();
76
77 if let Some(parent) = storage_path.parent() {
79 if !parent.exists() {
80 fs::create_dir_all(parent).map_err(|e| {
81 UndoRedoError::storage_error(format!("Failed to create storage directory: {}", e))
82 })?;
83 }
84 }
85
86 Ok(HistoryStore {
87 storage_path,
88 history_data: None,
89 max_retries: 3,
90 })
91 }
92
93 pub fn save_history(&mut self, snapshot: HistorySnapshot) -> Result<(), UndoRedoError> {
95 snapshot.validate()?;
97
98 let mut last_error = None;
99
100 for attempt in 0..self.max_retries {
102 match self.save_history_attempt(&snapshot) {
103 Ok(_) => {
104 self.history_data = Some(snapshot);
105 return Ok(());
106 }
107 Err(e) => {
108 last_error = Some(e);
109 if attempt < self.max_retries - 1 {
110 let backoff_ms = 100 * (2_u64.pow(attempt as u32));
112 std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
113 }
114 }
115 }
116 }
117
118 Err(last_error.unwrap_or_else(|| {
119 UndoRedoError::storage_error("Failed to save history after retries")
120 }))
121 }
122
123 fn save_history_attempt(&self, snapshot: &HistorySnapshot) -> Result<(), UndoRedoError> {
125 let json = serde_json::to_string_pretty(snapshot)?;
126 fs::write(&self.storage_path, json).map_err(|e| {
127 UndoRedoError::storage_error(format!("Failed to write history file: {}", e))
128 })?;
129 Ok(())
130 }
131
132 pub fn load_history(&mut self) -> Result<HistorySnapshot, UndoRedoError> {
134 if !self.storage_path.exists() {
136 let empty_snapshot = HistorySnapshot::new(Vec::new(), HashMap::new());
137 self.history_data = Some(empty_snapshot.clone());
138 return Ok(empty_snapshot);
139 }
140
141 match self.load_history_attempt() {
143 Ok(snapshot) => {
144 self.history_data = Some(snapshot.clone());
145 Ok(snapshot)
146 }
147 Err(e) => {
148 eprintln!("Warning: Failed to load history: {}. Starting with empty history.", e);
150 let empty_snapshot = HistorySnapshot::new(Vec::new(), HashMap::new());
151 self.history_data = Some(empty_snapshot.clone());
152 Ok(empty_snapshot)
153 }
154 }
155 }
156
157 fn load_history_attempt(&self) -> Result<HistorySnapshot, UndoRedoError> {
159 let content = fs::read_to_string(&self.storage_path).map_err(|e| {
160 UndoRedoError::storage_error(format!("Failed to read history file: {}", e))
161 })?;
162
163 let snapshot: HistorySnapshot = serde_json::from_str(&content).map_err(|e| {
164 UndoRedoError::storage_error(format!("Failed to deserialize history: {}", e))
165 })?;
166
167 snapshot.validate()?;
169
170 Ok(snapshot)
171 }
172
173 pub fn cleanup_old_entries(&mut self, retention_days: i64) -> Result<(), UndoRedoError> {
175 let cutoff_time = Utc::now() - Duration::days(retention_days);
176
177 if let Some(snapshot) = &mut self.history_data {
178 let original_count = snapshot.changes.len();
180 snapshot.changes.retain(|change| change.timestamp > cutoff_time);
181 let removed_count = original_count - snapshot.changes.len();
182
183 if removed_count > 0 {
184 eprintln!(
185 "Cleaned up {} old history entries (older than {} days)",
186 removed_count, retention_days
187 );
188 }
189
190 let original_checkpoint_count = snapshot.checkpoints.len();
192 snapshot.checkpoints.retain(|_, cp| cp.created_at > cutoff_time);
193 let removed_checkpoint_count = original_checkpoint_count - snapshot.checkpoints.len();
194
195 if removed_checkpoint_count > 0 {
196 eprintln!(
197 "Cleaned up {} old checkpoints (older than {} days)",
198 removed_checkpoint_count, retention_days
199 );
200 }
201 }
202
203 Ok(())
204 }
205
206 pub fn get_storage_stats(&self) -> Result<StorageStats, UndoRedoError> {
208 if !self.storage_path.exists() {
209 return Ok(StorageStats {
210 size_bytes: 0,
211 entry_count: 0,
212 oldest_entry: None,
213 newest_entry: None,
214 });
215 }
216
217 let metadata = fs::metadata(&self.storage_path).map_err(|e| {
218 UndoRedoError::storage_error(format!("Failed to get file metadata: {}", e))
219 })?;
220
221 let size_bytes = metadata.len();
222
223 let (entry_count, oldest_entry, newest_entry) = if let Some(snapshot) = &self.history_data {
224 let entry_count = snapshot.changes.len() + snapshot.checkpoints.len();
225
226 let oldest_entry = snapshot
227 .changes
228 .iter()
229 .map(|c| c.timestamp)
230 .chain(snapshot.checkpoints.values().map(|cp| cp.created_at))
231 .min();
232
233 let newest_entry = snapshot
234 .changes
235 .iter()
236 .map(|c| c.timestamp)
237 .chain(snapshot.checkpoints.values().map(|cp| cp.created_at))
238 .max();
239
240 (entry_count, oldest_entry, newest_entry)
241 } else {
242 (0, None, None)
243 };
244
245 Ok(StorageStats {
246 size_bytes,
247 entry_count,
248 oldest_entry,
249 newest_entry,
250 })
251 }
252
253 pub fn is_storage_full(&self) -> Result<bool, UndoRedoError> {
255 const MAX_STORAGE_BYTES: u64 = 1024 * 1024 * 1024; let stats = self.get_storage_stats()?;
257 Ok(stats.size_bytes > MAX_STORAGE_BYTES)
258 }
259
260 pub fn handle_storage_full(&self) -> Result<(), UndoRedoError> {
262 if self.is_storage_full()? {
263 eprintln!(
264 "Warning: History storage is full (>1GB). Consider cleaning up old entries."
265 );
266 }
267 Ok(())
268 }
269
270 pub fn get_history_data(&self) -> Option<HistorySnapshot> {
272 self.history_data.clone()
273 }
274
275 pub fn set_history_data(&mut self, snapshot: HistorySnapshot) {
277 self.history_data = Some(snapshot);
278 }
279}
280
281pub struct StorageManager {
283 store: HistoryStore,
284 retention_days: i64,
285 max_storage_bytes: u64,
286}
287
288impl StorageManager {
289 pub fn new(
291 storage_path: impl AsRef<Path>,
292 retention_days: i64,
293 max_storage_bytes: u64,
294 ) -> Result<Self, UndoRedoError> {
295 let store = HistoryStore::new(storage_path)?;
296 Ok(StorageManager {
297 store,
298 retention_days,
299 max_storage_bytes,
300 })
301 }
302
303 pub fn with_defaults(storage_path: impl AsRef<Path>) -> Result<Self, UndoRedoError> {
305 Self::new(storage_path, 30, 1024 * 1024 * 1024)
306 }
307
308 pub fn cleanup_on_session_start(&mut self) -> Result<(), UndoRedoError> {
310 self.store.load_history()?;
312
313 self.store.cleanup_old_entries(self.retention_days)?;
315
316 if self.store.is_storage_full()? {
318 self.store.handle_storage_full()?;
319 }
320
321 Ok(())
322 }
323
324 pub fn cleanup_on_session_end(&mut self) -> Result<(), UndoRedoError> {
326 self.store.cleanup_old_entries(self.retention_days)?;
328
329 if self.store.is_storage_full()? {
331 self.store.handle_storage_full()?;
332 }
333
334 Ok(())
335 }
336
337 pub fn enforce_storage_limit(&mut self) -> Result<(), UndoRedoError> {
339 let stats = self.store.get_storage_stats()?;
340
341 if stats.size_bytes > self.max_storage_bytes {
342 eprintln!(
343 "Storage limit exceeded: {} bytes > {} bytes. Removing oldest entries.",
344 stats.size_bytes, self.max_storage_bytes
345 );
346
347 self.store.cleanup_old_entries(self.retention_days)?;
349
350 let stats_after = self.store.get_storage_stats()?;
352 if stats_after.size_bytes > self.max_storage_bytes {
353 eprintln!(
354 "Still over limit after cleanup. Removing entries older than 7 days."
355 );
356 self.store.cleanup_old_entries(7)?;
357 }
358 }
359
360 Ok(())
361 }
362
363 pub fn get_store(&self) -> &HistoryStore {
365 &self.store
366 }
367
368 pub fn get_store_mut(&mut self) -> &mut HistoryStore {
370 &mut self.store
371 }
372
373 pub fn set_retention_days(&mut self, days: i64) {
375 self.retention_days = days;
376 }
377
378 pub fn set_max_storage_bytes(&mut self, bytes: u64) {
380 self.max_storage_bytes = bytes;
381 }
382
383 pub fn get_retention_days(&self) -> i64 {
385 self.retention_days
386 }
387
388 pub fn get_max_storage_bytes(&self) -> u64 {
390 self.max_storage_bytes
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::change::ChangeType;
398 use std::fs;
399 use tempfile::TempDir;
400
401 #[test]
402 fn test_history_store_create() {
403 let temp_dir = TempDir::new().unwrap();
404 let store_path = temp_dir.path().join("history.json");
405 let store = HistoryStore::new(&store_path);
406 assert!(store.is_ok());
407 }
408
409 #[test]
410 fn test_history_store_save_and_load() {
411 let temp_dir = TempDir::new().unwrap();
412 let store_path = temp_dir.path().join("history.json");
413
414 let mut store = HistoryStore::new(&store_path).unwrap();
416 let change = Change::new(
417 "test.txt",
418 "before",
419 "after",
420 "Test change",
421 ChangeType::Modify,
422 )
423 .unwrap();
424 let snapshot = HistorySnapshot::new(vec![change.clone()], HashMap::new());
425 store.save_history(snapshot).unwrap();
426
427 let mut store2 = HistoryStore::new(&store_path).unwrap();
429 let loaded = store2.load_history().unwrap();
430 assert_eq!(loaded.changes.len(), 1);
431 assert_eq!(loaded.changes[0].id, change.id);
432 }
433
434 #[test]
435 fn test_history_store_load_nonexistent() {
436 let temp_dir = TempDir::new().unwrap();
437 let store_path = temp_dir.path().join("nonexistent.json");
438
439 let mut store = HistoryStore::new(&store_path).unwrap();
440 let loaded = store.load_history().unwrap();
441 assert_eq!(loaded.changes.len(), 0);
442 assert_eq!(loaded.checkpoints.len(), 0);
443 }
444
445 #[test]
446 fn test_history_store_cleanup_old_entries() {
447 let temp_dir = TempDir::new().unwrap();
448 let store_path = temp_dir.path().join("history.json");
449
450 let mut store = HistoryStore::new(&store_path).unwrap();
451
452 let mut changes = Vec::new();
454 for i in 0..3 {
455 let change = Change::new(
456 "test.txt",
457 "before",
458 "after",
459 &format!("Change {}", i),
460 ChangeType::Modify,
461 )
462 .unwrap();
463 changes.push(change);
464 }
465
466 let snapshot = HistorySnapshot::new(changes, HashMap::new());
467 store.set_history_data(snapshot);
468
469 store.cleanup_old_entries(0).unwrap();
471
472 let stats = store.get_storage_stats().unwrap();
473 assert_eq!(stats.entry_count, 0);
474 }
475
476 #[test]
477 fn test_history_store_get_storage_stats() {
478 let temp_dir = TempDir::new().unwrap();
479 let store_path = temp_dir.path().join("history.json");
480
481 let mut store = HistoryStore::new(&store_path).unwrap();
482 let change = Change::new(
483 "test.txt",
484 "before",
485 "after",
486 "Test",
487 ChangeType::Modify,
488 )
489 .unwrap();
490 let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
491 store.save_history(snapshot).unwrap();
492
493 let stats = store.get_storage_stats().unwrap();
494 assert!(stats.size_bytes > 0);
495 assert_eq!(stats.entry_count, 1);
496 assert!(stats.oldest_entry.is_some());
497 assert!(stats.newest_entry.is_some());
498 }
499
500 #[test]
501 fn test_history_store_is_storage_full() {
502 let temp_dir = TempDir::new().unwrap();
503 let store_path = temp_dir.path().join("history.json");
504
505 let mut store = HistoryStore::new(&store_path).unwrap();
506 let change = Change::new(
507 "test.txt",
508 "before",
509 "after",
510 "Test",
511 ChangeType::Modify,
512 )
513 .unwrap();
514 let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
515 store.save_history(snapshot).unwrap();
516
517 let is_full = store.is_storage_full().unwrap();
518 assert!(!is_full); }
520
521 #[test]
522 fn test_history_snapshot_validate() {
523 let change = Change::new(
524 "test.txt",
525 "before",
526 "after",
527 "Test",
528 ChangeType::Modify,
529 )
530 .unwrap();
531 let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
532 assert!(snapshot.validate().is_ok());
533 }
534
535 #[test]
536 fn test_history_store_save_with_retry() {
537 let temp_dir = TempDir::new().unwrap();
538 let store_path = temp_dir.path().join("history.json");
539
540 let mut store = HistoryStore::new(&store_path).unwrap();
541 let change = Change::new(
542 "test.txt",
543 "before",
544 "after",
545 "Test",
546 ChangeType::Modify,
547 )
548 .unwrap();
549 let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
550
551 let result = store.save_history(snapshot);
553 assert!(result.is_ok());
554 }
555
556 #[test]
557 fn test_history_store_load_corrupted_file() {
558 let temp_dir = TempDir::new().unwrap();
559 let store_path = temp_dir.path().join("history.json");
560
561 fs::write(&store_path, "{ invalid json }").unwrap();
563
564 let mut store = HistoryStore::new(&store_path).unwrap();
565 let loaded = store.load_history().unwrap();
567 assert_eq!(loaded.changes.len(), 0);
568 }
569
570 #[test]
571 fn test_history_store_multiple_changes_and_checkpoints() {
572 let temp_dir = TempDir::new().unwrap();
573 let store_path = temp_dir.path().join("history.json");
574
575 let mut store = HistoryStore::new(&store_path).unwrap();
576
577 let mut changes = Vec::new();
579 for i in 0..5 {
580 let change = Change::new(
581 &format!("file{}.txt", i),
582 "before",
583 "after",
584 &format!("Change {}", i),
585 ChangeType::Modify,
586 )
587 .unwrap();
588 changes.push(change);
589 }
590
591 let mut checkpoints = HashMap::new();
593 for i in 0..2 {
594 let mut file_states = HashMap::new();
595 file_states.insert(format!("file{}.txt", i), format!("content{}", i));
596 let checkpoint = Checkpoint::new(
597 format!("Checkpoint {}", i),
598 "description",
599 file_states,
600 )
601 .unwrap();
602 checkpoints.insert(checkpoint.id.clone(), checkpoint);
603 }
604
605 let snapshot = HistorySnapshot::new(changes, checkpoints);
606 store.save_history(snapshot).unwrap();
607
608 let mut store2 = HistoryStore::new(&store_path).unwrap();
610 let loaded = store2.load_history().unwrap();
611 assert_eq!(loaded.changes.len(), 5);
612 assert_eq!(loaded.checkpoints.len(), 2);
613 }
614
615 #[test]
616 fn test_storage_manager_create() {
617 let temp_dir = TempDir::new().unwrap();
618 let store_path = temp_dir.path().join("history.json");
619 let manager = StorageManager::with_defaults(&store_path);
620 assert!(manager.is_ok());
621 }
622
623 #[test]
624 fn test_storage_manager_cleanup_on_session_start() {
625 let temp_dir = TempDir::new().unwrap();
626 let store_path = temp_dir.path().join("history.json");
627
628 let mut manager = StorageManager::with_defaults(&store_path).unwrap();
629 let result = manager.cleanup_on_session_start();
630 assert!(result.is_ok());
631 }
632
633 #[test]
634 fn test_storage_manager_cleanup_on_session_end() {
635 let temp_dir = TempDir::new().unwrap();
636 let store_path = temp_dir.path().join("history.json");
637
638 let mut manager = StorageManager::with_defaults(&store_path).unwrap();
639 let result = manager.cleanup_on_session_end();
640 assert!(result.is_ok());
641 }
642
643 #[test]
644 fn test_storage_manager_enforce_storage_limit() {
645 let temp_dir = TempDir::new().unwrap();
646 let store_path = temp_dir.path().join("history.json");
647
648 let mut manager = StorageManager::new(&store_path, 30, 1024 * 1024 * 1024).unwrap();
649
650 let change = Change::new(
652 "test.txt",
653 "before",
654 "after",
655 "Test",
656 ChangeType::Modify,
657 )
658 .unwrap();
659 let snapshot = HistorySnapshot::new(vec![change], HashMap::new());
660 manager.get_store_mut().save_history(snapshot).unwrap();
661
662 let result = manager.enforce_storage_limit();
664 assert!(result.is_ok());
665 }
666
667 #[test]
668 fn test_storage_manager_retention_days() {
669 let temp_dir = TempDir::new().unwrap();
670 let store_path = temp_dir.path().join("history.json");
671
672 let mut manager = StorageManager::with_defaults(&store_path).unwrap();
673 assert_eq!(manager.get_retention_days(), 30);
674
675 manager.set_retention_days(7);
676 assert_eq!(manager.get_retention_days(), 7);
677 }
678
679 #[test]
680 fn test_storage_manager_max_storage_bytes() {
681 let temp_dir = TempDir::new().unwrap();
682 let store_path = temp_dir.path().join("history.json");
683
684 let mut manager = StorageManager::with_defaults(&store_path).unwrap();
685 assert_eq!(manager.get_max_storage_bytes(), 1024 * 1024 * 1024);
686
687 manager.set_max_storage_bytes(512 * 1024 * 1024);
688 assert_eq!(manager.get_max_storage_bytes(), 512 * 1024 * 1024);
689 }
690}
691
692#[cfg(test)]
693mod property_tests {
694 use super::*;
695 use crate::change::ChangeType;
696 use proptest::prelude::*;
697 use tempfile::TempDir;
698
699 fn file_path_strategy() -> impl Strategy<Value = String> {
701 r"[a-zA-Z0-9_\-./]{1,50}\.rs"
702 .prop_map(|s| s.to_string())
703 }
704
705 fn content_strategy() -> impl Strategy<Value = String> {
707 r"[a-zA-Z0-9\s]{1,100}"
708 .prop_map(|s| s.to_string())
709 }
710
711 proptest! {
712 #[test]
717 fn prop_persistence_round_trip_small(
718 changes_data in prop::collection::vec(
719 (file_path_strategy(), content_strategy(), content_strategy()),
720 1..10
721 ),
722 ) {
723 let temp_dir = TempDir::new().unwrap();
724 let store_path = temp_dir.path().join("history.json");
725
726 let mut changes = Vec::new();
727 for (idx, (file_path, before, after)) in changes_data.iter().enumerate() {
728 prop_assume!(before != after);
729
730 if let Ok(change) = Change::new(
731 file_path.clone(),
732 before.clone(),
733 after.clone(),
734 format!("Change {}", idx),
735 ChangeType::Modify,
736 ) {
737 changes.push(change);
738 }
739 }
740
741 let mut store = HistoryStore::new(&store_path).unwrap();
743 let snapshot = HistorySnapshot::new(changes.clone(), HashMap::new());
744 store.save_history(snapshot).unwrap();
745
746 let mut store2 = HistoryStore::new(&store_path).unwrap();
748 let loaded = store2.load_history().unwrap();
749
750 prop_assert_eq!(
752 loaded.changes.len(),
753 changes.len(),
754 "Loaded changes count should match saved"
755 );
756
757 for (saved, loaded_change) in changes.iter().zip(loaded.changes.iter()) {
758 prop_assert_eq!(&saved.id, &loaded_change.id, "Change IDs should match");
759 prop_assert_eq!(
760 &saved.file_path, &loaded_change.file_path,
761 "File paths should match"
762 );
763 prop_assert_eq!(
764 &saved.before, &loaded_change.before,
765 "Before states should match"
766 );
767 prop_assert_eq!(
768 &saved.after, &loaded_change.after,
769 "After states should match"
770 );
771 }
772 }
773
774 #[test]
779 fn prop_persistence_round_trip_medium(
780 changes_data in prop::collection::vec(
781 (file_path_strategy(), content_strategy(), content_strategy()),
782 1..100
783 ),
784 ) {
785 let temp_dir = TempDir::new().unwrap();
786 let store_path = temp_dir.path().join("history.json");
787
788 let mut changes = Vec::new();
789 for (idx, (file_path, before, after)) in changes_data.iter().enumerate() {
790 prop_assume!(before != after);
791
792 if let Ok(change) = Change::new(
793 file_path.clone(),
794 before.clone(),
795 after.clone(),
796 format!("Change {}", idx),
797 ChangeType::Modify,
798 ) {
799 changes.push(change);
800 }
801 }
802
803 let mut store = HistoryStore::new(&store_path).unwrap();
805 let snapshot = HistorySnapshot::new(changes.clone(), HashMap::new());
806 store.save_history(snapshot).unwrap();
807
808 let mut store2 = HistoryStore::new(&store_path).unwrap();
810 let loaded = store2.load_history().unwrap();
811
812 prop_assert_eq!(
814 loaded.changes.len(),
815 changes.len(),
816 "Loaded changes count should match saved"
817 );
818
819 for (saved, loaded_change) in changes.iter().zip(loaded.changes.iter()) {
820 prop_assert_eq!(&saved.id, &loaded_change.id, "Change IDs should match");
821 prop_assert_eq!(
822 &saved.file_path, &loaded_change.file_path,
823 "File paths should match"
824 );
825 prop_assert_eq!(
826 &saved.before, &loaded_change.before,
827 "Before states should match"
828 );
829 prop_assert_eq!(
830 &saved.after, &loaded_change.after,
831 "After states should match"
832 );
833 }
834 }
835 }
836}