1use crate::error::Result;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BackupInfo {
10 pub path: PathBuf,
11 pub created_at: DateTime<Utc>,
12 pub size: u64,
13 pub override_count: usize,
14}
15
16pub struct BackupManager {
18 backup_dir: PathBuf,
19 max_backups: usize,
20}
21
22impl BackupManager {
23 pub fn new(workspace_path: &Path, max_backups: usize) -> Result<Self> {
25 let backup_dir = workspace_path.join(".raz").join("overrides.backup");
26
27 fs::create_dir_all(&backup_dir)?;
29
30 Ok(Self {
31 backup_dir,
32 max_backups,
33 })
34 }
35
36 pub fn create_backup<T: Serialize>(&self, current_state: &T) -> Result<PathBuf> {
38 let timestamp = Utc::now().format("%Y%m%d_%H%M%S_%3f");
40 let backup_filename = format!("overrides_{timestamp}.toml");
41 let backup_path = self.backup_dir.join(&backup_filename);
42
43 let content = toml::to_string_pretty(current_state)?;
45 fs::write(&backup_path, content)?;
46
47 self.cleanup_old_backups()?;
49
50 Ok(backup_path)
51 }
52
53 pub fn restore_backup<T: for<'de> Deserialize<'de>>(&self, backup_path: &Path) -> Result<T> {
55 let content = fs::read_to_string(backup_path)?;
56 let restored = toml::from_str(&content)?;
57 Ok(restored)
58 }
59
60 pub fn get_last_backup(&self) -> Result<Option<BackupInfo>> {
62 let backups = self.list_backups()?;
63 Ok(backups.into_iter().next())
64 }
65
66 pub fn list_backups(&self) -> Result<Vec<BackupInfo>> {
68 let mut backups = Vec::new();
69
70 for entry in fs::read_dir(&self.backup_dir)? {
72 let entry = entry?;
73 let path = entry.path();
74
75 if path.extension().and_then(|s| s.to_str()) != Some("toml") {
77 continue;
78 }
79
80 let metadata = entry.metadata()?;
82 let created_at = metadata
83 .modified()
84 .map(DateTime::<Utc>::from)
85 .unwrap_or_else(|_| Utc::now());
86
87 let override_count = if let Ok(content) = fs::read_to_string(&path) {
89 if let Ok(data) = toml::from_str::<toml::Value>(&content) {
90 data.get("overrides")
91 .and_then(|v| v.as_table())
92 .map(|t| t.len())
93 .unwrap_or(0)
94 } else {
95 0
96 }
97 } else {
98 0
99 };
100
101 backups.push(BackupInfo {
102 path,
103 created_at,
104 size: metadata.len(),
105 override_count,
106 });
107 }
108
109 backups.sort_by(|a, b| b.created_at.cmp(&a.created_at));
111
112 Ok(backups)
113 }
114
115 pub fn cleanup_old_backups(&self) -> Result<()> {
117 let backups = self.list_backups()?;
118
119 if backups.len() <= self.max_backups {
121 return Ok(());
122 }
123
124 for backup in backups.into_iter().skip(self.max_backups) {
126 if let Err(e) = fs::remove_file(&backup.path) {
127 log::warn!(
128 "Failed to remove old backup {}: {}",
129 backup.path.display(),
130 e
131 );
132 }
133 }
134
135 Ok(())
136 }
137
138 pub fn find_last_good_backup(&self) -> Result<Option<BackupInfo>> {
140 self.get_last_backup()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::thread;
150 use std::time::Duration;
151 use tempfile::TempDir;
152
153 #[derive(Debug, Serialize, Deserialize, PartialEq)]
154 struct TestData {
155 version: u32,
156 data: String,
157 }
158
159 #[test]
160 fn test_backup_create_and_restore() {
161 let temp_dir = TempDir::new().unwrap();
162 let manager = BackupManager::new(temp_dir.path(), 5).unwrap();
163
164 let test_data = TestData {
165 version: 1,
166 data: "test content".to_string(),
167 };
168
169 let backup_path = manager.create_backup(&test_data).unwrap();
171 assert!(backup_path.exists());
172
173 let restored: TestData = manager.restore_backup(&backup_path).unwrap();
175 assert_eq!(restored, test_data);
176 }
177
178 #[test]
179 fn test_backup_cleanup() {
180 let temp_dir = TempDir::new().unwrap();
181 let manager = BackupManager::new(temp_dir.path(), 3).unwrap();
182
183 for i in 0..5 {
185 let test_data = TestData {
186 version: i,
187 data: format!("backup {i}"),
188 };
189 manager.create_backup(&test_data).unwrap();
190 thread::sleep(Duration::from_millis(10));
192 }
193
194 let backups = manager.list_backups().unwrap();
196 assert_eq!(backups.len(), 3);
197
198 for (i, backup) in backups.iter().enumerate() {
200 let restored: TestData = manager.restore_backup(&backup.path).unwrap();
201 assert_eq!(restored.version, 4 - i as u32);
202 }
203 }
204
205 #[test]
206 fn test_list_backups_ordering() {
207 let temp_dir = TempDir::new().unwrap();
208 let manager = BackupManager::new(temp_dir.path(), 10).unwrap();
209
210 for i in 0..3 {
212 let test_data = TestData {
213 version: i,
214 data: format!("backup {i}"),
215 };
216 manager.create_backup(&test_data).unwrap();
217 thread::sleep(Duration::from_millis(50));
218 }
219
220 let backups = manager.list_backups().unwrap();
222 assert_eq!(backups.len(), 3);
223
224 for i in 0..2 {
226 assert!(backups[i].created_at > backups[i + 1].created_at);
227 }
228 }
229}