use async_trait::async_trait; use cm_dashboard_shared::{AgentData, BackupData, BackupDiskData}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::Path; use tracing::debug; use super::{Collector, CollectorError}; /// Backup collector that reads backup status from TOML files with structured data output pub struct BackupCollector { /// Path to backup status file status_file_path: String, } impl BackupCollector { pub fn new() -> Self { Self { status_file_path: "/var/lib/backup/backup-status.toml".to_string(), } } /// Read backup status from TOML file async fn read_backup_status(&self) -> Result, CollectorError> { if !Path::new(&self.status_file_path).exists() { debug!("Backup status file not found: {}", self.status_file_path); return Ok(None); } let content = fs::read_to_string(&self.status_file_path) .map_err(|e| CollectorError::SystemRead { path: self.status_file_path.clone(), error: e.to_string(), })?; let status: BackupStatusToml = toml::from_str(&content) .map_err(|e| CollectorError::Parse { value: content.clone(), error: format!("Failed to parse backup status TOML: {}", e), })?; Ok(Some(status)) } /// Convert BackupStatusToml to BackupData and populate AgentData async fn populate_backup_data(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> { if let Some(backup_status) = self.read_backup_status().await? { // Use raw start_time string from TOML // Extract disk information let repository_disk = if let Some(disk_space) = &backup_status.disk_space { Some(BackupDiskData { serial: backup_status.disk_serial_number.clone().unwrap_or_else(|| "Unknown".to_string()), usage_percent: disk_space.usage_percent as f32, used_gb: disk_space.used_gb as f32, total_gb: disk_space.total_gb as f32, wear_percent: backup_status.disk_wear_percent, temperature_celsius: None, // Not available in current TOML }) } else if let Some(serial) = &backup_status.disk_serial_number { // Fallback: create minimal disk info if we have serial but no disk_space Some(BackupDiskData { serial: serial.clone(), usage_percent: 0.0, used_gb: 0.0, total_gb: 0.0, wear_percent: backup_status.disk_wear_percent, temperature_celsius: None, }) } else { None }; // Calculate total repository size from services let total_size_gb = backup_status.services .values() .map(|service| service.repo_size_bytes as f32 / (1024.0 * 1024.0 * 1024.0)) .sum::(); let backup_data = BackupData { status: backup_status.status, total_size_gb: Some(total_size_gb), repository_health: Some("ok".to_string()), // Derive from status if needed repository_disk, last_backup_size_gb: None, // Not available in current TOML format start_time_raw: Some(backup_status.start_time), }; agent_data.backup = backup_data; } else { // No backup status available - set default values agent_data.backup = BackupData { status: "unavailable".to_string(), total_size_gb: None, repository_health: None, repository_disk: None, last_backup_size_gb: None, start_time_raw: None, }; } Ok(()) } } #[async_trait] impl Collector for BackupCollector { async fn collect_structured(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> { debug!("Collecting backup status"); self.populate_backup_data(agent_data).await } } /// TOML structure for backup status file #[derive(Debug, Clone, Serialize, Deserialize)] struct BackupStatusToml { pub backup_name: String, pub start_time: String, pub current_time: String, pub duration_seconds: i64, pub status: String, pub last_updated: String, pub disk_space: Option, pub disk_product_name: Option, pub disk_serial_number: Option, pub disk_wear_percent: Option, pub services: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] struct DiskSpace { pub total_bytes: u64, pub used_bytes: u64, pub available_bytes: u64, pub total_gb: f64, pub used_gb: f64, pub available_gb: f64, pub usage_percent: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] struct ServiceStatus { pub status: String, pub exit_code: i64, pub repo_path: String, pub archive_count: i64, pub repo_size_bytes: u64, }