orca_core/backup/
manager.rs1use std::path::Path;
2
3use anyhow::{Context, Result};
4use chrono::Utc;
5use tracing::info;
6
7use super::config::{BackupConfig, BackupTarget};
8use super::s3 as s3_backend;
9
10#[derive(Debug, Clone)]
12pub struct BackupResult {
13 pub service_name: String,
14 pub timestamp: String,
15 pub size_bytes: u64,
16 pub target: String,
17}
18
19pub struct BackupManager {
21 config: BackupConfig,
22}
23
24impl BackupManager {
25 pub fn new(config: BackupConfig) -> Self {
26 Self { config }
27 }
28
29 pub fn backup_volume(
31 &self,
32 service_name: &str,
33 volume_path: &str,
34 pre_hook: Option<&str>,
35 ) -> Result<BackupResult> {
36 let timestamp = Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
37
38 if let Some(hook) = pre_hook {
39 info!(service = service_name, hook, "Running pre-backup hook");
40 let status = std::process::Command::new("sh")
41 .arg("-c")
42 .arg(hook)
43 .status()
44 .context("Failed to execute pre-backup hook")?;
45 if !status.success() {
46 anyhow::bail!("Pre-backup hook failed: {:?}", status.code());
47 }
48 }
49
50 let archive_name = format!("{service_name}_{timestamp}.tar.gz");
51 let archive_path = std::env::temp_dir().join(&archive_name);
52
53 info!(
54 service = service_name,
55 src = volume_path,
56 "Creating archive"
57 );
58 let status = std::process::Command::new("tar")
59 .args([
60 "-czf",
61 archive_path.to_str().unwrap_or(""),
62 "-C",
63 volume_path,
64 ".",
65 ])
66 .status()
67 .context("Failed to create tar archive")?;
68 if !status.success() {
69 anyhow::bail!("tar failed: {:?}", status.code());
70 }
71
72 let size_bytes = std::fs::metadata(&archive_path)
73 .map(|m| m.len())
74 .unwrap_or(0);
75 let mut target_desc = String::new();
76 for t in &self.config.targets {
77 target_desc = self.store(&archive_path, t, &archive_name)?;
78 }
79 let _ = std::fs::remove_file(&archive_path);
80
81 Ok(BackupResult {
82 service_name: service_name.to_string(),
83 timestamp,
84 size_bytes,
85 target: target_desc,
86 })
87 }
88
89 pub fn backup_file(&self, name: &str, path: &Path) -> Result<()> {
91 let timestamp = Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
92 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("bak");
93 let backup_name = format!("{name}_{timestamp}.{ext}");
94 for t in &self.config.targets {
95 self.store(path, t, &backup_name)?;
96 }
97 Ok(())
98 }
99
100 fn store(&self, data_path: &Path, target: &BackupTarget, name: &str) -> Result<String> {
101 match target {
102 BackupTarget::Local { path } => {
103 let dest_dir = Path::new(path);
104 std::fs::create_dir_all(dest_dir)
105 .with_context(|| format!("create backup dir: {path}"))?;
106 let dest = dest_dir.join(name);
107 std::fs::copy(data_path, &dest)
108 .with_context(|| format!("copy to {}", dest.display()))?;
109 info!(dest = %dest.display(), "Stored backup locally");
110 Ok(format!("local:{path}"))
111 }
112 t @ BackupTarget::S3 { bucket, .. } => {
113 s3_backend::upload(data_path, t, name)?;
114 Ok(format!("s3://{bucket}"))
115 }
116 }
117 }
118
119 pub fn list_backups(&self, target: &BackupTarget) -> Result<Vec<String>> {
121 match target {
122 BackupTarget::Local { path } => {
123 let dir = Path::new(path);
124 if !dir.exists() {
125 return Ok(vec![]);
126 }
127 let mut entries = Vec::new();
128 for entry in std::fs::read_dir(dir)? {
129 if let Some(name) = entry?.file_name().to_str() {
130 entries.push(name.to_string());
131 }
132 }
133 entries.sort();
134 Ok(entries)
135 }
136 t @ BackupTarget::S3 { .. } => s3_backend::list_objects(t),
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn backup_file_to_local() {
147 let tmp = tempfile::tempdir().unwrap();
148 let target_dir = tmp.path().join("backups");
149 let config = BackupConfig {
150 schedule: None,
151 retention_days: 7,
152 targets: vec![BackupTarget::Local {
153 path: target_dir.to_str().unwrap().to_string(),
154 }],
155 };
156 let mgr = BackupManager::new(config);
157 let src = tmp.path().join("test.json");
158 std::fs::write(&src, r#"{"key":"value"}"#).unwrap();
159 mgr.backup_file("secrets", &src).unwrap();
160 let backups = std::fs::read_dir(&target_dir).unwrap().count();
161 assert_eq!(backups, 1);
162 }
163}