use std::io::Write;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::ServerState;
const MAGIC_V1: &[u8; 8] = b"SPGBKUP\x01";
const MAGIC_V2: &[u8; 8] = b"SPGBKUP\x02";
const KIND_FULL: u8 = 0;
const KIND_INCREMENTAL: u8 = 1;
#[derive(Debug)]
pub enum BackupError {
Io(std::io::Error),
NoWal,
BadSinceOffset(u64),
#[allow(dead_code)]
Corrupt {
expected: u32,
computed: u32,
},
}
impl core::fmt::Display for BackupError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Io(e) => write!(f, "backup io: {e}"),
Self::NoWal => f.write_str("server has no WAL configured — backup requires WAL"),
Self::BadSinceOffset(n) => {
write!(f, "incremental SINCE offset {n} exceeds current WAL length")
}
Self::Corrupt { expected, computed } => write!(
f,
"backup bundle CRC32 mismatch (expected={expected:#010x}, computed={computed:#010x})"
),
}
}
}
impl From<std::io::Error> for BackupError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
pub fn take_full_backup(state: &ServerState, dest: &Path) -> Result<u64, BackupError> {
let Some(wal_path) = state.wal_path.clone() else {
return Err(BackupError::NoWal);
};
let (snapshot, wal_pos) = {
let guard = state
.engine
.write()
.map_err(|_| std::io::Error::other("engine lock poisoned"))?;
let snap = guard.snapshot();
drop(guard);
let pos = std::fs::metadata(&wal_path).map_or(0, |m| m.len());
(snap, pos)
};
write_bundle(dest, KIND_FULL, 0, &snapshot, wal_pos, &[])?;
Ok(wal_pos)
}
pub fn take_incremental_backup(
state: &ServerState,
dest: &Path,
since: u64,
) -> Result<u64, BackupError> {
let Some(wal_path) = state.wal_path.clone() else {
return Err(BackupError::NoWal);
};
let (wal_bytes, wal_pos) = {
let guard = state
.engine
.write()
.map_err(|_| std::io::Error::other("engine lock poisoned"))?;
let wb = std::fs::read(&wal_path)?;
drop(guard);
let pos = u64::try_from(wb.len()).unwrap_or(u64::MAX);
(wb, pos)
};
let since_usize = usize::try_from(since).unwrap_or(usize::MAX);
if since_usize > wal_bytes.len() {
return Err(BackupError::BadSinceOffset(since));
}
let slice = &wal_bytes[since_usize..];
write_bundle(dest, KIND_INCREMENTAL, since, &[], wal_pos, slice)?;
Ok(wal_pos)
}
fn write_bundle(
dest: &Path,
kind: u8,
since: u64,
snapshot: &[u8],
wal_pos: u64,
wal_slice: &[u8],
) -> std::io::Result<()> {
let ts = u64::try_from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_micros()),
)
.unwrap_or(u64::MAX);
let mut body = Vec::with_capacity(
MAGIC_V2.len() + 1 + 8 + 8 + 8 + snapshot.len() + 8 + 8 + wal_slice.len(),
);
body.extend_from_slice(MAGIC_V2);
body.push(kind);
body.extend_from_slice(&since.to_le_bytes());
body.extend_from_slice(&ts.to_le_bytes());
let snap_len = u64::try_from(snapshot.len()).unwrap_or(u64::MAX);
body.extend_from_slice(&snap_len.to_le_bytes());
body.extend_from_slice(snapshot);
body.extend_from_slice(&wal_pos.to_le_bytes());
let wal_len = u64::try_from(wal_slice.len()).unwrap_or(u64::MAX);
body.extend_from_slice(&wal_len.to_le_bytes());
body.extend_from_slice(wal_slice);
let crc = spg_crypto::crc32::crc32(&body);
let mut f = std::fs::File::create(dest)?;
f.write_all(&body)?;
f.write_all(&crc.to_le_bytes())?;
f.sync_data()?;
Ok(())
}
#[allow(dead_code)] pub fn inspect_bundle(path: &Path) -> std::io::Result<BundleHeader> {
let bytes = std::fs::read(path)?;
if bytes.len() < 8 + 1 + 8 + 8 + 8 + 8 + 8 {
return Err(std::io::Error::other("bundle too short"));
}
let is_v2 = if &bytes[..8] == MAGIC_V1 {
false
} else if &bytes[..8] == MAGIC_V2 {
true
} else {
return Err(std::io::Error::other("bad bundle magic"));
};
let kind = bytes[8];
let since = u64::from_le_bytes(bytes[9..17].try_into().unwrap());
let ts = u64::from_le_bytes(bytes[17..25].try_into().unwrap());
let snap_len = u64::from_le_bytes(bytes[25..33].try_into().unwrap());
let snap_end = 33 + usize::try_from(snap_len).unwrap_or(usize::MAX);
if bytes.len() < snap_end + 16 {
return Err(std::io::Error::other("bundle truncated in body"));
}
let wal_pos = u64::from_le_bytes(bytes[snap_end..snap_end + 8].try_into().unwrap());
let wal_len = u64::from_le_bytes(bytes[snap_end + 8..snap_end + 16].try_into().unwrap());
if is_v2 {
let body_end = snap_end + 16 + usize::try_from(wal_len).unwrap_or(usize::MAX);
if bytes.len() != body_end + 4 {
return Err(std::io::Error::other(
"v2 bundle: trailing CRC missing or extra bytes after CRC",
));
}
let expected = u32::from_le_bytes(bytes[body_end..body_end + 4].try_into().unwrap());
let computed = spg_crypto::crc32::crc32(&bytes[..body_end]);
if expected != computed {
return Err(std::io::Error::other(format!(
"backup bundle CRC32 mismatch (expected={expected:#010x}, computed={computed:#010x})"
)));
}
}
Ok(BundleHeader {
kind,
since,
ts_micros: ts,
snap_len,
wal_pos,
wal_len,
})
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BundleHeader {
pub kind: u8,
pub since: u64,
pub ts_micros: u64,
pub snap_len: u64,
pub wal_pos: u64,
pub wal_len: u64,
}