ricecoder_files/
backup.rs1use crate::error::FileError;
4use crate::models::BackupMetadata;
5use crate::verifier::ContentVerifier;
6use chrono::Utc;
7use std::path::{Path, PathBuf};
8use tokio::fs;
9
10#[derive(Debug, Clone)]
12pub struct BackupManager {
13 backup_dir: PathBuf,
14 retention_count: usize,
15}
16
17impl BackupManager {
18 pub fn new(backup_dir: PathBuf, retention_count: usize) -> Self {
25 BackupManager {
26 backup_dir,
27 retention_count,
28 }
29 }
30
31 pub async fn create_backup(&self, path: &Path) -> Result<BackupMetadata, FileError> {
41 let content = fs::read_to_string(path).await.map_err(|e| {
43 FileError::BackupFailed(format!("Failed to read file for backup: {}", e))
44 })?;
45
46 let content_hash = ContentVerifier::compute_hash(&content);
48
49 fs::create_dir_all(&self.backup_dir).await.map_err(|e| {
51 FileError::BackupFailed(format!("Failed to create backup directory: {}", e))
52 })?;
53
54 let timestamp = Utc::now();
56 let filename = format!(
57 "{}.{}.bak",
58 path.file_name().and_then(|n| n.to_str()).unwrap_or("file"),
59 timestamp.format("%Y%m%d_%H%M%S_%f")
60 );
61
62 let backup_path = self.backup_dir.join(&filename);
63
64 fs::write(&backup_path, &content)
66 .await
67 .map_err(|e| FileError::BackupFailed(format!("Failed to write backup file: {}", e)))?;
68
69 Ok(BackupMetadata {
70 original_path: path.to_path_buf(),
71 backup_path,
72 timestamp,
73 content_hash,
74 })
75 }
76
77 pub async fn enforce_retention_policy(&self, path: &Path) -> Result<(), FileError> {
87 let base_filename = path
89 .file_name()
90 .and_then(|n| n.to_str())
91 .ok_or_else(|| FileError::BackupFailed("Invalid file path".to_string()))?;
92
93 let mut entries = fs::read_dir(&self.backup_dir).await.map_err(|e| {
95 FileError::BackupFailed(format!("Failed to read backup directory: {}", e))
96 })?;
97
98 let mut backups = Vec::new();
99
100 while let Some(entry) = entries
102 .next_entry()
103 .await
104 .map_err(|e| FileError::BackupFailed(format!("Failed to read backup entry: {}", e)))?
105 {
106 let path = entry.path();
107 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
108 if filename.starts_with(base_filename) && filename.ends_with(".bak") {
110 if let Ok(metadata) = fs::metadata(&path).await {
111 backups.push((
112 path,
113 metadata
114 .modified()
115 .unwrap_or_else(|_| std::time::SystemTime::now()),
116 ));
117 }
118 }
119 }
120 }
121
122 backups.sort_by(|a, b| b.1.cmp(&a.1));
124
125 for (backup_path, _) in backups.iter().skip(self.retention_count) {
127 fs::remove_file(backup_path).await.map_err(|e| {
128 FileError::BackupFailed(format!("Failed to delete old backup: {}", e))
129 })?;
130 }
131
132 Ok(())
133 }
134
135 pub async fn restore_from_backup(
146 &self,
147 backup_path: &Path,
148 target_path: &Path,
149 ) -> Result<(), FileError> {
150 let content = fs::read_to_string(backup_path)
152 .await
153 .map_err(|e| FileError::BackupFailed(format!("Failed to read backup file: {}", e)))?;
154
155 if let Some(parent) = target_path.parent() {
157 fs::create_dir_all(parent).await.map_err(|e| {
158 FileError::BackupFailed(format!("Failed to create target directory: {}", e))
159 })?;
160 }
161
162 fs::write(target_path, &content)
164 .await
165 .map_err(|e| FileError::BackupFailed(format!("Failed to restore file: {}", e)))?;
166
167 Ok(())
168 }
169
170 pub async fn verify_backup_integrity(
181 &self,
182 backup_path: &Path,
183 stored_hash: &str,
184 ) -> Result<(), FileError> {
185 let verifier = ContentVerifier::new();
186 verifier.verify_backup(backup_path, stored_hash).await
187 }
188}
189
190impl Default for BackupManager {
191 fn default() -> Self {
192 Self::new(PathBuf::from(".ricecoder/backups"), 10)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use tempfile::TempDir;
200
201 #[tokio::test]
202 async fn test_create_backup() {
203 let temp_dir = TempDir::new().unwrap();
204 let backup_dir = temp_dir.path().join("backups");
205 let file_path = temp_dir.path().join("test.txt");
206
207 fs::write(&file_path, "test content").await.unwrap();
209
210 let manager = BackupManager::new(backup_dir, 10);
211 let metadata = manager.create_backup(&file_path).await.unwrap();
212
213 assert!(metadata.backup_path.exists());
215 let backup_content = fs::read_to_string(&metadata.backup_path).await.unwrap();
216 assert_eq!(backup_content, "test content");
217 }
218
219 #[tokio::test]
220 async fn test_backup_metadata_hash() {
221 let temp_dir = TempDir::new().unwrap();
222 let backup_dir = temp_dir.path().join("backups");
223 let file_path = temp_dir.path().join("test.txt");
224
225 let content = "test content";
226 fs::write(&file_path, content).await.unwrap();
227
228 let manager = BackupManager::new(backup_dir, 10);
229 let metadata = manager.create_backup(&file_path).await.unwrap();
230
231 let expected_hash = ContentVerifier::compute_hash(content);
233 assert_eq!(metadata.content_hash, expected_hash);
234 }
235
236 #[tokio::test]
237 async fn test_restore_from_backup() {
238 let temp_dir = TempDir::new().unwrap();
239 let backup_dir = temp_dir.path().join("backups");
240 let original_path = temp_dir.path().join("original.txt");
241 let restore_path = temp_dir.path().join("restored.txt");
242
243 let content = "original content";
244 fs::write(&original_path, content).await.unwrap();
245
246 let manager = BackupManager::new(backup_dir, 10);
247 let metadata = manager.create_backup(&original_path).await.unwrap();
248
249 manager
251 .restore_from_backup(&metadata.backup_path, &restore_path)
252 .await
253 .unwrap();
254
255 let restored_content = fs::read_to_string(&restore_path).await.unwrap();
257 assert_eq!(restored_content, content);
258 }
259
260 #[tokio::test]
261 async fn test_verify_backup_integrity_success() {
262 let temp_dir = TempDir::new().unwrap();
263 let backup_dir = temp_dir.path().join("backups");
264 let file_path = temp_dir.path().join("test.txt");
265
266 let content = "test content";
267 fs::write(&file_path, content).await.unwrap();
268
269 let manager = BackupManager::new(backup_dir, 10);
270 let metadata = manager.create_backup(&file_path).await.unwrap();
271
272 let result = manager
274 .verify_backup_integrity(&metadata.backup_path, &metadata.content_hash)
275 .await;
276 assert!(result.is_ok());
277 }
278
279 #[tokio::test]
280 async fn test_verify_backup_integrity_corruption() {
281 let temp_dir = TempDir::new().unwrap();
282 let backup_dir = temp_dir.path().join("backups");
283 let file_path = temp_dir.path().join("test.txt");
284
285 let content = "test content";
286 fs::write(&file_path, content).await.unwrap();
287
288 let manager = BackupManager::new(backup_dir, 10);
289 let metadata = manager.create_backup(&file_path).await.unwrap();
290
291 fs::write(&metadata.backup_path, "corrupted content")
293 .await
294 .unwrap();
295
296 let result = manager
298 .verify_backup_integrity(&metadata.backup_path, &metadata.content_hash)
299 .await;
300 assert!(result.is_err());
301 match result {
302 Err(FileError::BackupCorrupted) => (),
303 _ => panic!("Expected BackupCorrupted error"),
304 }
305 }
306
307 #[tokio::test]
308 async fn test_enforce_retention_policy() {
309 let temp_dir = TempDir::new().unwrap();
310 let backup_dir = temp_dir.path().join("backups");
311 let file_path = temp_dir.path().join("test.txt");
312
313 fs::write(&file_path, "content").await.unwrap();
314
315 let manager = BackupManager::new(backup_dir.clone(), 3);
316
317 for i in 0..5 {
319 fs::write(&file_path, &format!("content {}", i))
320 .await
321 .unwrap();
322 manager.create_backup(&file_path).await.unwrap();
323 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
325 }
326
327 manager.enforce_retention_policy(&file_path).await.unwrap();
329
330 let mut entries = fs::read_dir(&backup_dir).await.unwrap();
332 let mut count = 0;
333 while let Some(_entry) = entries.next_entry().await.unwrap() {
334 count += 1;
335 }
336
337 assert!(count <= 3);
339 }
340}