use crate::{GraphDb, GraphError, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, Eq)]
pub enum RestoreOutcome {
AlreadyPresent,
Restored {
from: PathBuf,
files: Vec<String>,
bytes: u64,
},
Empty,
}
const RESTORE_FILES: [&str; 6] = [
"snapshot.bin",
"snapshot.bin.bak",
"wal.bin",
"wal.floor",
"wal.genesis",
"roles.json",
];
pub fn holds_a_store(dir: &Path) -> bool {
if dir.join("snapshot.bin").is_file() {
return true;
}
std::fs::metadata(dir.join("wal.bin"))
.map(|m| m.is_file() && m.len() > 0)
.unwrap_or(false)
}
fn backup_mtime(dir: &Path) -> Option<std::time::SystemTime> {
["snapshot.bin", "wal.bin"]
.iter()
.filter_map(|n| {
std::fs::metadata(dir.join(n))
.and_then(|m| m.modified())
.ok()
})
.max()
}
fn choose_backup(from: &Path) -> Option<PathBuf> {
if holds_a_store(from) {
return Some(from.to_path_buf());
}
let entries = std::fs::read_dir(from).ok()?;
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
for entry in entries.flatten() {
let dir = entry.path();
if !holds_a_store(&dir) {
continue;
}
if dir.file_name().map(|n| n == "latest").unwrap_or(false) {
return Some(dir);
}
let Some(mtime) = backup_mtime(&dir) else {
continue;
};
let better = match &best {
None => true,
Some((best_mtime, best_dir)) => (mtime, &dir) > (*best_mtime, best_dir),
};
if better {
best = Some((mtime, dir));
}
}
best.map(|(_, dir)| dir)
}
pub fn restore_if_empty(db_dir: &Path, from: &Path) -> Result<RestoreOutcome> {
if holds_a_store(db_dir) {
return Ok(RestoreOutcome::AlreadyPresent);
}
let Some(backup) = choose_backup(from) else {
return Ok(RestoreOutcome::Empty);
};
std::fs::create_dir_all(db_dir)
.map_err(|e| failed(format!("restore into {}: {e}", db_dir.display())))?;
let staging = db_dir.join(format!(".restore-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&staging); std::fs::create_dir_all(&staging)
.map_err(|e| failed(format!("restore into {}: {e}", staging.display())))?;
let outcome = stage_and_install(db_dir, &staging, &backup);
let _ = std::fs::remove_dir_all(&staging);
outcome
}
fn failed(msg: String) -> GraphError {
GraphError::Io(std::io::Error::other(msg))
}
fn stage_and_install(db_dir: &Path, staging: &Path, backup: &Path) -> Result<RestoreOutcome> {
let mut names: Vec<String> = RESTORE_FILES.iter().map(|n| n.to_string()).collect();
let mut archives: Vec<String> = std::fs::read_dir(backup)
.map_err(|e| failed(format!("restore from {}: {e}", backup.display())))?
.flatten()
.filter_map(|e| e.file_name().into_string().ok())
.filter(|n| n.starts_with("wal.") && n.ends_with(".archive"))
.collect();
archives.sort();
names.extend(archives);
let mut files = Vec::new();
let mut bytes = 0u64;
for name in names {
let src = backup.join(&name);
if !src.is_file() {
continue;
}
let n = std::fs::copy(&src, staging.join(&name))
.map_err(|e| failed(format!("restore {} from {}: {e}", name, backup.display())))?;
bytes += n;
files.push(name);
}
GraphDb::<core_storage::fs::RealFs>::open(staging).map_err(|e| {
failed(format!(
"restore into {} from {} failed: the copy does not open: {e}",
db_dir.display(),
backup.display()
))
})?;
let mut moved: Vec<&String> = Vec::new();
for name in &files {
if let Err(e) = std::fs::rename(staging.join(name), db_dir.join(name)) {
for done in &moved {
let _ = std::fs::remove_file(db_dir.join(done));
}
return Err(failed(format!(
"restore into {} from {}: installing {name}: {e}",
db_dir.display(),
backup.display()
)));
}
moved.push(name);
}
Ok(RestoreOutcome::Restored {
from: backup.to_path_buf(),
files,
bytes,
})
}