envelope_cli/storage/
mod.rs1pub mod accounts;
8pub mod budget;
9pub mod categories;
10pub mod file_io;
11pub mod income;
12pub mod init;
13pub mod payees;
14pub mod targets;
15pub mod transactions;
16
17pub use accounts::AccountRepository;
18pub use budget::BudgetRepository;
19pub use categories::CategoryRepository;
20pub use file_io::{read_json, write_json_atomic};
21pub use income::IncomeRepository;
22pub use init::initialize_storage;
23pub use payees::PayeeRepository;
24pub use targets::TargetRepository;
25pub use transactions::TransactionRepository;
26
27use std::path::{Path, PathBuf};
28
29use crate::audit::{AuditEntry, AuditLogger, EntityType};
30use crate::backup::{BackupManager, RestoreManager, RestoreResult};
31use crate::config::paths::EnvelopePaths;
32use crate::config::settings::BackupRetention;
33use crate::error::{EnvelopeError, EnvelopeResult};
34
35pub struct Storage {
38 paths: EnvelopePaths,
39 pub accounts: AccountRepository,
40 pub transactions: TransactionRepository,
41 pub categories: CategoryRepository,
42 pub budget: BudgetRepository,
43 pub payees: PayeeRepository,
44 pub targets: TargetRepository,
45 pub income: IncomeRepository,
46 audit: AuditLogger,
47}
48
49impl Storage {
50 pub fn new(paths: EnvelopePaths) -> Result<Self, EnvelopeError> {
52 paths.ensure_directories()?;
54
55 let audit = AuditLogger::new(paths.audit_log());
56
57 Ok(Self {
58 accounts: AccountRepository::new(paths.accounts_file()),
59 transactions: TransactionRepository::new(paths.transactions_file()),
60 categories: CategoryRepository::new(paths.budget_file()),
61 budget: BudgetRepository::new(paths.allocations_file()),
62 payees: PayeeRepository::new(paths.payees_file()),
63 targets: TargetRepository::new(paths.targets_file()),
64 income: IncomeRepository::new(paths.income_file()),
65 audit,
66 paths,
67 })
68 }
69
70 pub fn paths(&self) -> &EnvelopePaths {
72 &self.paths
73 }
74
75 pub fn audit(&self) -> &AuditLogger {
77 &self.audit
78 }
79
80 pub fn log_audit(&self, entry: &AuditEntry) -> EnvelopeResult<()> {
82 self.audit.log(entry)
83 }
84
85 pub fn log_create<T: serde::Serialize>(
87 &self,
88 entity_type: EntityType,
89 entity_id: impl Into<String>,
90 entity_name: Option<String>,
91 entity: &T,
92 ) -> EnvelopeResult<()> {
93 let entry = AuditEntry::create(entity_type, entity_id, entity_name, entity);
94 self.audit.log(&entry)
95 }
96
97 pub fn log_update<T: serde::Serialize>(
99 &self,
100 entity_type: EntityType,
101 entity_id: impl Into<String>,
102 entity_name: Option<String>,
103 before: &T,
104 after: &T,
105 diff_summary: Option<String>,
106 ) -> EnvelopeResult<()> {
107 let entry = AuditEntry::update(
108 entity_type,
109 entity_id,
110 entity_name,
111 before,
112 after,
113 diff_summary,
114 );
115 self.audit.log(&entry)
116 }
117
118 pub fn log_delete<T: serde::Serialize>(
120 &self,
121 entity_type: EntityType,
122 entity_id: impl Into<String>,
123 entity_name: Option<String>,
124 entity: &T,
125 ) -> EnvelopeResult<()> {
126 let entry = AuditEntry::delete(entity_type, entity_id, entity_name, entity);
127 self.audit.log(&entry)
128 }
129
130 pub fn read_audit_log(&self, count: usize) -> EnvelopeResult<Vec<AuditEntry>> {
132 self.audit.read_recent(count)
133 }
134
135 pub fn load_all(&mut self) -> Result<(), EnvelopeError> {
137 self.accounts.load()?;
138 self.transactions.load()?;
139 self.categories.load()?;
140 self.budget.load()?;
141 self.payees.load()?;
142 self.targets.load()?;
143 self.income.load()?;
144 Ok(())
145 }
146
147 pub fn save_all(&self) -> Result<(), EnvelopeError> {
149 self.accounts.save()?;
150 self.transactions.save()?;
151 self.categories.save()?;
152 self.budget.save()?;
153 self.payees.save()?;
154 self.targets.save()?;
155 self.income.save()?;
156 Ok(())
157 }
158
159 pub fn is_initialized(&self) -> bool {
161 self.paths.settings_file().exists()
162 }
163
164 pub fn create_backup(&self) -> EnvelopeResult<PathBuf> {
169 let retention = BackupRetention::default();
170 let manager = BackupManager::new(self.paths.clone(), retention);
171 manager.create_backup()
172 }
173
174 pub fn create_backup_with_retention(
176 &self,
177 retention: BackupRetention,
178 ) -> EnvelopeResult<(PathBuf, Vec<PathBuf>)> {
179 let manager = BackupManager::new(self.paths.clone(), retention);
180 manager.create_backup_with_retention()
181 }
182
183 pub fn restore_from_backup(&mut self, backup_path: &Path) -> EnvelopeResult<RestoreResult> {
188 let restore_manager = RestoreManager::new(self.paths.clone());
189 let result = restore_manager.restore_from_file(backup_path)?;
190
191 self.load_all()?;
193
194 Ok(result)
195 }
196
197 pub fn backup_manager(&self, retention: BackupRetention) -> BackupManager {
199 BackupManager::new(self.paths.clone(), retention)
200 }
201
202 pub fn backup_before_destructive(&self) -> EnvelopeResult<Option<PathBuf>> {
213 let retention = BackupRetention::default();
214 let manager = BackupManager::new(self.paths.clone(), retention);
215
216 if let Some(latest) = manager.get_latest_backup()? {
218 let age = chrono::Utc::now().signed_duration_since(latest.created_at);
219
220 if age.num_seconds() < 60 {
222 return Ok(None);
223 }
224 }
225
226 let path = manager.create_backup()?;
228 Ok(Some(path))
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use tempfile::TempDir;
236
237 #[test]
238 fn test_storage_creation() {
239 let temp_dir = TempDir::new().unwrap();
240 let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
241 let storage = Storage::new(paths).unwrap();
242
243 assert!(temp_dir.path().join("data").exists());
244 assert!(temp_dir.path().join("backups").exists());
245 assert!(!storage.is_initialized());
246 }
247}