envelope_cli/backup/
restore.rs1use std::fs;
6use std::path::Path;
7
8use crate::config::paths::EnvelopePaths;
9use crate::error::{EnvelopeError, EnvelopeResult};
10
11use super::manager::BackupArchive;
12
13fn parse_backup_contents(path: &Path, contents: &str) -> EnvelopeResult<BackupArchive> {
15 let extension = path
16 .extension()
17 .and_then(|e| e.to_str())
18 .unwrap_or("")
19 .to_lowercase();
20
21 match extension.as_str() {
22 "yaml" | "yml" => serde_yaml::from_str(contents)
23 .map_err(|e| EnvelopeError::Json(format!("Failed to parse YAML backup file: {}", e))),
24 _ => serde_json::from_str(contents)
25 .map_err(|e| EnvelopeError::Json(format!("Failed to parse backup file: {}", e))),
26 }
27}
28
29pub struct RestoreManager {
31 paths: EnvelopePaths,
32}
33
34impl RestoreManager {
35 pub fn new(paths: EnvelopePaths) -> Self {
37 Self { paths }
38 }
39
40 pub fn restore_from_file(&self, backup_path: &Path) -> EnvelopeResult<RestoreResult> {
46 let contents = fs::read_to_string(backup_path)
48 .map_err(|e| EnvelopeError::Io(format!("Failed to read backup file: {}", e)))?;
49
50 let archive: BackupArchive = parse_backup_contents(backup_path, &contents)?;
51
52 self.restore_from_archive(&archive)
53 }
54
55 pub fn restore_from_archive(&self, archive: &BackupArchive) -> EnvelopeResult<RestoreResult> {
57 self.paths.ensure_directories()?;
59
60 let mut result = RestoreResult::default();
61
62 if !archive.accounts.is_null() {
64 let json = serde_json::to_string_pretty(&archive.accounts)
65 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize accounts: {}", e)))?;
66 fs::write(self.paths.accounts_file(), json)
67 .map_err(|e| EnvelopeError::Io(format!("Failed to restore accounts: {}", e)))?;
68 result.accounts_restored = true;
69 }
70
71 if !archive.transactions.is_null() {
73 let json = serde_json::to_string_pretty(&archive.transactions).map_err(|e| {
74 EnvelopeError::Json(format!("Failed to serialize transactions: {}", e))
75 })?;
76 fs::write(self.paths.transactions_file(), json)
77 .map_err(|e| EnvelopeError::Io(format!("Failed to restore transactions: {}", e)))?;
78 result.transactions_restored = true;
79 }
80
81 if !archive.budget.is_null() {
83 let json = serde_json::to_string_pretty(&archive.budget)
84 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize budget: {}", e)))?;
85 fs::write(self.paths.budget_file(), json)
86 .map_err(|e| EnvelopeError::Io(format!("Failed to restore budget: {}", e)))?;
87 result.budget_restored = true;
88 }
89
90 if !archive.payees.is_null() {
92 let json = serde_json::to_string_pretty(&archive.payees)
93 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize payees: {}", e)))?;
94 fs::write(self.paths.payees_file(), json)
95 .map_err(|e| EnvelopeError::Io(format!("Failed to restore payees: {}", e)))?;
96 result.payees_restored = true;
97 }
98
99 result.schema_version = archive.schema_version;
100 result.backup_date = archive.created_at;
101
102 Ok(result)
103 }
104
105 pub fn validate_backup(&self, backup_path: &Path) -> EnvelopeResult<ValidationResult> {
108 let contents = fs::read_to_string(backup_path)
109 .map_err(|e| EnvelopeError::Io(format!("Failed to read backup file: {}", e)))?;
110
111 let archive: BackupArchive = parse_backup_contents(backup_path, &contents)?;
112
113 Ok(ValidationResult {
114 is_valid: true,
115 schema_version: archive.schema_version,
116 backup_date: archive.created_at,
117 has_accounts: !archive.accounts.is_null() && archive.accounts.is_object(),
118 has_transactions: !archive.transactions.is_null() && archive.transactions.is_object(),
119 has_budget: !archive.budget.is_null() && archive.budget.is_object(),
120 has_payees: !archive.payees.is_null() && archive.payees.is_object(),
121 })
122 }
123}
124
125#[derive(Debug, Default)]
127pub struct RestoreResult {
128 pub schema_version: u32,
130 pub backup_date: chrono::DateTime<chrono::Utc>,
132 pub accounts_restored: bool,
134 pub transactions_restored: bool,
136 pub budget_restored: bool,
138 pub payees_restored: bool,
140}
141
142impl RestoreResult {
143 pub fn all_restored(&self) -> bool {
145 self.accounts_restored
146 && self.transactions_restored
147 && self.budget_restored
148 && self.payees_restored
149 }
150
151 pub fn summary(&self) -> String {
153 let mut parts = Vec::new();
154 if self.accounts_restored {
155 parts.push("accounts");
156 }
157 if self.transactions_restored {
158 parts.push("transactions");
159 }
160 if self.budget_restored {
161 parts.push("budget");
162 }
163 if self.payees_restored {
164 parts.push("payees");
165 }
166 format!("Restored: {}", parts.join(", "))
167 }
168}
169
170#[derive(Debug)]
172pub struct ValidationResult {
173 pub is_valid: bool,
175 pub schema_version: u32,
177 pub backup_date: chrono::DateTime<chrono::Utc>,
179 pub has_accounts: bool,
181 pub has_transactions: bool,
183 pub has_budget: bool,
185 pub has_payees: bool,
187}
188
189impl ValidationResult {
190 pub fn is_complete(&self) -> bool {
192 self.has_accounts && self.has_transactions && self.has_budget && self.has_payees
193 }
194
195 pub fn summary(&self) -> String {
197 let mut present = Vec::new();
198 let mut missing = Vec::new();
199
200 if self.has_accounts {
201 present.push("accounts");
202 } else {
203 missing.push("accounts");
204 }
205 if self.has_transactions {
206 present.push("transactions");
207 } else {
208 missing.push("transactions");
209 }
210 if self.has_budget {
211 present.push("budget");
212 } else {
213 missing.push("budget");
214 }
215 if self.has_payees {
216 present.push("payees");
217 } else {
218 missing.push("payees");
219 }
220
221 if missing.is_empty() {
222 format!("Complete backup (v{})", self.schema_version)
223 } else {
224 format!(
225 "Partial backup (v{}): has {}, missing {}",
226 self.schema_version,
227 present.join(", "),
228 missing.join(", ")
229 )
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::backup::manager::BackupManager;
238 use crate::config::settings::BackupRetention;
239 use tempfile::TempDir;
240
241 fn create_test_env() -> (RestoreManager, BackupManager, TempDir) {
242 let temp_dir = TempDir::new().unwrap();
243 let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
244 paths.ensure_directories().unwrap();
245
246 let retention = BackupRetention::default();
247 let backup_manager = BackupManager::new(paths.clone(), retention);
248 let restore_manager = RestoreManager::new(paths);
249
250 (restore_manager, backup_manager, temp_dir)
251 }
252
253 #[test]
254 fn test_restore_from_backup() {
255 let (restore_manager, backup_manager, _temp) = create_test_env();
256
257 let backup_path = backup_manager.create_backup().unwrap();
259
260 let result = restore_manager.restore_from_file(&backup_path).unwrap();
262
263 assert!(result.accounts_restored);
264 assert!(result.transactions_restored);
265 assert!(result.budget_restored);
266 assert!(result.payees_restored);
267 }
268
269 #[test]
270 fn test_validate_backup() {
271 let (restore_manager, backup_manager, _temp) = create_test_env();
272
273 let backup_path = backup_manager.create_backup().unwrap();
275
276 let result = restore_manager.validate_backup(&backup_path).unwrap();
278
279 assert!(result.is_valid);
280 assert_eq!(result.schema_version, 1);
281 }
282
283 #[test]
284 fn test_restore_result_summary() {
285 let result = RestoreResult {
286 schema_version: 1,
287 backup_date: chrono::Utc::now(),
288 accounts_restored: true,
289 transactions_restored: true,
290 budget_restored: false,
291 payees_restored: true,
292 };
293
294 assert!(!result.all_restored());
295 assert!(result.summary().contains("accounts"));
296 assert!(result.summary().contains("transactions"));
297 assert!(!result.summary().contains("budget"));
298 }
299
300 #[test]
301 fn test_validation_result_summary() {
302 let result = ValidationResult {
303 is_valid: true,
304 schema_version: 1,
305 backup_date: chrono::Utc::now(),
306 has_accounts: true,
307 has_transactions: true,
308 has_budget: true,
309 has_payees: true,
310 };
311
312 assert!(result.is_complete());
313 assert!(result.summary().contains("Complete backup"));
314 }
315
316 #[test]
317 fn test_restore_creates_files() {
318 let (restore_manager, backup_manager, temp) = create_test_env();
319
320 let backup_path = backup_manager.create_backup().unwrap();
322
323 let data_dir = temp.path().join("data");
325 if data_dir.exists() {
326 fs::remove_dir_all(&data_dir).unwrap();
327 }
328
329 restore_manager.restore_from_file(&backup_path).unwrap();
331
332 assert!(restore_manager.paths.accounts_file().exists());
334 assert!(restore_manager.paths.transactions_file().exists());
335 assert!(restore_manager.paths.budget_file().exists());
336 assert!(restore_manager.paths.payees_file().exists());
337 }
338}