use crate::backend::native::{
types::{NativeBackendError, NativeResult},
v2::{
export::ExportMode,
export::manifest::ManifestSerializer,
import::ImportMode,
import::snapshot::{SnapshotImportConfig, SnapshotImporter},
},
};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct RestoreConfig {
pub backup_dir: PathBuf,
pub target_path: PathBuf,
pub overwrite_existing: bool,
pub validate_manifest: bool,
pub verify_checksum: bool,
pub import_mode: ImportMode,
}
impl Default for RestoreConfig {
fn default() -> Self {
Self {
backup_dir: PathBuf::from("backup"),
target_path: PathBuf::from("restored.v2"),
overwrite_existing: false,
validate_manifest: true,
verify_checksum: true,
import_mode: ImportMode::Fresh,
}
}
}
impl RestoreConfig {
pub fn new(backup_dir: impl AsRef<Path>, target_path: impl AsRef<Path>) -> Self {
Self {
backup_dir: backup_dir.as_ref().to_path_buf(),
target_path: target_path.as_ref().to_path_buf(),
..Default::default()
}
}
pub fn with_overwrite(mut self, allow: bool) -> Self {
self.overwrite_existing = allow;
self
}
pub fn with_validation(mut self, enabled: bool) -> Self {
self.validate_manifest = enabled;
self
}
pub fn with_checksum_verification(mut self, enabled: bool) -> Self {
self.verify_checksum = enabled;
self
}
}
#[derive(Debug, Clone)]
pub struct RestoreResult {
pub restored_path: PathBuf,
pub records_imported: u64,
pub duration_secs: f64,
pub checksum: u64,
pub validation_passed: bool,
pub recovery_state: String,
}
pub fn restore_backup(config: RestoreConfig) -> NativeResult<RestoreResult> {
if !config.backup_dir.exists() {
return Err(NativeBackendError::InvalidParameter {
context: format!("Backup directory does not exist: {:?}", config.backup_dir),
source: None,
});
}
let manifest_path = config.backup_dir.join("export.manifest");
if !manifest_path.exists() {
return Err(NativeBackendError::InvalidParameter {
context: format!("Manifest not found: {:?}", manifest_path),
source: None,
});
}
let manifest = if config.validate_manifest {
ManifestSerializer::read_from_file(&manifest_path)?
} else {
ManifestSerializer::read_from_file(&manifest_path)?
};
if manifest.export_mode != ExportMode::Snapshot {
return Err(NativeBackendError::InvalidParameter {
context: format!("Backup is not a snapshot: {:?}", manifest.export_mode),
source: None,
});
}
if config.target_path.exists() && !config.overwrite_existing {
return Err(NativeBackendError::InvalidParameter {
context: format!(
"Target file exists and overwrite disabled: {:?}",
config.target_path
),
source: None,
});
}
let import_config = SnapshotImportConfig {
target_graph_path: config.target_path.clone(),
export_dir_path: config.backup_dir.clone(),
import_mode: config.import_mode,
validate_manifest: config.validate_manifest,
verify_checksum: config.verify_checksum,
overwrite_existing: config.overwrite_existing,
};
let importer = SnapshotImporter::from_export_dir(
&config.backup_dir,
&config.target_path,
import_config.clone(),
)?;
let import_result = importer.import()?;
Ok(RestoreResult {
restored_path: config.target_path.clone(),
records_imported: import_result.records_imported,
duration_secs: import_result.import_duration.as_secs_f64(),
checksum: import_result.imported_checksum,
validation_passed: import_result.validation_passed,
recovery_state: format!("{:?}", import_result.final_recovery_state),
})
}
pub fn restore(backup_dir: &Path, target_path: &Path) -> NativeResult<RestoreResult> {
restore_backup(RestoreConfig::new(backup_dir, target_path))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::native::v2::export::snapshot::{SnapshotExportConfig, SnapshotExporter};
use crate::backend::native::v2::export::manifest::ExportManifest;
use tempfile::TempDir;
fn create_test_snapshot(export_dir: &Path) -> NativeResult<(PathBuf, ExportManifest)> {
let graph_path = export_dir.join("source.v2");
let _graph = crate::backend::native::graph_file::GraphFile::create(&graph_path)?;
let config = SnapshotExportConfig {
export_path: export_dir.to_path_buf(),
snapshot_id: "test_snapshot".to_string(),
include_statistics: true,
min_stable_duration: std::time::Duration::from_secs(0),
checksum_validation: true,
};
let mut exporter = SnapshotExporter::new(&graph_path, config)?;
let _export_result = exporter.export_snapshot()?;
let manifest_path = export_dir.join("export.manifest");
let manifest = ManifestSerializer::read_from_file(&manifest_path)?;
Ok((graph_path, manifest))
}
#[test]
fn test_restore_config_default() {
let config = RestoreConfig::default();
assert_eq!(config.backup_dir, PathBuf::from("backup"));
assert_eq!(config.target_path, PathBuf::from("restored.v2"));
assert_eq!(config.overwrite_existing, false);
assert_eq!(config.validate_manifest, true);
assert_eq!(config.verify_checksum, true);
}
#[test]
fn test_restore_config_builder() {
let config = RestoreConfig::new("test_backup", "test_restored.v2")
.with_overwrite(true)
.with_validation(false)
.with_checksum_verification(false);
assert_eq!(config.backup_dir, PathBuf::from("test_backup"));
assert_eq!(config.target_path, PathBuf::from("test_restored.v2"));
assert_eq!(config.overwrite_existing, true);
assert_eq!(config.validate_manifest, false);
assert_eq!(config.verify_checksum, false);
}
#[test]
fn test_restore_rejects_missing_backup_dir() {
let temp_dir = TempDir::new().unwrap();
let missing_dir = temp_dir.path().join("nonexistent_backup");
let target_path = temp_dir.path().join("restored.v2");
let result = restore(&missing_dir, &target_path);
assert!(result.is_err());
match result {
Err(NativeBackendError::InvalidParameter { context, .. }) => {
assert!(context.contains("does not exist"));
}
_ => panic!("Expected InvalidParameter error"),
}
}
#[test]
fn test_restore_rejects_missing_manifest() {
let temp_dir = TempDir::new().unwrap();
let backup_dir = temp_dir.path().join("incomplete_backup");
std::fs::create_dir(&backup_dir).unwrap();
let target_path = temp_dir.path().join("restored.v2");
let result = restore(&backup_dir, &target_path);
assert!(result.is_err());
match result {
Err(NativeBackendError::InvalidParameter { context, .. }) => {
assert!(context.contains("Manifest not found"));
}
_ => panic!("Expected InvalidParameter error"),
}
}
#[test]
fn test_restore_rejects_overwrite_without_flag() {
let temp_dir = TempDir::new().unwrap();
let target_path = temp_dir.path().join("existing.v2");
std::fs::write(&target_path, b"existing data").unwrap();
let backup_dir = temp_dir.path().join("backup");
std::fs::create_dir(&backup_dir).unwrap();
let config = RestoreConfig::new(&backup_dir, &target_path).with_overwrite(false);
let result = restore_backup(config);
assert!(result.is_err());
}
#[test]
fn test_restore_accepts_overwrite_with_flag() {
let temp_dir = TempDir::new().unwrap();
let backup_dir = temp_dir.path().join("backup");
std::fs::create_dir(&backup_dir).unwrap();
let (_source_path, _manifest) = create_test_snapshot(&backup_dir).unwrap();
let target_path = temp_dir.path().join("existing.v2");
std::fs::write(&target_path, b"existing data").unwrap();
let config = RestoreConfig::new(&backup_dir, &target_path).with_overwrite(true);
let result = restore_backup(config);
match result {
Ok(_) => {
assert!(target_path.exists());
}
Err(e) => {
let err_msg = format!("{:?}", e);
assert!(
!err_msg.contains("overwrite disabled"),
"Should not fail due to overwrite protection when flag is set"
);
}
}
}
#[test]
fn test_backup_restore_roundtrip() {
let temp_dir = TempDir::new().unwrap();
let backup_dir = temp_dir.path().join("backup");
std::fs::create_dir(&backup_dir).unwrap();
let (_source_path, manifest) = create_test_snapshot(&backup_dir).unwrap();
assert!(backup_dir.join("export.manifest").exists());
let restore_path = temp_dir.path().join("restored.v2");
let result = restore(&backup_dir, &restore_path);
assert!(result.is_ok(), "Restore should succeed: {:?}", result.err());
let restore_result = result.unwrap();
assert!(restore_path.exists());
assert_eq!(restore_result.restored_path, restore_path);
assert!(restore_result.validation_passed);
let restored_graph = crate::backend::native::graph_file::GraphFile::open(&restore_path);
assert!(
restored_graph.is_ok(),
"Restored file should be a valid GraphFile"
);
let restored = restored_graph.unwrap();
assert_eq!(
restored.persistent_header().node_count as u64,
manifest.total_records
);
}
#[test]
fn test_restore_creates_result_with_correct_fields() {
let temp_dir = TempDir::new().unwrap();
let backup_dir = temp_dir.path().join("backup");
std::fs::create_dir(&backup_dir).unwrap();
create_test_snapshot(&backup_dir).unwrap();
let restore_path = temp_dir.path().join("restored.v2");
let result = restore(&backup_dir, &restore_path).unwrap();
assert_eq!(result.restored_path, restore_path);
assert!(result.records_imported >= 0);
assert!(result.duration_secs >= 0.0);
assert!(result.validation_passed);
assert!(!result.recovery_state.is_empty());
}
}