mentedb_storage/
backup.rs1use std::collections::BTreeMap;
7use std::fs;
8use std::io::{Read, Write};
9use std::path::Path;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use mentedb_core::error::MenteResult;
13use mentedb_core::types::Timestamp;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct BackupManifest {
19 pub created_at: Timestamp,
21 pub size_bytes: u64,
23 pub memory_count: u64,
25 pub version: String,
27}
28
29pub struct BackupManager;
31
32const BACKUP_VERSION: &str = "mentedb-backup-v1";
33
34impl BackupManager {
35 pub fn create_backup(data_dir: &Path, backup_path: &Path) -> MenteResult<BackupManifest> {
37 let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
38 Self::collect_files(data_dir, data_dir, &mut files)?;
39
40 let now = SystemTime::now()
41 .duration_since(UNIX_EPOCH)
42 .unwrap_or_default()
43 .as_micros() as Timestamp;
44
45 let total_bytes: u64 = files.values().map(|v| v.len() as u64).sum();
46
47 let manifest = BackupManifest {
48 created_at: now,
49 size_bytes: total_bytes,
50 memory_count: files.len() as u64,
51 version: BACKUP_VERSION.to_string(),
52 };
53
54 let manifest_json = serde_json::to_vec(&manifest)
55 .map_err(|e| mentedb_core::MenteError::Serialization(e.to_string()))?;
56
57 let mut out = fs::File::create(backup_path)?;
58
59 out.write_all(&(manifest_json.len() as u32).to_le_bytes())?;
61 out.write_all(&manifest_json)?;
62
63 for (name, data) in &files {
65 let name_bytes = name.as_bytes();
66 out.write_all(&(name_bytes.len() as u32).to_le_bytes())?;
67 out.write_all(name_bytes)?;
68 out.write_all(&(data.len() as u64).to_le_bytes())?;
69 out.write_all(data)?;
70 }
71
72 out.flush()?;
73 Ok(manifest)
74 }
75
76 pub fn restore_backup(backup_path: &Path, target_dir: &Path) -> MenteResult<BackupManifest> {
78 let mut file = fs::File::open(backup_path)?;
79
80 let mut len_buf = [0u8; 4];
82 file.read_exact(&mut len_buf)?;
83 let manifest_len = u32::from_le_bytes(len_buf) as usize;
84
85 let mut manifest_buf = vec![0u8; manifest_len];
86 file.read_exact(&mut manifest_buf)?;
87
88 let manifest: BackupManifest = serde_json::from_slice(&manifest_buf)
89 .map_err(|e| mentedb_core::MenteError::Serialization(e.to_string()))?;
90
91 fs::create_dir_all(target_dir)?;
92
93 for _ in 0..manifest.memory_count {
95 let mut name_len_buf = [0u8; 4];
96 file.read_exact(&mut name_len_buf)?;
97 let name_len = u32::from_le_bytes(name_len_buf) as usize;
98
99 let mut name_buf = vec![0u8; name_len];
100 file.read_exact(&mut name_buf)?;
101 let name = String::from_utf8(name_buf)
102 .map_err(|e| mentedb_core::MenteError::Serialization(e.to_string()))?;
103
104 let mut data_len_buf = [0u8; 8];
105 file.read_exact(&mut data_len_buf)?;
106 let data_len = u64::from_le_bytes(data_len_buf) as usize;
107
108 let mut data = vec![0u8; data_len];
109 file.read_exact(&mut data)?;
110
111 let dest = target_dir.join(&name);
112 if let Some(parent) = dest.parent() {
113 fs::create_dir_all(parent)?;
114 }
115 fs::write(&dest, &data)?;
116 }
117
118 Ok(manifest)
119 }
120
121 pub fn list_backups(backup_dir: &Path) -> MenteResult<Vec<BackupManifest>> {
123 let mut manifests = Vec::new();
124
125 if !backup_dir.exists() {
126 return Ok(manifests);
127 }
128
129 for entry in fs::read_dir(backup_dir)? {
130 let entry = entry?;
131 let path = entry.path();
132 if path.extension().and_then(|e| e.to_str()) == Some("mentebackup")
133 && let Ok(m) = Self::read_manifest(&path)
134 {
135 manifests.push(m);
136 }
137 }
138
139 manifests.sort_by(|a, b| b.created_at.cmp(&a.created_at));
140 Ok(manifests)
141 }
142
143 fn read_manifest(backup_path: &Path) -> MenteResult<BackupManifest> {
145 let mut file = fs::File::open(backup_path)?;
146 let mut len_buf = [0u8; 4];
147 file.read_exact(&mut len_buf)?;
148 let manifest_len = u32::from_le_bytes(len_buf) as usize;
149 let mut manifest_buf = vec![0u8; manifest_len];
150 file.read_exact(&mut manifest_buf)?;
151 serde_json::from_slice(&manifest_buf)
152 .map_err(|e| mentedb_core::MenteError::Serialization(e.to_string()))
153 }
154
155 fn collect_files(
157 base: &Path,
158 dir: &Path,
159 files: &mut BTreeMap<String, Vec<u8>>,
160 ) -> MenteResult<()> {
161 if !dir.exists() {
162 return Ok(());
163 }
164 for entry in fs::read_dir(dir)? {
165 let entry = entry?;
166 let path = entry.path();
167 if path.is_dir() {
168 Self::collect_files(base, &path, files)?;
169 } else {
170 let rel = path
171 .strip_prefix(base)
172 .map_err(|e| mentedb_core::MenteError::Storage(e.to_string()))?;
173 let data = fs::read(&path)?;
174 files.insert(rel.to_string_lossy().into_owned(), data);
175 }
176 }
177 Ok(())
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use tempfile::TempDir;
185
186 #[test]
187 fn backup_restore_roundtrip() {
188 let data_dir = TempDir::new().unwrap();
189 let backup_dir = TempDir::new().unwrap();
190
191 fs::write(data_dir.path().join("file1.dat"), b"hello world").unwrap();
193 fs::create_dir_all(data_dir.path().join("sub")).unwrap();
194 fs::write(data_dir.path().join("sub/file2.dat"), b"nested data").unwrap();
195
196 let backup_path = backup_dir.path().join("test.mentebackup");
197 let manifest = BackupManager::create_backup(data_dir.path(), &backup_path).unwrap();
198 assert_eq!(manifest.memory_count, 2);
199 assert_eq!(manifest.version, BACKUP_VERSION);
200
201 let restore_dir = TempDir::new().unwrap();
203 let restored = BackupManager::restore_backup(&backup_path, restore_dir.path()).unwrap();
204 assert_eq!(restored.memory_count, 2);
205
206 assert_eq!(
207 fs::read_to_string(restore_dir.path().join("file1.dat")).unwrap(),
208 "hello world"
209 );
210 assert_eq!(
211 fs::read_to_string(restore_dir.path().join("sub/file2.dat")).unwrap(),
212 "nested data"
213 );
214 }
215
216 #[test]
217 fn restore_into_different_directory() {
218 let data_dir = TempDir::new().unwrap();
219 fs::write(data_dir.path().join("data.bin"), vec![0u8; 1024]).unwrap();
220
221 let backup_dir = TempDir::new().unwrap();
222 let backup_path = backup_dir.path().join("backup.mentebackup");
223 BackupManager::create_backup(data_dir.path(), &backup_path).unwrap();
224
225 let alt_dir = TempDir::new().unwrap();
227 let alt_target = alt_dir.path().join("deep/nested/restore");
228 let manifest = BackupManager::restore_backup(&backup_path, &alt_target).unwrap();
229
230 assert_eq!(manifest.memory_count, 1);
231 assert_eq!(manifest.size_bytes, 1024);
232 assert_eq!(fs::read(alt_target.join("data.bin")).unwrap().len(), 1024);
233 }
234}