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