raz_override/
backup.rs

1use crate::error::Result;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7/// Information about a backup file
8#[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
16/// Manages backup operations for override storage
17pub struct BackupManager {
18    backup_dir: PathBuf,
19    max_backups: usize,
20}
21
22impl BackupManager {
23    /// Create a new backup manager
24    pub fn new(workspace_path: &Path, max_backups: usize) -> Result<Self> {
25        let backup_dir = workspace_path.join(".raz").join("overrides.backup");
26
27        // Ensure backup directory exists
28        fs::create_dir_all(&backup_dir)?;
29
30        Ok(Self {
31            backup_dir,
32            max_backups,
33        })
34    }
35
36    /// Create a backup of the current state
37    pub fn create_backup<T: Serialize>(&self, current_state: &T) -> Result<PathBuf> {
38        // Generate timestamp-based filename
39        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        // Serialize and save
44        let content = toml::to_string_pretty(current_state)?;
45        fs::write(&backup_path, content)?;
46
47        // Clean up old backups
48        self.cleanup_old_backups()?;
49
50        Ok(backup_path)
51    }
52
53    /// Restore from a backup file
54    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    /// Get the most recent backup
61    pub fn get_last_backup(&self) -> Result<Option<BackupInfo>> {
62        let backups = self.list_backups()?;
63        Ok(backups.into_iter().next())
64    }
65
66    /// List all backups, sorted by creation time (newest first)
67    pub fn list_backups(&self) -> Result<Vec<BackupInfo>> {
68        let mut backups = Vec::new();
69
70        // Read all backup files
71        for entry in fs::read_dir(&self.backup_dir)? {
72            let entry = entry?;
73            let path = entry.path();
74
75            // Skip non-toml files
76            if path.extension().and_then(|s| s.to_str()) != Some("toml") {
77                continue;
78            }
79
80            // Get file metadata
81            let metadata = entry.metadata()?;
82            let created_at = metadata
83                .modified()
84                .map(DateTime::<Utc>::from)
85                .unwrap_or_else(|_| Utc::now());
86
87            // Try to read override count
88            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        // Sort by creation time, newest first
110        backups.sort_by(|a, b| b.created_at.cmp(&a.created_at));
111
112        Ok(backups)
113    }
114
115    /// Clean up old backups, keeping only the most recent N
116    pub fn cleanup_old_backups(&self) -> Result<()> {
117        let backups = self.list_backups()?;
118
119        // Skip if we're under the limit
120        if backups.len() <= self.max_backups {
121            return Ok(());
122        }
123
124        // Remove old backups
125        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    /// Find a known-good backup (one that was marked as validated)
139    pub fn find_last_good_backup(&self) -> Result<Option<BackupInfo>> {
140        // For now, just return the most recent backup
141        // In the future, we could store validation metadata with backups
142        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        // Create backup
170        let backup_path = manager.create_backup(&test_data).unwrap();
171        assert!(backup_path.exists());
172
173        // Restore backup
174        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        // Create multiple backups
184        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            // Small delay to ensure different timestamps
191            thread::sleep(Duration::from_millis(10));
192        }
193
194        // Check that only 3 backups remain
195        let backups = manager.list_backups().unwrap();
196        assert_eq!(backups.len(), 3);
197
198        // Verify newest backups are kept
199        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        // Create backups with delays
211        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        // List should be newest first
221        let backups = manager.list_backups().unwrap();
222        assert_eq!(backups.len(), 3);
223
224        // Verify ordering
225        for i in 0..2 {
226            assert!(backups[i].created_at > backups[i + 1].created_at);
227        }
228    }
229}