use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use anyhow::Context as _;
use serde::Serialize;
use serde::de::DeserializeOwned;
use super::write::{self, Access, Durability};
pub(crate) trait Area {
const ACCESS: Access;
}
pub(crate) struct OutputRoot;
pub(crate) struct SealedDir;
impl Area for OutputRoot {
const ACCESS: Access = Access::Shared;
}
impl Area for SealedDir {
const ACCESS: Access = Access::Private;
}
pub(crate) trait Record {
const NAME: &'static str;
type Area: Area;
const VERSION: u32;
const DURABILITY: Durability;
type Key;
type Value;
fn relative(key: &Self::Key) -> PathBuf;
fn encode(value: &Self::Value) -> anyhow::Result<Vec<u8>>;
fn decode(bytes: &[u8]) -> anyhow::Result<Self::Value>;
}
pub(crate) trait Put: Record {}
pub(crate) trait Once: Record {}
/// refresh tokens are single use, and two refreshes at once replay one.
pub(crate) trait Guarded: Record {
/// don't unlink the lock file, a new inode lets new holders skip the old ones.
fn lock(key: &Self::Key) -> PathBuf;
}
pub(crate) trait Locked: Record {
const LOCK: &'static str;
fn initial() -> Self::Value;
}
#[derive(Serialize, serde::Deserialize)]
struct Stored<T> {
version: u32,
#[serde(flatten)]
value: T,
}
pub(crate) fn encode_json<T: Serialize>(version: u32, value: &T) -> anyhow::Result<Vec<u8>> {
Ok(serde_json::to_vec_pretty(&Stored { version, value })?)
}
pub(crate) fn decode_json<T: DeserializeOwned>(
version: u32,
name: &str,
bytes: &[u8],
) -> anyhow::Result<T> {
let stored: Stored<T> =
serde_json::from_slice(bytes).with_context(|| format!("decode the {name}"))?;
anyhow::ensure!(
stored.version == version,
"the {name} is version {}, this knot writes {version}",
stored.version
);
Ok(stored.value)
}
pub(crate) struct Rec<'a, R: Record> {
root: &'a Path,
key: R::Key,
path: PathBuf,
record: PhantomData<fn() -> R>,
}
impl<'a, R: Record> Rec<'a, R> {
pub(super) fn new(root: &'a Path, key: &R::Key) -> Self
where
R::Key: Clone,
{
Self {
root,
key: key.clone(),
path: root.join(R::relative(key)),
record: PhantomData,
}
}
/// bad bytes are an error, not `None`, or a caller starts empty and writes over them.
pub(crate) fn read(&self) -> anyhow::Result<Option<R::Value>> {
match std::fs::read(&self.path) {
Ok(bytes) => Ok(Some(R::decode(&bytes)?)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error)
.with_context(|| format!("read the {} at {}", R::NAME, self.path.display())),
}
}
pub(crate) fn remove(&self) -> anyhow::Result<()> {
match std::fs::remove_file(&self.path) {
Err(error) if error.kind() != std::io::ErrorKind::NotFound => Err(error)
.with_context(|| format!("remove the {} at {}", R::NAME, self.path.display())),
_ => Ok(()),
}
}
#[cfg(test)]
pub(crate) fn path(&self) -> &Path {
&self.path
}
fn store(&self, value: &R::Value) -> anyhow::Result<()> {
let bytes = R::encode(value)?;
write::publish_as(&self.path, &bytes, R::DURABILITY, R::Area::ACCESS)
.with_context(|| format!("write the {} at {}", R::NAME, self.path.display()))
}
}
impl<R: Put> Rec<'_, R> {
pub(crate) fn put(&self, value: &R::Value) -> anyhow::Result<()> {
self.store(value)
}
}
impl<R: Once> Rec<'_, R> {
pub(crate) fn publish_once(
&self,
make: impl FnOnce() -> anyhow::Result<R::Value>,
) -> anyhow::Result<R::Value> {
if let Some(value) = self.read()? {
return Ok(value);
}
let value = make()?;
let bytes = R::encode(&value)?;
match write::publish_once(&self.path, &bytes, R::DURABILITY, R::Area::ACCESS)
.with_context(|| format!("publish the {} at {}", R::NAME, self.path.display()))?
{
write::Published::Mine => Ok(value),
write::Published::Existing(bytes) => R::decode(&bytes),
}
}
}
impl<'a, R: Guarded> Rec<'a, R> {
pub(crate) fn guard(&'a self) -> anyhow::Result<RecordGuard<'a, R>> {
let lock = self.root.join(R::lock(&self.key));
if let Some(parent) = lock.parent() {
(R::Area::ACCESS.create_dir_all(parent))
.with_context(|| format!("create {}", parent.display()))?;
}
let file = (R::Area::ACCESS.open_options())
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&lock)
.with_context(|| format!("open {}", lock.display()))?;
file.lock()
.with_context(|| format!("lock the {}", R::NAME))?;
Ok(RecordGuard {
record: self,
_file: file,
})
}
}
pub(crate) struct RecordGuard<'a, R: Guarded> {
record: &'a Rec<'a, R>,
_file: std::fs::File,
}
impl<R: Guarded> RecordGuard<'_, R> {
pub(crate) fn read(&self) -> anyhow::Result<Option<R::Value>> {
self.record.read()
}
pub(crate) fn put(&self, value: &R::Value) -> anyhow::Result<()> {
self.record.store(value)
}
}
impl<R: Locked> Rec<'_, R> {
pub(crate) fn update<T>(&self, change: impl FnOnce(&mut R::Value) -> T) -> anyhow::Result<T> {
let mut held = self.hold()?;
let result = change(held.value());
held.commit()?;
Ok(result)
}
pub(crate) fn hold(&self) -> anyhow::Result<Held<'_, R>> {
let lock = crate::output::lock_named(self.root, R::LOCK)?;
let value = self.read()?.unwrap_or_else(R::initial);
Ok(Held {
record: self,
stored: R::encode(&value)?,
value,
_lock: lock,
})
}
}
pub(crate) struct Held<'a, R: Locked> {
record: &'a Rec<'a, R>,
stored: Vec<u8>,
value: R::Value,
_lock: std::fs::File,
}
impl<R: Locked> Held<'_, R> {
pub(crate) fn value(&mut self) -> &mut R::Value {
&mut self.value
}
pub(crate) fn commit(self) -> anyhow::Result<()> {
let bytes = R::encode(&self.value)?;
if bytes == self.stored {
return Ok(());
}
write::publish_as(&self.record.path, &bytes, R::DURABILITY, R::Area::ACCESS)
.with_context(|| format!("write the {} at {}", R::NAME, self.record.path.display()))
}
}