Skip to main content

fraiseql_server/backup/
backup_manager.rs

1//! Backup manager orchestrating all backup providers.
2
3use std::{collections::HashMap, sync::Arc};
4
5use tokio::sync::RwLock;
6
7use super::{
8    backup_config::{BackupConfig, BackupStatus},
9    backup_provider::{BackupError, BackupProvider, BackupResult},
10};
11
12/// Manages backups across all data stores.
13pub struct BackupManager {
14    /// Registered backup providers
15    providers: Arc<RwLock<HashMap<String, Arc<dyn BackupProvider>>>>,
16
17    /// Backup status cache
18    status_cache: Arc<RwLock<HashMap<String, BackupStatus>>>,
19
20    /// Backup configs
21    configs: HashMap<String, BackupConfig>,
22}
23
24impl BackupManager {
25    /// Create new backup manager.
26    pub fn new(configs: HashMap<String, BackupConfig>) -> Self {
27        Self {
28            providers: Arc::new(RwLock::new(HashMap::new())),
29            status_cache: Arc::new(RwLock::new(HashMap::new())),
30            configs,
31        }
32    }
33
34    /// Register a backup provider.
35    pub async fn register_provider(
36        &self,
37        name: String,
38        provider: Arc<dyn BackupProvider>,
39    ) -> Result<(), String> {
40        let mut providers = self.providers.write().await;
41
42        if providers.contains_key(&name) {
43            return Err(format!("Provider '{}' already registered", name));
44        }
45
46        providers.insert(name, provider);
47        Ok(())
48    }
49
50    /// Start backup scheduler (spawns background task).
51    pub async fn start(&self) -> Result<(), String> {
52        // In production, would spawn scheduler task
53        // that reads BackupConfig and triggers backups on schedule
54
55        // For now, just validate all providers are healthy
56        let providers = self.providers.read().await;
57        for (name, provider) in providers.iter() {
58            match provider.health_check().await {
59                Ok(_) => {
60                    eprintln!("✓ Backup provider '{}' healthy", name);
61                },
62                Err(e) => {
63                    eprintln!("✗ Backup provider '{}' failed health check: {:?}", name, e);
64                },
65            }
66        }
67
68        Ok(())
69    }
70
71    /// Create a backup for a specific provider.
72    pub async fn backup(&self, provider_name: &str) -> BackupResult<()> {
73        let providers = self.providers.read().await;
74
75        let provider = providers.get(provider_name).ok_or_else(|| BackupError::BackupFailed {
76            store:   provider_name.to_string(),
77            message: "Provider not registered".to_string(),
78        })?;
79
80        let config = self.configs.get(provider_name).ok_or_else(|| BackupError::BackupFailed {
81            store:   provider_name.to_string(),
82            message: "No configuration found".to_string(),
83        })?;
84
85        if !config.enabled {
86            return Err(BackupError::BackupFailed {
87                store:   provider_name.to_string(),
88                message: "Backups disabled".to_string(),
89            });
90        }
91
92        // Execute backup with timeout
93        let backup_future = provider.backup();
94        let timeout_duration = config.timeout();
95
96        let result = tokio::time::timeout(timeout_duration, backup_future).await.map_err(|_| {
97            BackupError::Timeout {
98                store: provider_name.to_string(),
99            }
100        })?;
101
102        match result {
103            Ok(backup_info) => {
104                // Update status cache
105                let mut cache = self.status_cache.write().await;
106                cache.insert(
107                    provider_name.to_string(),
108                    BackupStatus {
109                        store_name:             provider_name.to_string(),
110                        enabled:                config.enabled,
111                        last_successful_backup: Some(backup_info.timestamp),
112                        last_backup_size:       Some(backup_info.size_bytes),
113                        available_backups:      1, // Would count in production
114                        last_error:             None,
115                        status:                 "healthy".to_string(),
116                    },
117                );
118                Ok(())
119            },
120            Err(e) => {
121                // Update status cache with error
122                let mut cache = self.status_cache.write().await;
123                cache.insert(
124                    provider_name.to_string(),
125                    BackupStatus {
126                        store_name:             provider_name.to_string(),
127                        enabled:                config.enabled,
128                        last_successful_backup: None,
129                        last_backup_size:       None,
130                        available_backups:      0,
131                        last_error:             Some(e.to_string()),
132                        status:                 "error".to_string(),
133                    },
134                );
135                Err(e)
136            },
137        }
138    }
139
140    /// Get backup status for all providers.
141    pub async fn get_status(&self) -> HashMap<String, BackupStatus> {
142        self.status_cache.read().await.clone()
143    }
144
145    /// Get backup status for a specific provider.
146    pub async fn get_provider_status(&self, provider_name: &str) -> Option<BackupStatus> {
147        self.status_cache.read().await.get(provider_name).cloned()
148    }
149
150    /// Restore from a backup.
151    pub async fn restore(&self, provider_name: &str, backup_id: &str) -> BackupResult<()> {
152        let providers = self.providers.read().await;
153
154        let provider = providers.get(provider_name).ok_or_else(|| BackupError::RestoreFailed {
155            store:   provider_name.to_string(),
156            message: "Provider not registered".to_string(),
157        })?;
158
159        let config = self.configs.get(provider_name).ok_or_else(|| BackupError::RestoreFailed {
160            store:   provider_name.to_string(),
161            message: "No configuration found".to_string(),
162        })?;
163
164        provider.restore(backup_id, config.verify_after_backup).await
165    }
166
167    /// List backups for a provider.
168    pub async fn list_backups(&self, provider_name: &str) -> BackupResult<Vec<String>> {
169        let providers = self.providers.read().await;
170
171        let provider = providers.get(provider_name).ok_or_else(|| BackupError::BackupFailed {
172            store:   provider_name.to_string(),
173            message: "Provider not registered".to_string(),
174        })?;
175
176        let backups = provider.list_backups().await?;
177        Ok(backups.iter().map(|b| b.backup_id.clone()).collect())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::backup::backup_provider::{BackupInfo, StorageUsage};
185
186    /// Mock backup provider for testing
187    struct MockBackupProvider {
188        name: String,
189    }
190
191    #[async_trait::async_trait]
192    impl BackupProvider for MockBackupProvider {
193        fn name(&self) -> &str {
194            &self.name
195        }
196
197        async fn health_check(&self) -> BackupResult<()> {
198            Ok(())
199        }
200
201        async fn backup(&self) -> BackupResult<BackupInfo> {
202            Ok(BackupInfo {
203                backup_id:   format!("{}-backup-1", self.name),
204                store_name:  self.name.clone(),
205                timestamp:   1_000_000,
206                size_bytes:  1024 * 1024,
207                verified:    true,
208                compression: Some("gzip".to_string()),
209                metadata:    Default::default(),
210            })
211        }
212
213        async fn restore(&self, _backup_id: &str, _verify: bool) -> BackupResult<()> {
214            Ok(())
215        }
216
217        async fn list_backups(&self) -> BackupResult<Vec<BackupInfo>> {
218            Ok(vec![BackupInfo {
219                backup_id:   format!("{}-backup-1", self.name),
220                store_name:  self.name.clone(),
221                timestamp:   1_000_000,
222                size_bytes:  1024 * 1024,
223                verified:    true,
224                compression: Some("gzip".to_string()),
225                metadata:    Default::default(),
226            }])
227        }
228
229        async fn get_backup(&self, backup_id: &str) -> BackupResult<BackupInfo> {
230            Ok(BackupInfo {
231                backup_id:   backup_id.to_string(),
232                store_name:  self.name.clone(),
233                timestamp:   1_000_000,
234                size_bytes:  1024 * 1024,
235                verified:    true,
236                compression: Some("gzip".to_string()),
237                metadata:    Default::default(),
238            })
239        }
240
241        async fn delete_backup(&self, _backup_id: &str) -> BackupResult<()> {
242            Ok(())
243        }
244
245        async fn verify_backup(&self, _backup_id: &str) -> BackupResult<()> {
246            Ok(())
247        }
248
249        async fn get_storage_usage(&self) -> BackupResult<StorageUsage> {
250            Ok(StorageUsage {
251                total_bytes:             1024 * 1024 * 100,
252                backup_count:            7,
253                oldest_backup_timestamp: Some(999_999),
254                newest_backup_timestamp: Some(1_000_000),
255            })
256        }
257    }
258
259    #[tokio::test]
260    async fn test_register_provider() {
261        let configs = HashMap::new();
262        let manager = BackupManager::new(configs);
263
264        let provider = Arc::new(MockBackupProvider {
265            name: "postgres".to_string(),
266        });
267
268        assert!(manager.register_provider("postgres".to_string(), provider).await.is_ok());
269    }
270
271    #[tokio::test]
272    async fn test_duplicate_provider() {
273        let configs = HashMap::new();
274        let manager = BackupManager::new(configs);
275
276        let provider = Arc::new(MockBackupProvider {
277            name: "postgres".to_string(),
278        });
279
280        manager.register_provider("postgres".to_string(), provider.clone()).await.ok();
281        let result = manager.register_provider("postgres".to_string(), provider).await;
282
283        assert!(result.is_err());
284    }
285
286    #[tokio::test]
287    async fn test_backup_updates_status() {
288        let mut configs = HashMap::new();
289        configs.insert("postgres".to_string(), BackupConfig::postgres_default());
290
291        let manager = BackupManager::new(configs);
292
293        let provider = Arc::new(MockBackupProvider {
294            name: "postgres".to_string(),
295        });
296
297        manager.register_provider("postgres".to_string(), provider).await.unwrap();
298
299        manager.backup("postgres").await.unwrap();
300
301        let status = manager.get_provider_status("postgres").await;
302        assert!(status.is_some());
303        assert_eq!(status.unwrap().status, "healthy");
304    }
305
306    #[tokio::test]
307    async fn test_list_backups() {
308        let configs = HashMap::new();
309        let manager = BackupManager::new(configs);
310
311        let provider = Arc::new(MockBackupProvider {
312            name: "postgres".to_string(),
313        });
314
315        manager.register_provider("postgres".to_string(), provider).await.unwrap();
316
317        let backups = manager.list_backups("postgres").await.unwrap();
318        assert_eq!(backups.len(), 1);
319        assert!(backups[0].contains("backup-1"));
320    }
321}