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
13pub struct RestoreManager {
15 paths: EnvelopePaths,
16}
17
18impl RestoreManager {
19 pub fn new(paths: EnvelopePaths) -> Self {
21 Self { paths }
22 }
23
24 pub fn restore_from_file(&self, backup_path: &Path) -> EnvelopeResult<RestoreResult> {
29 let contents = fs::read_to_string(backup_path)
31 .map_err(|e| EnvelopeError::Io(format!("Failed to read backup file: {}", e)))?;
32
33 let archive: BackupArchive = serde_json::from_str(&contents)
34 .map_err(|e| EnvelopeError::Json(format!("Failed to parse backup file: {}", e)))?;
35
36 self.restore_from_archive(&archive)
37 }
38
39 pub fn restore_from_archive(&self, archive: &BackupArchive) -> EnvelopeResult<RestoreResult> {
41 self.paths.ensure_directories()?;
43
44 let mut result = RestoreResult::default();
45
46 if !archive.accounts.is_null() {
48 let json = serde_json::to_string_pretty(&archive.accounts)
49 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize accounts: {}", e)))?;
50 fs::write(self.paths.accounts_file(), json)
51 .map_err(|e| EnvelopeError::Io(format!("Failed to restore accounts: {}", e)))?;
52 result.accounts_restored = true;
53 }
54
55 if !archive.transactions.is_null() {
57 let json = serde_json::to_string_pretty(&archive.transactions).map_err(|e| {
58 EnvelopeError::Json(format!("Failed to serialize transactions: {}", e))
59 })?;
60 fs::write(self.paths.transactions_file(), json)
61 .map_err(|e| EnvelopeError::Io(format!("Failed to restore transactions: {}", e)))?;
62 result.transactions_restored = true;
63 }
64
65 if !archive.budget.is_null() {
67 let json = serde_json::to_string_pretty(&archive.budget)
68 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize budget: {}", e)))?;
69 fs::write(self.paths.budget_file(), json)
70 .map_err(|e| EnvelopeError::Io(format!("Failed to restore budget: {}", e)))?;
71 result.budget_restored = true;
72 }
73
74 if !archive.payees.is_null() {
76 let json = serde_json::to_string_pretty(&archive.payees)
77 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize payees: {}", e)))?;
78 fs::write(self.paths.payees_file(), json)
79 .map_err(|e| EnvelopeError::Io(format!("Failed to restore payees: {}", e)))?;
80 result.payees_restored = true;
81 }
82
83 result.schema_version = archive.schema_version;
84 result.backup_date = archive.created_at;
85
86 Ok(result)
87 }
88
89 pub fn validate_backup(&self, backup_path: &Path) -> EnvelopeResult<ValidationResult> {
91 let contents = fs::read_to_string(backup_path)
92 .map_err(|e| EnvelopeError::Io(format!("Failed to read backup file: {}", e)))?;
93
94 let archive: BackupArchive = serde_json::from_str(&contents)
95 .map_err(|e| EnvelopeError::Json(format!("Failed to parse backup file: {}", e)))?;
96
97 Ok(ValidationResult {
98 is_valid: true,
99 schema_version: archive.schema_version,
100 backup_date: archive.created_at,
101 has_accounts: !archive.accounts.is_null() && archive.accounts.is_object(),
102 has_transactions: !archive.transactions.is_null() && archive.transactions.is_object(),
103 has_budget: !archive.budget.is_null() && archive.budget.is_object(),
104 has_payees: !archive.payees.is_null() && archive.payees.is_object(),
105 })
106 }
107}
108
109#[derive(Debug, Default)]
111pub struct RestoreResult {
112 pub schema_version: u32,
114 pub backup_date: chrono::DateTime<chrono::Utc>,
116 pub accounts_restored: bool,
118 pub transactions_restored: bool,
120 pub budget_restored: bool,
122 pub payees_restored: bool,
124}
125
126impl RestoreResult {
127 pub fn all_restored(&self) -> bool {
129 self.accounts_restored
130 && self.transactions_restored
131 && self.budget_restored
132 && self.payees_restored
133 }
134
135 pub fn summary(&self) -> String {
137 let mut parts = Vec::new();
138 if self.accounts_restored {
139 parts.push("accounts");
140 }
141 if self.transactions_restored {
142 parts.push("transactions");
143 }
144 if self.budget_restored {
145 parts.push("budget");
146 }
147 if self.payees_restored {
148 parts.push("payees");
149 }
150 format!("Restored: {}", parts.join(", "))
151 }
152}
153
154#[derive(Debug)]
156pub struct ValidationResult {
157 pub is_valid: bool,
159 pub schema_version: u32,
161 pub backup_date: chrono::DateTime<chrono::Utc>,
163 pub has_accounts: bool,
165 pub has_transactions: bool,
167 pub has_budget: bool,
169 pub has_payees: bool,
171}
172
173impl ValidationResult {
174 pub fn is_complete(&self) -> bool {
176 self.has_accounts && self.has_transactions && self.has_budget && self.has_payees
177 }
178
179 pub fn summary(&self) -> String {
181 let mut present = Vec::new();
182 let mut missing = Vec::new();
183
184 if self.has_accounts {
185 present.push("accounts");
186 } else {
187 missing.push("accounts");
188 }
189 if self.has_transactions {
190 present.push("transactions");
191 } else {
192 missing.push("transactions");
193 }
194 if self.has_budget {
195 present.push("budget");
196 } else {
197 missing.push("budget");
198 }
199 if self.has_payees {
200 present.push("payees");
201 } else {
202 missing.push("payees");
203 }
204
205 if missing.is_empty() {
206 format!("Complete backup (v{})", self.schema_version)
207 } else {
208 format!(
209 "Partial backup (v{}): has {}, missing {}",
210 self.schema_version,
211 present.join(", "),
212 missing.join(", ")
213 )
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::backup::manager::BackupManager;
222 use crate::config::settings::BackupRetention;
223 use tempfile::TempDir;
224
225 fn create_test_env() -> (RestoreManager, BackupManager, TempDir) {
226 let temp_dir = TempDir::new().unwrap();
227 let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
228 paths.ensure_directories().unwrap();
229
230 let retention = BackupRetention::default();
231 let backup_manager = BackupManager::new(paths.clone(), retention);
232 let restore_manager = RestoreManager::new(paths);
233
234 (restore_manager, backup_manager, temp_dir)
235 }
236
237 #[test]
238 fn test_restore_from_backup() {
239 let (restore_manager, backup_manager, _temp) = create_test_env();
240
241 let backup_path = backup_manager.create_backup().unwrap();
243
244 let result = restore_manager.restore_from_file(&backup_path).unwrap();
246
247 assert!(result.accounts_restored);
248 assert!(result.transactions_restored);
249 assert!(result.budget_restored);
250 assert!(result.payees_restored);
251 }
252
253 #[test]
254 fn test_validate_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.validate_backup(&backup_path).unwrap();
262
263 assert!(result.is_valid);
264 assert_eq!(result.schema_version, 1);
265 }
266
267 #[test]
268 fn test_restore_result_summary() {
269 let result = RestoreResult {
270 schema_version: 1,
271 backup_date: chrono::Utc::now(),
272 accounts_restored: true,
273 transactions_restored: true,
274 budget_restored: false,
275 payees_restored: true,
276 };
277
278 assert!(!result.all_restored());
279 assert!(result.summary().contains("accounts"));
280 assert!(result.summary().contains("transactions"));
281 assert!(!result.summary().contains("budget"));
282 }
283
284 #[test]
285 fn test_validation_result_summary() {
286 let result = ValidationResult {
287 is_valid: true,
288 schema_version: 1,
289 backup_date: chrono::Utc::now(),
290 has_accounts: true,
291 has_transactions: true,
292 has_budget: true,
293 has_payees: true,
294 };
295
296 assert!(result.is_complete());
297 assert!(result.summary().contains("Complete backup"));
298 }
299
300 #[test]
301 fn test_restore_creates_files() {
302 let (restore_manager, backup_manager, temp) = create_test_env();
303
304 let backup_path = backup_manager.create_backup().unwrap();
306
307 let data_dir = temp.path().join("data");
309 if data_dir.exists() {
310 fs::remove_dir_all(&data_dir).unwrap();
311 }
312
313 restore_manager.restore_from_file(&backup_path).unwrap();
315
316 assert!(restore_manager.paths.accounts_file().exists());
318 assert!(restore_manager.paths.transactions_file().exists());
319 assert!(restore_manager.paths.budget_file().exists());
320 assert!(restore_manager.paths.payees_file().exists());
321 }
322}