use std::path::PathBuf;
use std::time::Duration;
use anyhow::Context as _;
use super::record::{Area, OutputRoot};
pub(crate) trait MarkSet {
const NAME: &'static str;
type Area: Area;
const DIR: &'static str;
const LIFETIME: Duration;
}
pub(crate) struct ReplayMarks;
impl MarkSet for ReplayMarks {
const NAME: &'static str = "replay mark";
type Area = OutputRoot;
const DIR: &'static str = "jti";
const LIFETIME: Duration = Duration::from_secs(15 * 60);
}
#[derive(Debug, PartialEq, Eq)]
#[must_use]
pub(crate) enum Marked {
Fresh,
Already,
}
pub(crate) struct Marks<M: MarkSet> {
dir: PathBuf,
set: std::marker::PhantomData<fn() -> M>,
}
impl<M: MarkSet> Marks<M> {
pub(super) fn new(root: &std::path::Path) -> Self {
Self {
dir: root.join(M::DIR),
set: std::marker::PhantomData,
}
}
pub(crate) fn mark_once(&self, name: &MarkName) -> anyhow::Result<Marked> {
std::fs::create_dir_all(&self.dir)
.with_context(|| format!("create the {} directory", M::NAME))?;
expire(&self.dir, M::LIFETIME);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(self.dir.join(&name.0))
{
Ok(_) => Ok(Marked::Fresh),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(Marked::Already),
Err(error) => Err(error).with_context(|| format!("write a {}", M::NAME)),
}
}
}
pub(crate) struct MarkName(String);
impl MarkName {
pub(crate) fn of(parts: &str) -> Self {
use sha2::{Digest as _, Sha256};
Self(crate::cas::Digest::from_raw(Sha256::digest(parts).into()).hex())
}
}
pub(crate) fn expire(dir: &std::path::Path, lifetime: Duration) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let old = entry
.metadata()
.and_then(|meta| meta.modified())
.is_ok_and(|modified| modified.elapsed().is_ok_and(|age| age > lifetime));
if old {
let _ = std::fs::remove_file(entry.path());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_name_is_marked_once_and_the_rest_are_told_so() {
let dir = tempfile::tempdir().unwrap();
let marks = Marks::<ReplayMarks>::new(dir.path());
let name = MarkName::of("did:plc:a|jti-1");
assert_eq!(marks.mark_once(&name).unwrap(), Marked::Fresh);
assert_eq!(marks.mark_once(&name).unwrap(), Marked::Already);
assert_eq!(
marks.mark_once(&MarkName::of("did:plc:a|jti-2")).unwrap(),
Marked::Fresh
);
}
}