use crate::checkpoint::CheckpointMetadata;
use crate::{AzothError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryFile {
pub main_backup_id: String,
pub timestamp: String,
pub sealed_event_id: u64,
pub size_bytes: u64,
pub storage_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub component_backups: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mnemonic_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
pub struct RecoveryFileManager {
base_path: PathBuf,
filename: String,
}
impl RecoveryFileManager {
pub fn new(base_path: PathBuf) -> Self {
Self {
base_path,
filename: ".latest_backup".to_string(),
}
}
pub fn with_filename(base_path: PathBuf, filename: String) -> Self {
Self {
base_path,
filename,
}
}
fn file_path(&self) -> PathBuf {
self.base_path.join(&self.filename)
}
pub fn write(&self, metadata: &CheckpointMetadata) -> Result<()> {
std::fs::create_dir_all(&self.base_path)?;
let recovery = RecoveryFile {
main_backup_id: metadata.id.clone(),
timestamp: metadata.timestamp.to_string(),
sealed_event_id: metadata.sealed_event_id,
size_bytes: metadata.size_bytes,
storage_type: metadata.storage_type.clone(),
component_backups: None,
mnemonic_fingerprint: None,
name: metadata.name.clone(),
};
let path = self.file_path();
let json = serde_json::to_string_pretty(&recovery)
.map_err(|e| AzothError::Serialization(e.to_string()))?;
std::fs::write(&path, json)?;
tracing::info!("Wrote recovery file to {}", path.display());
Ok(())
}
pub fn write_with_components(
&self,
metadata: &CheckpointMetadata,
component_backups: HashMap<String, String>,
) -> Result<()> {
std::fs::create_dir_all(&self.base_path)?;
let recovery = RecoveryFile {
main_backup_id: metadata.id.clone(),
timestamp: metadata.timestamp.to_string(),
sealed_event_id: metadata.sealed_event_id,
size_bytes: metadata.size_bytes,
storage_type: metadata.storage_type.clone(),
component_backups: Some(component_backups),
mnemonic_fingerprint: None,
name: metadata.name.clone(),
};
let path = self.file_path();
let json = serde_json::to_string_pretty(&recovery)
.map_err(|e| AzothError::Serialization(e.to_string()))?;
std::fs::write(&path, json)?;
tracing::info!(
"Wrote recovery file with {} components to {}",
recovery
.component_backups
.as_ref()
.map(|c| c.len())
.unwrap_or(0),
path.display()
);
Ok(())
}
pub fn read(&self) -> Result<RecoveryFile> {
let path = self.file_path();
let json = std::fs::read_to_string(&path)?;
let recovery: RecoveryFile =
serde_json::from_str(&json).map_err(|e| AzothError::Serialization(e.to_string()))?;
Ok(recovery)
}
pub fn exists(&self) -> bool {
self.file_path().exists()
}
pub fn delete(&self) -> Result<()> {
let path = self.file_path();
if path.exists() {
std::fs::remove_file(&path)?;
tracing::info!("Deleted recovery file at {}", path.display());
}
Ok(())
}
pub fn path(&self) -> &Path {
&self.base_path
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use tempfile::TempDir;
#[test]
fn test_write_and_read() {
let temp_dir = TempDir::new().unwrap();
let manager = RecoveryFileManager::new(temp_dir.path().to_path_buf());
let metadata = CheckpointMetadata {
id: "QmTest123".to_string(),
timestamp: Utc::now(),
sealed_event_id: 12345,
size_bytes: 1_000_000,
name: Some("test-backup".to_string()),
storage_type: "ipfs".to_string(),
};
manager.write(&metadata).unwrap();
assert!(manager.exists());
let recovery = manager.read().unwrap();
assert_eq!(recovery.main_backup_id, "QmTest123");
assert_eq!(recovery.sealed_event_id, 12345);
assert_eq!(recovery.size_bytes, 1_000_000);
assert_eq!(recovery.name, Some("test-backup".to_string()));
}
#[test]
fn test_write_with_components() {
let temp_dir = TempDir::new().unwrap();
let manager = RecoveryFileManager::new(temp_dir.path().to_path_buf());
let metadata = CheckpointMetadata {
id: "QmMain".to_string(),
timestamp: Utc::now(),
sealed_event_id: 100,
size_bytes: 2_000_000,
name: None,
storage_type: "ipfs".to_string(),
};
let mut components = HashMap::new();
components.insert("database1".to_string(), "QmDb1".to_string());
components.insert("database2".to_string(), "QmDb2".to_string());
manager
.write_with_components(&metadata, components)
.unwrap();
let recovery = manager.read().unwrap();
assert_eq!(recovery.component_backups.as_ref().unwrap().len(), 2);
assert_eq!(
recovery
.component_backups
.as_ref()
.unwrap()
.get("database1"),
Some(&"QmDb1".to_string())
);
}
#[test]
fn test_custom_filename() {
let temp_dir = TempDir::new().unwrap();
let manager = RecoveryFileManager::with_filename(
temp_dir.path().to_path_buf(),
"custom_recovery.json".to_string(),
);
let metadata = CheckpointMetadata {
id: "QmCustom".to_string(),
timestamp: Utc::now(),
sealed_event_id: 999,
size_bytes: 500_000,
name: None,
storage_type: "local".to_string(),
};
manager.write(&metadata).unwrap();
assert!(temp_dir.path().join("custom_recovery.json").exists());
}
#[test]
fn test_delete() {
let temp_dir = TempDir::new().unwrap();
let manager = RecoveryFileManager::new(temp_dir.path().to_path_buf());
let metadata = CheckpointMetadata {
id: "QmDelete".to_string(),
timestamp: Utc::now(),
sealed_event_id: 555,
size_bytes: 100_000,
name: None,
storage_type: "local".to_string(),
};
manager.write(&metadata).unwrap();
assert!(manager.exists());
manager.delete().unwrap();
assert!(!manager.exists());
manager.delete().unwrap();
}
}