use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupManifest {
pub version: u32,
pub created_at: String,
pub oxios_version: String,
pub sections: Vec<BackupSection>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BackupSection {
pub name: String,
pub entry_count: usize,
}
pub async fn create_backup(
state_store: &crate::state_store::StateStore,
output_path: &Path,
) -> Result<BackupManifest> {
let mut manifest = BackupManifest {
version: 1,
created_at: chrono::Utc::now().to_rfc3339(),
oxios_version: env!("CARGO_PKG_VERSION").to_string(),
sections: Vec::new(),
};
let categories = [
"seeds",
"evals",
"memory/conversations",
"memory/sessions",
"memory/facts",
"memory/episodes",
"memory/knowledge",
"sessions",
"agent_groups",
];
for category in &categories {
if let Ok(names) = state_store.list_category(category).await {
if !names.is_empty() {
manifest.sections.push(BackupSection {
name: category.to_string(),
entry_count: names.len(),
});
}
}
}
let src = &state_store.base_path;
if output_path.exists() {
tokio::fs::remove_dir_all(output_path).await?;
}
copy_dir_recursive(src, output_path).await?;
let manifest_json = serde_json::to_string_pretty(&manifest)?;
tokio::fs::write(output_path.join("manifest.json"), manifest_json).await?;
tracing::info!(path = %output_path.display(), sections = manifest.sections.len(), "Backup created");
Ok(manifest)
}
pub async fn restore_backup(
state_store: &crate::state_store::StateStore,
backup_path: &Path,
) -> Result<BackupManifest> {
let manifest_data = tokio::fs::read_to_string(backup_path.join("manifest.json"))
.await
.context("Backup missing manifest.json")?;
let manifest: BackupManifest = serde_json::from_str(&manifest_data)?;
copy_dir_recursive(backup_path, &state_store.base_path).await?;
tracing::info!(path = %backup_path.display(), sections = manifest.sections.len(), "Backup restored");
Ok(manifest)
}
fn copy_dir_recursive<'a>(
src: &'a Path,
dest: &'a Path,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
tokio::fs::create_dir_all(dest).await?;
let mut entries = tokio::fs::read_dir(src).await?;
while let Some(entry) = entries.next_entry().await? {
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dest_path).await?;
} else {
tokio::fs::copy(&src_path, &dest_path).await?;
}
}
Ok(())
})
}