use serde::{Deserialize, Serialize};
pub type BackupResult<T> = Result<T, BackupError>;
#[derive(Debug, Clone)]
pub enum BackupError {
ConnectionFailed { store: String, message: String },
BackupFailed { store: String, message: String },
RestoreFailed { store: String, message: String },
VerificationFailed { store: String, message: String },
StorageError { message: String },
NotFound {
store: String,
backup_id: String,
},
Timeout { store: String },
}
impl std::fmt::Display for BackupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConnectionFailed { store, message } => {
write!(f, "Failed to connect to {}: {}", store, message)
},
Self::BackupFailed { store, message } => {
write!(f, "Backup failed for {}: {}", store, message)
},
Self::RestoreFailed { store, message } => {
write!(f, "Restore failed for {}: {}", store, message)
},
Self::VerificationFailed { store, message } => {
write!(f, "Verification failed for {}: {}", store, message)
},
Self::StorageError { message } => write!(f, "Storage error: {}", message),
Self::NotFound { store, backup_id } => {
write!(f, "Backup not found for {}: {}", store, backup_id)
},
Self::Timeout { store } => write!(f, "Backup timeout for {}", store),
}
}
}
impl std::error::Error for BackupError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupInfo {
pub backup_id: String,
pub store_name: String,
pub timestamp: i64,
pub size_bytes: u64,
pub verified: bool,
pub compression: Option<String>,
pub metadata: std::collections::HashMap<String, String>,
}
impl BackupInfo {
pub fn timestamp_display(&self) -> String {
let secs = self.timestamp;
let duration = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64);
match duration.elapsed() {
Ok(_) => format_timestamp(secs),
Err(_) => format_timestamp(secs),
}
}
pub fn size_display(&self) -> String {
format_size_bytes(self.size_bytes)
}
}
#[async_trait::async_trait]
pub trait BackupProvider: Send + Sync {
fn name(&self) -> &str;
async fn health_check(&self) -> BackupResult<()>;
async fn backup(&self) -> BackupResult<BackupInfo>;
async fn restore(&self, backup_id: &str, verify: bool) -> BackupResult<()>;
async fn list_backups(&self) -> BackupResult<Vec<BackupInfo>>;
async fn get_backup(&self, backup_id: &str) -> BackupResult<BackupInfo>;
async fn delete_backup(&self, backup_id: &str) -> BackupResult<()>;
async fn verify_backup(&self, backup_id: &str) -> BackupResult<()>;
async fn get_storage_usage(&self) -> BackupResult<StorageUsage>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageUsage {
pub total_bytes: u64,
pub backup_count: u32,
pub oldest_backup_timestamp: Option<i64>,
pub newest_backup_timestamp: Option<i64>,
}
fn format_timestamp(secs: i64) -> String {
format!("{}", secs)
}
fn format_size_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = bytes as f64;
let mut unit_idx = 0;
while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
size /= 1024.0;
unit_idx += 1;
}
format!("{:.2} {}", size, UNITS[unit_idx])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_size_bytes() {
assert_eq!(format_size_bytes(100), "100.00 B");
assert_eq!(format_size_bytes(1024), "1.00 KB");
assert_eq!(format_size_bytes(1024 * 1024), "1.00 MB");
assert_eq!(format_size_bytes(1024 * 1024 * 1024), "1.00 GB");
}
#[test]
fn test_backup_info_display() {
let info = BackupInfo {
backup_id: "backup-123".to_string(),
store_name: "postgres".to_string(),
timestamp: 1_000_000,
size_bytes: 1024 * 1024,
verified: true,
compression: Some("gzip".to_string()),
metadata: Default::default(),
};
assert_eq!(info.size_display(), "1.00 MB");
assert!(!info.timestamp_display().is_empty());
}
#[test]
fn test_backup_error_display() {
let err = BackupError::BackupFailed {
store: "postgres".to_string(),
message: "Connection timeout".to_string(),
};
assert!(err.to_string().contains("postgres"));
assert!(err.to_string().contains("Connection timeout"));
}
}