use std::io;
use std::path::Path;
use std::time::UNIX_EPOCH;
pub type Ident = (u64, u64);
pub fn ident_of(meta: &std::fs::Metadata) -> Ident {
let mtime_nanos = meta
.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
(meta.len(), mtime_nanos)
}
fn not_found_is_none<T>(r: io::Result<T>) -> io::Result<Option<T>> {
match r {
Ok(v) => Ok(Some(v)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiskState {
pub snapshot: Option<Ident>,
pub wal_len: u64,
pub wal: Option<u64>,
pub intent_nonempty: bool,
}
pub fn observe(dir: &Path) -> io::Result<DiskState> {
let snapshot =
not_found_is_none(std::fs::metadata(dir.join("snapshot.bin")))?.map(|m| ident_of(&m));
let wal_meta = not_found_is_none(std::fs::metadata(dir.join("wal.bin")))?;
let wal_len = wal_meta.as_ref().map_or(0, |m| m.len());
let wal = wal_meta.as_ref().and_then(file_ident);
let intent_nonempty = not_found_is_none(std::fs::metadata(dir.join("snapshot.intent")))?
.is_some_and(|m| m.len() > 0);
Ok(DiskState {
snapshot,
wal_len,
wal,
intent_nonempty,
})
}
impl DiskState {
pub fn unchanged_until(&self, after: &DiskState) -> bool {
!self.intent_nonempty
&& !after.intent_nonempty
&& self.snapshot == after.snapshot
&& self.wal == after.wal
&& after.wal_len >= self.wal_len
}
}
fn file_ident(meta: &std::fs::Metadata) -> Option<u64> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
Some(meta.ino())
}
#[cfg(not(unix))]
{
let _ = meta;
None
}
}
pub fn open_snapshot(dir: &Path) -> io::Result<Option<(std::fs::File, Ident)>> {
let Some(f) = not_found_is_none(std::fs::File::open(dir.join("snapshot.bin")))? else {
return Ok(None);
};
let ident = ident_of(&f.metadata()?);
Ok(Some((f, ident)))
}
pub fn local_archives(dir: &Path) -> io::Result<Vec<(u64, u64)>> {
let mut out = Vec::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name();
if let Some(n) = crate::manifest::parse_archive_name(&name.to_string_lossy()) {
if let Some(m) = not_found_is_none(entry.metadata())? {
out.push((n, m.len()));
}
}
}
out.sort_unstable();
Ok(out)
}
pub fn read_side(
dir: &Path,
name: &str,
known: Option<Ident>,
) -> io::Result<Option<(Ident, Option<Vec<u8>>)>> {
let path = dir.join(name);
let Some(meta) = not_found_is_none(std::fs::metadata(&path))? else {
return Ok(None);
};
let ident = ident_of(&meta);
if known == Some(ident) {
return Ok(Some((ident, None)));
}
match not_found_is_none(std::fs::read(&path))? {
Some(bytes) => Ok(Some((ident, Some(bytes)))),
None => Ok(None),
}
}
pub const LINEAGE_FILE: &str = "backup.lineage";
pub fn read_lineage(dir: &Path) -> io::Result<Option<String>> {
Ok(
not_found_is_none(std::fs::read_to_string(dir.join(LINEAGE_FILE)))?
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()),
)
}
pub fn create_lineage(dir: &Path, id: &str) -> io::Result<()> {
use std::io::Write as _;
let tmp = dir.join(format!("{LINEAGE_FILE}.tmp"));
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(id.as_bytes())?;
f.sync_all()?;
}
std::fs::rename(&tmp, dir.join(LINEAGE_FILE))?;
#[cfg(unix)]
std::fs::File::open(dir)?.sync_all()?;
Ok(())
}
pub const PARENT_FILE: &str = "backup.parent";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ParentRecord {
pub lineage: String,
pub manifest: String,
pub commit_seq: u64,
}
pub fn read_parent(dir: &Path) -> io::Result<Option<ParentRecord>> {
Ok(not_found_is_none(std::fs::read(dir.join(PARENT_FILE)))?
.and_then(|b| serde_json::from_slice(&b).ok()))
}
pub fn create_parent(dir: &Path, record: &ParentRecord) -> io::Result<()> {
use std::io::Write as _;
let bytes = serde_json::to_vec(record).map_err(io::Error::other)?;
let tmp = dir.join(format!("{PARENT_FILE}.tmp"));
{
let mut f = std::fs::File::create(&tmp)?;
f.write_all(&bytes)?;
f.sync_all()?;
}
std::fs::rename(&tmp, dir.join(PARENT_FILE))?;
#[cfg(unix)]
std::fs::File::open(dir)?.sync_all()?;
Ok(())
}
pub fn crc32_of(file: &mut std::fs::File) -> io::Result<String> {
use std::io::{Read as _, Seek as _, SeekFrom};
file.seek(SeekFrom::Start(0))?;
let mut h = crc32fast::Hasher::new();
let mut buf = vec![0u8; 1 << 20];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
h.update(&buf[..n]);
}
file.seek(SeekFrom::Start(0))?;
Ok(crate::manifest::format_crc32(h.finalize()))
}
pub fn floor(dir: &Path) -> u64 {
std::fs::read(dir.join("wal.floor"))
.ok()
.and_then(|b| {
b.get(..8)
.map(|s| u64::from_le_bytes(s.try_into().unwrap()))
})
.unwrap_or(0)
}