use crate::engine::types::{DbError, LogEntry};
use std::fs::{File, OpenOptions};
use std::path::Path;
use std::time::SystemTime;
use std::io::{BufWriter, Write};
pub fn snapshot_path(log_path: &str) -> String {
format!("{}.snapshot.bin", log_path)
}
pub fn write_snapshot(log_path: &str, entries: &[LogEntry], seq: u64) -> Result<(), DbError> {
let path = snapshot_path(log_path);
let tmp = format!("{}.tmp", path);
let file = OpenOptions::new()
.create(true) .write(true)
.truncate(true) .open(&tmp)?;
let mut w = BufWriter::new(file);
w.write_all(b"MOLTSNAP")?;
w.write_all(&seq.to_le_bytes())?;
let count = entries.len() as u64;
w.write_all(&count.to_le_bytes())?;
for entry in entries {
let encoded = serde_json::to_vec(entry).map_err(|_| DbError::WriteError)?;
let len = encoded.len() as u64;
w.write_all(&len.to_le_bytes())?;
w.write_all(&encoded)?;
}
w.flush()?;
drop(w);
if Path::new(&path).exists() {
let log_dir = Path::new(log_path).parent().unwrap_or_else(|| Path::new("."));
let backup_dir = log_dir.join("backup");
std::fs::create_dir_all(&backup_dir)?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let filename = Path::new(&path).file_name()
.and_then(|n| n.to_str())
.unwrap_or("snapshot.bin");
let backup_path = backup_dir.join(format!("{}.{}.bak", filename, now));
let _ = std::fs::rename(&path, &backup_path);
}
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn load_snapshot(log_path: &str) -> Option<(Vec<LogEntry>, u64)> {
let path = snapshot_path(log_path);
if !Path::new(&path).exists() {
return None;
}
tracing::info!("🔍 Attempting to load snapshot from {}", path);
let mut file = File::open(&path).ok()?;
use std::io::Read;
let mut magic = [0u8; 8];
file.read_exact(&mut magic).ok()?;
if &magic != b"MOLTSNAP" {
tracing::warn!("❌ Invalid snapshot magic header");
return None; }
let mut seq_bytes = [0u8; 8];
file.read_exact(&mut seq_bytes).ok()?;
let seq = u64::from_le_bytes(seq_bytes);
let mut count_bytes = [0u8; 8];
file.read_exact(&mut count_bytes).ok()?;
let count = u64::from_le_bytes(count_bytes) as usize;
tracing::info!("📂 Snapshot header: seq={}, count={}", seq, count);
let mut entries = Vec::with_capacity(count);
for i in 0..count {
let mut len_bytes = [0u8; 8];
if let Err(e) = file.read_exact(&mut len_bytes) {
tracing::error!("❌ Failed to read entry {} length: {}", i, e);
return None;
}
let len = u64::from_le_bytes(len_bytes) as usize;
let mut buf = vec![0u8; len];
if let Err(e) = file.read_exact(&mut buf) {
tracing::error!("❌ Failed to read entry {} data: {}", i, e);
return None;
}
if len > 0 && buf.iter().all(|&b| b == 0) {
tracing::error!("❌ Entry {} data is all zeros. Snapshot might be corrupt.", i);
return None;
}
let entry: LogEntry = match serde_json::from_slice(&buf) {
Ok(e) => e,
Err(err) => {
let sample = if buf.len() > 20 { &buf[..20] } else { &buf };
tracing::error!(
"❌ Failed to deserialize entry {} (len {}): {}. Sample: {:?}. This usually happens if the snapshot was created with an older version of MoltenDB or is corrupt. Falling back to log replay.",
i, len, err, sample
);
return None;
}
};
entries.push(entry);
}
Some((entries, seq))
}