Skip to main content

mentedb_storage/
backup.rs

1//! Backup and restore for MenteDB storage.
2//!
3//! Uses a simple custom format: a JSON manifest header followed by
4//! length-prefixed file entries. No external compression dependencies needed.
5
6use 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/// Metadata about a backup.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct BackupManifest {
19    /// When the backup was created (microseconds since epoch).
20    pub created_at: Timestamp,
21    /// Total size of all backed-up files in bytes.
22    pub size_bytes: u64,
23    /// Number of files included in the backup.
24    pub memory_count: u64,
25    /// Format version string.
26    pub version: String,
27}
28
29/// Manages backup creation and restoration.
30pub struct BackupManager;
31
32const BACKUP_VERSION: &str = "mentedb-backup-v1";
33
34impl BackupManager {
35    /// Create a full backup of `data_dir` into a single file at `backup_path`.
36    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        // Write manifest length + manifest
60        out.write_all(&(manifest_json.len() as u32).to_le_bytes())?;
61        out.write_all(&manifest_json)?;
62
63        // Write each file entry
64        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    /// Restore a backup file into `target_dir`.
77    pub fn restore_backup(backup_path: &Path, target_dir: &Path) -> MenteResult<BackupManifest> {
78        let mut file = fs::File::open(backup_path)?;
79
80        // Read manifest
81        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        // Read file entries
94        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    /// List backup manifests found in `backup_dir` (files matching `*.mentebackup`).
122    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    /// Read only the manifest header from a backup file.
144    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    /// Recursively collect all files under `base` into the map.
156    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        // Create some test files
192        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        // Restore into same directory structure
202        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        // Restore into a completely different location
226        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}