1use serde::{Deserialize, Serialize};
4
5pub type BackupResult<T> = Result<T, BackupError>;
7
8#[derive(Debug, Clone)]
10pub enum BackupError {
11 ConnectionFailed { store: String, message: String },
13 BackupFailed { store: String, message: String },
15 RestoreFailed { store: String, message: String },
17 VerificationFailed { store: String, message: String },
19 StorageError { message: String },
21 NotFound {
23 store: String,
24 backup_id: String,
25 },
26 Timeout { store: String },
28}
29
30impl std::fmt::Display for BackupError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::ConnectionFailed { store, message } => {
34 write!(f, "Failed to connect to {}: {}", store, message)
35 },
36 Self::BackupFailed { store, message } => {
37 write!(f, "Backup failed for {}: {}", store, message)
38 },
39 Self::RestoreFailed { store, message } => {
40 write!(f, "Restore failed for {}: {}", store, message)
41 },
42 Self::VerificationFailed { store, message } => {
43 write!(f, "Verification failed for {}: {}", store, message)
44 },
45 Self::StorageError { message } => write!(f, "Storage error: {}", message),
46 Self::NotFound { store, backup_id } => {
47 write!(f, "Backup not found for {}: {}", store, backup_id)
48 },
49 Self::Timeout { store } => write!(f, "Backup timeout for {}", store),
50 }
51 }
52}
53
54impl std::error::Error for BackupError {}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct BackupInfo {
59 pub backup_id: String,
61
62 pub store_name: String,
64
65 pub timestamp: i64,
67
68 pub size_bytes: u64,
70
71 pub verified: bool,
73
74 pub compression: Option<String>,
76
77 pub metadata: std::collections::HashMap<String, String>,
79}
80
81impl BackupInfo {
82 pub fn timestamp_display(&self) -> String {
84 let secs = self.timestamp;
85 let duration = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64);
86 match duration.elapsed() {
87 Ok(_) => format_timestamp(secs),
88 Err(_) => format_timestamp(secs),
89 }
90 }
91
92 pub fn size_display(&self) -> String {
94 format_size_bytes(self.size_bytes)
95 }
96}
97
98#[async_trait::async_trait]
100pub trait BackupProvider: Send + Sync {
101 fn name(&self) -> &str;
103
104 async fn health_check(&self) -> BackupResult<()>;
106
107 async fn backup(&self) -> BackupResult<BackupInfo>;
111
112 async fn restore(&self, backup_id: &str, verify: bool) -> BackupResult<()>;
118
119 async fn list_backups(&self) -> BackupResult<Vec<BackupInfo>>;
121
122 async fn get_backup(&self, backup_id: &str) -> BackupResult<BackupInfo>;
124
125 async fn delete_backup(&self, backup_id: &str) -> BackupResult<()>;
127
128 async fn verify_backup(&self, backup_id: &str) -> BackupResult<()>;
130
131 async fn get_storage_usage(&self) -> BackupResult<StorageUsage>;
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct StorageUsage {
138 pub total_bytes: u64,
140
141 pub backup_count: u32,
143
144 pub oldest_backup_timestamp: Option<i64>,
146
147 pub newest_backup_timestamp: Option<i64>,
149}
150
151fn format_timestamp(secs: i64) -> String {
154 format!("{}", secs)
156}
157
158fn format_size_bytes(bytes: u64) -> String {
159 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
160 let mut size = bytes as f64;
161 let mut unit_idx = 0;
162
163 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
164 size /= 1024.0;
165 unit_idx += 1;
166 }
167
168 format!("{:.2} {}", size, UNITS[unit_idx])
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_format_size_bytes() {
177 assert_eq!(format_size_bytes(100), "100.00 B");
178 assert_eq!(format_size_bytes(1024), "1.00 KB");
179 assert_eq!(format_size_bytes(1024 * 1024), "1.00 MB");
180 assert_eq!(format_size_bytes(1024 * 1024 * 1024), "1.00 GB");
181 }
182
183 #[test]
184 fn test_backup_info_display() {
185 let info = BackupInfo {
186 backup_id: "backup-123".to_string(),
187 store_name: "postgres".to_string(),
188 timestamp: 1_000_000,
189 size_bytes: 1024 * 1024,
190 verified: true,
191 compression: Some("gzip".to_string()),
192 metadata: Default::default(),
193 };
194
195 assert_eq!(info.size_display(), "1.00 MB");
196 assert!(!info.timestamp_display().is_empty());
197 }
198
199 #[test]
200 fn test_backup_error_display() {
201 let err = BackupError::BackupFailed {
202 store: "postgres".to_string(),
203 message: "Connection timeout".to_string(),
204 };
205 assert!(err.to_string().contains("postgres"));
206 assert!(err.to_string().contains("Connection timeout"));
207 }
208}