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 =
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    /// Load snapshot metadata from a snapshot directory
332    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    /// Restore a database from a snapshot
347    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        // Create destination directory
352        fs::create_dir_all(&dest)?;
353
354        // Copy all files from snapshot
355        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            // Create parent directories
360            if let Some(parent) = dst_path.parent() {
361                fs::create_dir_all(parent)?;
362            }
363
364            // Copy file
365            if src_path.exists() {
366                fs::copy(&src_path, &dst_path)?;
367            }
368        }
369
370        Ok(())
371    }
372
373    /// List all tracked snapshots
374    pub fn list_snapshots(&self) -> &[SnapshotMeta] {
375        &self.snapshots
376    }
377
378    /// Delete a snapshot
379    pub fn delete_snapshot(&mut self, snapshot_id: &str) -> Result<bool> {
380        // Find and remove from tracking
381        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            // Delete the directory
387            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    /// Get snapshot by ID
399    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        // Create a mock database structure
411        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        // Verify files were copied
443        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        // Load the snapshot from disk
460        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        // Restore to new location
478        manager.restore_snapshot(&snapshot, restore_dir.path()).unwrap();
479        
480        // Verify files were restored
481        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        // Compute checksum
509        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        // WAL should not be included
529        assert!(!snapshot.files.iter().any(|f| f.relative_path.contains("wal")));
530    }
531}