1use rustlite_core::{Error, Result};
39use serde::{Deserialize, Serialize};
40use std::fs::{self, File};
41use std::io::{BufReader, BufWriter, Read, Write};
42use std::path::{Path, PathBuf};
43use std::time::{SystemTime, UNIX_EPOCH};
44
45pub mod manager;
46
47const SNAPSHOT_META_FILE: &str = "SNAPSHOT_META";
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SnapshotMeta {
53 pub id: String,
55 pub timestamp: u64,
57 pub path: String,
59 pub source_path: String,
61 pub sequence: u64,
63 pub files: Vec<SnapshotFile>,
65 pub total_size: u64,
67 pub snapshot_type: SnapshotType,
69 pub parent_id: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SnapshotFile {
76 pub relative_path: String,
78 pub size: u64,
80 pub modified: u64,
82 pub checksum: u32,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum SnapshotType {
89 Full,
91 Incremental,
93}
94
95#[derive(Debug, Clone)]
97pub struct SnapshotConfig {
98 pub include_wal: bool,
100 pub verify_checksums: bool,
102 pub compression: u8,
104}
105
106impl Default for SnapshotConfig {
107 fn default() -> Self {
108 Self {
109 include_wal: true,
110 verify_checksums: true,
111 compression: 0,
112 }
113 }
114}
115
116pub struct SnapshotManager {
118 source_dir: PathBuf,
120 config: SnapshotConfig,
122 snapshots: Vec<SnapshotMeta>,
124}
125
126impl SnapshotManager {
127 pub fn new(source_dir: impl AsRef<Path>) -> Result<Self> {
129 Self::with_config(source_dir, SnapshotConfig::default())
130 }
131
132 pub fn with_config(source_dir: impl AsRef<Path>, config: SnapshotConfig) -> Result<Self> {
134 let source_dir = source_dir.as_ref().to_path_buf();
135
136 if !source_dir.exists() {
137 return Err(Error::Storage(format!(
138 "Source directory does not exist: {:?}",
139 source_dir
140 )));
141 }
142
143 Ok(Self {
144 source_dir,
145 config,
146 snapshots: Vec::new(),
147 })
148 }
149
150 pub fn create_snapshot(&mut self, dest: impl AsRef<Path>) -> Result<SnapshotMeta> {
152 let dest = dest.as_ref().to_path_buf();
153
154 fs::create_dir_all(&dest)?;
156
157 let timestamp = SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .unwrap_or_default()
161 .as_millis() as u64;
162 let id = format!("snap_{}", timestamp);
163
164 let mut files = Vec::new();
166 let mut total_size = 0u64;
167
168 self.collect_files(
169 &self.source_dir.clone(),
170 &self.source_dir.clone(),
171 &mut files,
172 &mut total_size,
173 )?;
174
175 for file in &files {
177 let src_path = self.source_dir.join(&file.relative_path);
178 let dst_path = dest.join(&file.relative_path);
179
180 if let Some(parent) = dst_path.parent() {
182 fs::create_dir_all(parent)?;
183 }
184
185 fs::copy(&src_path, &dst_path)?;
187
188 if self.config.verify_checksums {
190 let copied_checksum = Self::compute_checksum(&dst_path)?;
191 if copied_checksum != file.checksum {
192 return Err(Error::Corruption(format!(
193 "Checksum mismatch for {}: expected {}, got {}",
194 file.relative_path, file.checksum, copied_checksum
195 )));
196 }
197 }
198 }
199
200 let sequence = self.read_sequence()?;
202
203 let meta = SnapshotMeta {
205 id: id.clone(),
206 timestamp,
207 path: dest.to_string_lossy().to_string(),
208 source_path: self.source_dir.to_string_lossy().to_string(),
209 sequence,
210 files,
211 total_size,
212 snapshot_type: SnapshotType::Full,
213 parent_id: None,
214 };
215
216 self.write_metadata(&dest, &meta)?;
218
219 self.snapshots.push(meta.clone());
221
222 Ok(meta)
223 }
224
225 fn collect_files(
227 &self,
228 dir: &Path,
229 base: &Path,
230 files: &mut Vec<SnapshotFile>,
231 total_size: &mut u64,
232 ) -> Result<()> {
233 if !dir.exists() {
234 return Ok(());
235 }
236
237 for entry in fs::read_dir(dir)? {
238 let entry = entry?;
239 let path = entry.path();
240
241 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
243 if name == "lock" || name.starts_with('.') {
244 continue;
245 }
246
247 if !self.config.include_wal && name == "wal" {
249 continue;
250 }
251
252 if path.is_dir() {
253 self.collect_files(&path, base, files, total_size)?;
254 } else {
255 let relative_path = path
256 .strip_prefix(base)
257 .map_err(|_| Error::Storage("Failed to get relative path".into()))?
258 .to_string_lossy()
259 .to_string();
260
261 let metadata = fs::metadata(&path)?;
262 let size = metadata.len();
263 let modified = metadata
264 .modified()
265 .ok()
266 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
267 .map(|d| d.as_millis() as u64)
268 .unwrap_or(0);
269
270 let checksum = Self::compute_checksum(&path)?;
271
272 files.push(SnapshotFile {
273 relative_path,
274 size,
275 modified,
276 checksum,
277 });
278
279 *total_size += size;
280 }
281 }
282
283 Ok(())
284 }
285
286 fn compute_checksum(path: &Path) -> Result<u32> {
288 let file = File::open(path)?;
289 let mut reader = BufReader::new(file);
290 let mut hasher = crc32fast::Hasher::new();
291
292 let mut buffer = [0u8; 8192];
293 loop {
294 let bytes_read = reader.read(&mut buffer)?;
295 if bytes_read == 0 {
296 break;
297 }
298 hasher.update(&buffer[..bytes_read]);
299 }
300
301 Ok(hasher.finalize())
302 }
303
304 fn read_sequence(&self) -> Result<u64> {
306 let manifest_path = self.source_dir.join("MANIFEST");
308 if !manifest_path.exists() {
309 return Ok(0);
310 }
311
312 Ok(0)
314 }
315
316 fn write_metadata(&self, dest: &Path, meta: &SnapshotMeta) -> Result<()> {
318 let meta_path = dest.join(SNAPSHOT_META_FILE);
319 let file = File::create(&meta_path)?;
320 let mut writer = BufWriter::new(file);
321
322 let encoded =
323 bincode::serialize(meta).map_err(|e| Error::Serialization(e.to_string()))?;
324
325 writer.write_all(&encoded)?;
326 writer.flush()?;
327
328 Ok(())
329 }
330
331 pub fn load_snapshot(snapshot_dir: impl AsRef<Path>) -> Result<SnapshotMeta> {
333 let meta_path = snapshot_dir.as_ref().join(SNAPSHOT_META_FILE);
334 let file = File::open(&meta_path)?;
335 let mut reader = BufReader::new(file);
336
337 let mut contents = Vec::new();
338 reader.read_to_end(&mut contents)?;
339
340 let meta: SnapshotMeta =
341 bincode::deserialize(&contents).map_err(|e| Error::Serialization(e.to_string()))?;
342
343 Ok(meta)
344 }
345
346 pub fn restore_snapshot(&self, snapshot: &SnapshotMeta, dest: impl AsRef<Path>) -> Result<()> {
348 let dest = dest.as_ref().to_path_buf();
349 let snapshot_dir = PathBuf::from(&snapshot.path);
350
351 fs::create_dir_all(&dest)?;
353
354 for file in &snapshot.files {
356 let src_path = snapshot_dir.join(&file.relative_path);
357 let dst_path = dest.join(&file.relative_path);
358
359 if let Some(parent) = dst_path.parent() {
361 fs::create_dir_all(parent)?;
362 }
363
364 if src_path.exists() {
366 fs::copy(&src_path, &dst_path)?;
367 }
368 }
369
370 Ok(())
371 }
372
373 pub fn list_snapshots(&self) -> &[SnapshotMeta] {
375 &self.snapshots
376 }
377
378 pub fn delete_snapshot(&mut self, snapshot_id: &str) -> Result<bool> {
380 let pos = self.snapshots.iter().position(|s| s.id == snapshot_id);
382
383 if let Some(idx) = pos {
384 let snapshot = self.snapshots.remove(idx);
385
386 let path = PathBuf::from(&snapshot.path);
388 if path.exists() {
389 fs::remove_dir_all(&path)?;
390 }
391
392 Ok(true)
393 } else {
394 Ok(false)
395 }
396 }
397
398 pub fn get_snapshot(&self, id: &str) -> Option<&SnapshotMeta> {
400 self.snapshots.iter().find(|s| s.id == id)
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use tempfile::tempdir;
408
409 fn create_test_db(dir: &Path) {
410 fs::create_dir_all(dir.join("sst")).unwrap();
412 fs::create_dir_all(dir.join("wal")).unwrap();
413
414 fs::write(dir.join("MANIFEST"), b"test manifest").unwrap();
415 fs::write(dir.join("sst/L0_001.sst"), b"test sstable data").unwrap();
416 fs::write(dir.join("wal/00000001.wal"), b"test wal data").unwrap();
417 }
418
419 #[test]
420 fn test_snapshot_manager_new() {
421 let dir = tempdir().unwrap();
422 create_test_db(dir.path());
423
424 let manager = SnapshotManager::new(dir.path()).unwrap();
425 assert!(manager.list_snapshots().is_empty());
426 }
427
428 #[test]
429 fn test_create_snapshot() {
430 let source_dir = tempdir().unwrap();
431 let dest_dir = tempdir().unwrap();
432
433 create_test_db(source_dir.path());
434
435 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
436 let snapshot = manager.create_snapshot(dest_dir.path()).unwrap();
437
438 assert!(snapshot.id.starts_with("snap_"));
439 assert_eq!(snapshot.snapshot_type, SnapshotType::Full);
440 assert!(!snapshot.files.is_empty());
441
442 assert!(dest_dir.path().join("MANIFEST").exists());
444 assert!(dest_dir.path().join("sst/L0_001.sst").exists());
445 assert!(dest_dir.path().join("wal/00000001.wal").exists());
446 assert!(dest_dir.path().join(SNAPSHOT_META_FILE).exists());
447 }
448
449 #[test]
450 fn test_load_snapshot() {
451 let source_dir = tempdir().unwrap();
452 let dest_dir = tempdir().unwrap();
453
454 create_test_db(source_dir.path());
455
456 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
457 let original = manager.create_snapshot(dest_dir.path()).unwrap();
458
459 let loaded = SnapshotManager::load_snapshot(dest_dir.path()).unwrap();
461
462 assert_eq!(loaded.id, original.id);
463 assert_eq!(loaded.files.len(), original.files.len());
464 }
465
466 #[test]
467 fn test_restore_snapshot() {
468 let source_dir = tempdir().unwrap();
469 let snapshot_dir = tempdir().unwrap();
470 let restore_dir = tempdir().unwrap();
471
472 create_test_db(source_dir.path());
473
474 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
475 let snapshot = manager.create_snapshot(snapshot_dir.path()).unwrap();
476
477 manager.restore_snapshot(&snapshot, restore_dir.path()).unwrap();
479
480 assert!(restore_dir.path().join("MANIFEST").exists());
482 assert!(restore_dir.path().join("sst/L0_001.sst").exists());
483 }
484
485 #[test]
486 fn test_delete_snapshot() {
487 let source_dir = tempdir().unwrap();
488 let dest_dir = tempdir().unwrap();
489
490 create_test_db(source_dir.path());
491
492 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
493 let snapshot = manager.create_snapshot(dest_dir.path()).unwrap();
494
495 assert_eq!(manager.list_snapshots().len(), 1);
496
497 let deleted = manager.delete_snapshot(&snapshot.id).unwrap();
498 assert!(deleted);
499 assert!(manager.list_snapshots().is_empty());
500 }
501
502 #[test]
503 fn test_checksum_verification() {
504 let source_dir = tempdir().unwrap();
505
506 create_test_db(source_dir.path());
507
508 let checksum = SnapshotManager::compute_checksum(&source_dir.path().join("MANIFEST")).unwrap();
510 assert!(checksum > 0);
511 }
512
513 #[test]
514 fn test_snapshot_without_wal() {
515 let source_dir = tempdir().unwrap();
516 let dest_dir = tempdir().unwrap();
517
518 create_test_db(source_dir.path());
519
520 let config = SnapshotConfig {
521 include_wal: false,
522 ..Default::default()
523 };
524
525 let mut manager = SnapshotManager::with_config(source_dir.path(), config).unwrap();
526 let snapshot = manager.create_snapshot(dest_dir.path()).unwrap();
527
528 assert!(!snapshot.files.iter().any(|f| f.relative_path.contains("wal")));
530 }
531}