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 = bincode::serialize(meta).map_err(|e| Error::Serialization(e.to_string()))?;
323
324 writer.write_all(&encoded)?;
325 writer.flush()?;
326
327 Ok(())
328 }
329
330 pub fn load_snapshot(snapshot_dir: impl AsRef<Path>) -> Result<SnapshotMeta> {
332 let meta_path = snapshot_dir.as_ref().join(SNAPSHOT_META_FILE);
333 let file = File::open(&meta_path)?;
334 let mut reader = BufReader::new(file);
335
336 let mut contents = Vec::new();
337 reader.read_to_end(&mut contents)?;
338
339 let meta: SnapshotMeta =
340 bincode::deserialize(&contents).map_err(|e| Error::Serialization(e.to_string()))?;
341
342 Ok(meta)
343 }
344
345 pub fn restore_snapshot(&self, snapshot: &SnapshotMeta, dest: impl AsRef<Path>) -> Result<()> {
347 let dest = dest.as_ref().to_path_buf();
348 let snapshot_dir = PathBuf::from(&snapshot.path);
349
350 fs::create_dir_all(&dest)?;
352
353 for file in &snapshot.files {
355 let src_path = snapshot_dir.join(&file.relative_path);
356 let dst_path = dest.join(&file.relative_path);
357
358 if let Some(parent) = dst_path.parent() {
360 fs::create_dir_all(parent)?;
361 }
362
363 if src_path.exists() {
365 fs::copy(&src_path, &dst_path)?;
366 }
367 }
368
369 Ok(())
370 }
371
372 pub fn list_snapshots(&self) -> &[SnapshotMeta] {
374 &self.snapshots
375 }
376
377 pub fn delete_snapshot(&mut self, snapshot_id: &str) -> Result<bool> {
379 let pos = self.snapshots.iter().position(|s| s.id == snapshot_id);
381
382 if let Some(idx) = pos {
383 let snapshot = self.snapshots.remove(idx);
384
385 let path = PathBuf::from(&snapshot.path);
387 if path.exists() {
388 fs::remove_dir_all(&path)?;
389 }
390
391 Ok(true)
392 } else {
393 Ok(false)
394 }
395 }
396
397 pub fn get_snapshot(&self, id: &str) -> Option<&SnapshotMeta> {
399 self.snapshots.iter().find(|s| s.id == id)
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use tempfile::tempdir;
407
408 fn create_test_db(dir: &Path) {
409 fs::create_dir_all(dir.join("sst")).unwrap();
411 fs::create_dir_all(dir.join("wal")).unwrap();
412
413 fs::write(dir.join("MANIFEST"), b"test manifest").unwrap();
414 fs::write(dir.join("sst/L0_001.sst"), b"test sstable data").unwrap();
415 fs::write(dir.join("wal/00000001.wal"), b"test wal data").unwrap();
416 }
417
418 #[test]
419 fn test_snapshot_manager_new() {
420 let dir = tempdir().unwrap();
421 create_test_db(dir.path());
422
423 let manager = SnapshotManager::new(dir.path()).unwrap();
424 assert!(manager.list_snapshots().is_empty());
425 }
426
427 #[test]
428 fn test_create_snapshot() {
429 let source_dir = tempdir().unwrap();
430 let dest_dir = tempdir().unwrap();
431
432 create_test_db(source_dir.path());
433
434 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
435 let snapshot = manager.create_snapshot(dest_dir.path()).unwrap();
436
437 assert!(snapshot.id.starts_with("snap_"));
438 assert_eq!(snapshot.snapshot_type, SnapshotType::Full);
439 assert!(!snapshot.files.is_empty());
440
441 assert!(dest_dir.path().join("MANIFEST").exists());
443 assert!(dest_dir.path().join("sst/L0_001.sst").exists());
444 assert!(dest_dir.path().join("wal/00000001.wal").exists());
445 assert!(dest_dir.path().join(SNAPSHOT_META_FILE).exists());
446 }
447
448 #[test]
449 fn test_load_snapshot() {
450 let source_dir = tempdir().unwrap();
451 let dest_dir = tempdir().unwrap();
452
453 create_test_db(source_dir.path());
454
455 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
456 let original = manager.create_snapshot(dest_dir.path()).unwrap();
457
458 let loaded = SnapshotManager::load_snapshot(dest_dir.path()).unwrap();
460
461 assert_eq!(loaded.id, original.id);
462 assert_eq!(loaded.files.len(), original.files.len());
463 }
464
465 #[test]
466 fn test_restore_snapshot() {
467 let source_dir = tempdir().unwrap();
468 let snapshot_dir = tempdir().unwrap();
469 let restore_dir = tempdir().unwrap();
470
471 create_test_db(source_dir.path());
472
473 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
474 let snapshot = manager.create_snapshot(snapshot_dir.path()).unwrap();
475
476 manager
478 .restore_snapshot(&snapshot, restore_dir.path())
479 .unwrap();
480
481 assert!(restore_dir.path().join("MANIFEST").exists());
483 assert!(restore_dir.path().join("sst/L0_001.sst").exists());
484 }
485
486 #[test]
487 fn test_delete_snapshot() {
488 let source_dir = tempdir().unwrap();
489 let dest_dir = tempdir().unwrap();
490
491 create_test_db(source_dir.path());
492
493 let mut manager = SnapshotManager::new(source_dir.path()).unwrap();
494 let snapshot = manager.create_snapshot(dest_dir.path()).unwrap();
495
496 assert_eq!(manager.list_snapshots().len(), 1);
497
498 let deleted = manager.delete_snapshot(&snapshot.id).unwrap();
499 assert!(deleted);
500 assert!(manager.list_snapshots().is_empty());
501 }
502
503 #[test]
504 fn test_checksum_verification() {
505 let source_dir = tempdir().unwrap();
506
507 create_test_db(source_dir.path());
508
509 let checksum =
511 SnapshotManager::compute_checksum(&source_dir.path().join("MANIFEST")).unwrap();
512 assert!(checksum > 0);
513 }
514
515 #[test]
516 fn test_snapshot_without_wal() {
517 let source_dir = tempdir().unwrap();
518 let dest_dir = tempdir().unwrap();
519
520 create_test_db(source_dir.path());
521
522 let config = SnapshotConfig {
523 include_wal: false,
524 ..Default::default()
525 };
526
527 let mut manager = SnapshotManager::with_config(source_dir.path(), config).unwrap();
528 let snapshot = manager.create_snapshot(dest_dir.path()).unwrap();
529
530 assert!(!snapshot
532 .files
533 .iter()
534 .any(|f| f.relative_path.contains("wal")));
535 }
536}