use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::UNIX_EPOCH;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use sha2::{Digest, Sha256};
use super::Clock;
const MAX_AGE_SECS: u64 = 7 * 24 * 60 * 60;
static TEMP_NONCE: AtomicU64 = AtomicU64::new(0);
pub struct ReplayStash {
dir: PathBuf,
}
impl ReplayStash {
pub fn new(cache_root: impl Into<PathBuf>) -> Self {
ReplayStash {
dir: cache_root.into().join("brazen").join("replay"),
}
}
fn path(&self, key: &str) -> Option<PathBuf> {
let named = !key.is_empty() && !key.starts_with('.') && !key.contains(['/', '\\']);
named.then(|| self.dir.join(key))
}
pub fn stash(&self, key: &str, payload: &[u8], clock: &dyn Clock) -> io::Result<()> {
let path = self.path(key).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("replay key {key:?} is not a file name"),
)
})?;
fs::create_dir_all(&self.dir)?;
let nonce = TEMP_NONCE.fetch_add(1, Ordering::Relaxed);
let tmp = self
.dir
.join(format!(".{}.{nonce}.tmp", std::process::id()));
let mut file = fs::File::create(&tmp)?;
file.write_all(payload)?;
file.sync_all()?;
fs::rename(&tmp, path)?;
self.prune(clock.now());
Ok(())
}
pub fn recall(&self, key: &str) -> Option<Vec<u8>> {
fs::read(self.path(key)?).ok()
}
fn prune(&self, now: u64) {
for entry in fs::read_dir(&self.dir).into_iter().flatten().flatten() {
if let Some(stale) = stale_path(&entry, now) {
let _ = fs::remove_file(stale);
}
}
}
}
fn stale_path(entry: &fs::DirEntry, now: u64) -> Option<PathBuf> {
let modified = entry.metadata().ok()?.modified().ok()?;
let secs = modified
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
(now.saturating_sub(secs) > MAX_AGE_SECS).then(|| entry.path())
}
pub fn content_key(text: &str) -> String {
URL_SAFE_NO_PAD.encode(Sha256::digest(text.as_bytes()))
}