use std::{
fs::{self, File},
io::{Read, Write},
path::{Path, PathBuf},
};
use oxgraph_snapshot::{Snapshot, SnapshotBuilder};
use serde::{Deserialize, Serialize};
use crate::{CommitSeq, DbError, TransactionId, state::DatabaseState};
const STORE_FILE: &str = "store.oxgdb";
const TEMP_STORE_FILE: &str = "store.oxgdb.tmp";
const SNAPSHOT_KIND_DB_STATE: u32 = 0x0300;
const SNAPSHOT_DB_STATE_VERSION: u32 = 1;
const SNAPSHOT_DB_STATE_ALIGNMENT_LOG2: u8 = 0;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct StoredDatabase {
pub(crate) commit_seq: CommitSeq,
pub(crate) transaction_id: TransactionId,
pub(crate) state: DatabaseState,
}
impl StoredDatabase {
#[must_use]
pub(crate) const fn empty() -> Self {
Self {
commit_seq: CommitSeq::new(0),
transaction_id: TransactionId::new(0),
state: DatabaseState::empty(),
}
}
}
#[must_use]
pub(crate) fn store_path(root: &Path) -> PathBuf {
root.join(STORE_FILE)
}
pub(crate) fn write_store(root: &Path, stored: &StoredDatabase) -> Result<(), DbError> {
fs::create_dir_all(root).map_err(|error| DbError::io("create database directory", error))?;
let bytes = encode_store(stored)?;
let temp_path = root.join(TEMP_STORE_FILE);
let mut file = File::create(&temp_path).map_err(|error| DbError::io("create store", error))?;
file.write_all(&bytes)
.map_err(|error| DbError::io("write store payload", error))?;
file.flush()
.map_err(|error| DbError::io("flush store", error))?;
file.sync_all()
.map_err(|error| DbError::io("sync store", error))?;
fs::rename(temp_path, store_path(root)).map_err(|error| DbError::io("publish store", error))?;
sync_directory(root)?;
Ok(())
}
fn encode_store(stored: &StoredDatabase) -> Result<Vec<u8>, DbError> {
let payload = serde_json::to_vec(stored)?;
let mut builder = SnapshotBuilder::new();
builder
.add_section(
SNAPSHOT_KIND_DB_STATE,
SNAPSHOT_DB_STATE_VERSION,
SNAPSHOT_DB_STATE_ALIGNMENT_LOG2,
payload,
)
.map_err(|error| DbError::invalid_store(error.to_string()))?;
builder
.finish()
.map_err(|error| DbError::invalid_store(error.to_string()))
}
pub(crate) fn read_store(root: &Path) -> Result<StoredDatabase, DbError> {
let mut file = File::open(store_path(root)).map_err(|error| match error.kind() {
std::io::ErrorKind::NotFound => DbError::NotFound,
_kind => DbError::io("open store", error),
})?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|error| DbError::io("read store", error))?;
let snapshot =
Snapshot::open(&bytes).map_err(|error| DbError::invalid_store(error.to_string()))?;
let section = snapshot
.section(SNAPSHOT_KIND_DB_STATE)
.ok_or_else(|| DbError::invalid_store("store is missing the database-state section"))?;
let stored: StoredDatabase = serde_json::from_slice(section.bytes())?;
if encode_store(&stored)? != bytes {
return Err(DbError::invalid_store("store bytes are not canonical"));
}
stored.state.validate()?;
Ok(stored)
}
pub(crate) fn validate_store(root: &Path) -> Result<(), DbError> {
read_store(root).map(|_stored| ())
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> Result<(), DbError> {
let directory =
File::open(path).map_err(|error| DbError::io("open database directory", error))?;
directory
.sync_all()
.map_err(|error| DbError::io("sync database directory", error))
}
#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> Result<(), DbError> {
Ok(())
}