1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7#[derive(Debug, Serialize, Deserialize)]
9pub struct BackupManifest {
10 pub version: u32,
12 pub created_at: String,
14 pub oxios_version: String,
16 pub sections: Vec<BackupSection>,
18}
19
20#[derive(Debug, Serialize, Deserialize)]
22pub struct BackupSection {
23 pub name: String,
25 pub entry_count: usize,
27}
28
29pub async fn create_backup(
31 state_store: &crate::state_store::StateStore,
32 output_path: &Path,
33) -> Result<BackupManifest> {
34 let mut manifest = BackupManifest {
35 version: 1,
36 created_at: chrono::Utc::now().to_rfc3339(),
37 oxios_version: env!("CARGO_PKG_VERSION").to_string(),
38 sections: Vec::new(),
39 };
40
41 let categories = [
42 "seeds",
43 "evals",
44 "memory/conversations",
45 "memory/sessions",
46 "memory/facts",
47 "memory/episodes",
48 "memory/knowledge",
49 "sessions",
50 "agent_groups",
51 ];
52
53 for category in &categories {
54 if let Ok(names) = state_store.list_category(category).await {
55 if !names.is_empty() {
56 manifest.sections.push(BackupSection {
57 name: category.to_string(),
58 entry_count: names.len(),
59 });
60 }
61 }
62 }
63
64 let src = &state_store.base_path;
66 if output_path.exists() {
67 tokio::fs::remove_dir_all(output_path).await?;
68 }
69 copy_dir_recursive(src, output_path).await?;
70
71 let manifest_json = serde_json::to_string_pretty(&manifest)?;
73 tokio::fs::write(output_path.join("manifest.json"), manifest_json).await?;
74
75 tracing::info!(path = %output_path.display(), sections = manifest.sections.len(), "Backup created");
76 Ok(manifest)
77}
78
79pub async fn restore_backup(
81 state_store: &crate::state_store::StateStore,
82 backup_path: &Path,
83) -> Result<BackupManifest> {
84 let manifest_data = tokio::fs::read_to_string(backup_path.join("manifest.json"))
85 .await
86 .context("Backup missing manifest.json")?;
87 let manifest: BackupManifest = serde_json::from_str(&manifest_data)?;
88
89 copy_dir_recursive(backup_path, &state_store.base_path).await?;
91
92 tracing::info!(path = %backup_path.display(), sections = manifest.sections.len(), "Backup restored");
93 Ok(manifest)
94}
95
96fn copy_dir_recursive<'a>(
97 src: &'a Path,
98 dest: &'a Path,
99) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
100 Box::pin(async move {
101 tokio::fs::create_dir_all(dest).await?;
102 let mut entries = tokio::fs::read_dir(src).await?;
103 while let Some(entry) = entries.next_entry().await? {
104 let src_path = entry.path();
105 let dest_path = dest.join(entry.file_name());
106 if src_path.is_dir() {
107 copy_dir_recursive(&src_path, &dest_path).await?;
108 } else {
109 tokio::fs::copy(&src_path, &dest_path).await?;
110 }
111 }
112 Ok(())
113 })
114}