rustlite_snapshot/
lib.rs

1//! # RustLite Snapshot Manager
2//!
3//! Snapshot and backup functionality for RustLite databases.
4//!
5//! ## ⚠️ Internal Implementation Detail
6//!
7//! **This crate is an internal implementation detail of RustLite.**
8//!
9//! Users should depend on the main [`rustlite`](https://crates.io/crates/rustlite) crate
10//! instead, which provides the stable public API. This crate's API may change
11//! without notice between minor versions.
12//!
13//! ```toml
14//! # In your Cargo.toml - use the main crate, not this one:
15//! [dependencies]
16//! rustlite = "0.3"
17//! ```
18//!
19//! ---
20//!
21//! This crate provides point-in-time snapshot and backup capabilities
22//! for RustLite databases, enabling:
23//!
24//! - **Point-in-time snapshots**: Create consistent snapshots without blocking writes
25//! - **Backup and restore**: Full database backups for disaster recovery
26//! - **Incremental snapshots**: Copy only changed files since last snapshot
27//!
28//! ## Usage
29//!
30//! ```ignore
31//! use rustlite_snapshot::{SnapshotManager, SnapshotConfig};
32//!
33//! let manager = SnapshotManager::new("/path/to/db", SnapshotConfig::default())?;
34//! let snapshot = manager.create_snapshot("/path/to/backup")?;
35//! println!("Snapshot created at: {}", snapshot.path);
36//! ```
37
38use 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
47/// Snapshot metadata file name
48const SNAPSHOT_META_FILE: &str = "SNAPSHOT_META";
49
50/// Snapshot metadata
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SnapshotMeta {
53    /// Unique snapshot ID
54    pub id: String,
55    /// Timestamp when snapshot was created (Unix milliseconds)
56    pub timestamp: u64,
57    /// Path where snapshot is stored
58    pub path: String,
59    /// Source database path
60    pub source_path: String,
61    /// Sequence number at snapshot time
62    pub sequence: u64,
63    /// List of files included in the snapshot
64    pub files: Vec<SnapshotFile>,
65    /// Total size in bytes
66    pub total_size: u64,
67    /// Snapshot type (full or incremental)
68    pub snapshot_type: SnapshotType,
69    /// Parent snapshot ID (for incremental snapshots)
70    pub parent_id: Option<String>,
71}
72
73/// File included in a snapshot
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SnapshotFile {
76    /// Relative path within the database directory
77    pub relative_path: String,
78    /// File size in bytes
79    pub size: u64,
80    /// Last modified timestamp
81    pub modified: u64,
82    /// Checksum (CRC32)
83    pub checksum: u32,
84}
85
86/// Type of snapshot
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub enum SnapshotType {
89    /// Full snapshot - includes all files
90    Full,
91    /// Incremental snapshot - only changed files since parent
92    Incremental,
93}
94
95/// Snapshot configuration
96#[derive(Debug, Clone)]
97pub struct SnapshotConfig {
98    /// Include WAL files in snapshot
99    pub include_wal: bool,
100    /// Verify checksums after copy
101    pub verify_checksums: bool,
102    /// Compression level (0 = none, 1-9 = gzip levels)
103    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
116/// Snapshot manager
117pub struct SnapshotManager {
118    /// Source database directory
119    source_dir: PathBuf,
120    /// Configuration
121    config: SnapshotConfig,
122    /// List of created snapshots
123    snapshots: Vec<SnapshotMeta>,
124}
125
126impl SnapshotManager {
127    /// Create a new snapshot manager for the given database directory
128    pub fn new(source_dir: impl AsRef<Path>) -> Result<Self> {
129        Self::with_config(source_dir, SnapshotConfig::default())
130    }
131
132    /// Create a new snapshot manager with custom configuration
133    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    /// Create a full snapshot of the database
151    pub fn create_snapshot(&mut self, dest: impl AsRef<Path>) -> Result<SnapshotMeta> {
152        let dest = dest.as_ref().to_path_buf();
153
154        // Create destination directory
155        fs::create_dir_all(&dest)?;
156
157        // Generate snapshot ID
158        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        // Collect files to copy
165        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        // Copy files
176        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            // Create parent directories
181            if let Some(parent) = dst_path.parent() {
182                fs::create_dir_all(parent)?;
183            }
184
185            // Copy file
186            fs::copy(&src_path, &dst_path)?;
187
188            // Verify if configured
189            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        // Get sequence number from manifest
201        let sequence = self.read_sequence()?;
202
203        // Create metadata
204        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        // Write metadata file
217        self.write_metadata(&dest, &meta)?;
218
219        // Track snapshot
220        self.snapshots.push(meta.clone());
221
222        Ok(meta)
223    }
224
225    /// Collect all files to include in the snapshot
226    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            // Skip certain directories/files
242            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            // Skip WAL if not configured
248            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    /// Compute CRC32 checksum of a file
287    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    /// Read sequence number from manifest
305    fn read_sequence(&self) -> Result<u64> {
306        // Try to read from manifest
307        let manifest_path = self.source_dir.join("MANIFEST");
308        if !manifest_path.exists() {
309            return Ok(0);
310        }
311
312        // For now, return 0 - in a real implementation, we'd parse the manifest
313        Ok(0)
314    }
315
316    /// Write snapshot metadata to file
317    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    /// Load snapshot metadata from a snapshot directory
331    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    /// Restore a database from a snapshot
346    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        // Create destination directory
351        fs::create_dir_all(&dest)?;
352
353        // Copy all files from snapshot
354        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            // Create parent directories
359            if let Some(parent) = dst_path.parent() {
360                fs::create_dir_all(parent)?;
361            }
362
363            // Copy file
364            if src_path.exists() {
365                fs::copy(&src_path, &dst_path)?;
366            }
367        }
368
369        Ok(())
370    }
371
372    /// List all tracked snapshots
373    pub fn list_snapshots(&self) -> &[SnapshotMeta] {
374        &self.snapshots
375    }
376
377    /// Delete a snapshot
378    pub fn delete_snapshot(&mut self, snapshot_id: &str) -> Result<bool> {
379        // Find and remove from tracking
380        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            // Delete the directory
386            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    /// Get snapshot by ID
398    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        // Create a mock database structure
410        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        // Verify files were copied
442        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        // Load the snapshot from disk
459        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        // Restore to new location
477        manager
478            .restore_snapshot(&snapshot, restore_dir.path())
479            .unwrap();
480
481        // Verify files were restored
482        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        // Compute checksum
510        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        // WAL should not be included
531        assert!(!snapshot
532            .files
533            .iter()
534            .any(|f| f.relative_path.contains("wal")));
535    }
536}