use std::{
collections::BTreeMap,
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
sync::Mutex,
};
use super::{
entry::{ResultCacheEntry, ResultCacheError},
key::{entry_id, CacheKey},
};
pub trait ResultCache {
fn get(
&self,
key: &CacheKey,
check_name: &str,
) -> Result<Option<ResultCacheEntry>, ResultCacheError>;
fn put(&self, entry: &ResultCacheEntry) -> Result<(), ResultCacheError>;
}
#[derive(Debug, Default)]
pub struct MemoryResultCache {
entries: Mutex<BTreeMap<String, ResultCacheEntry>>,
}
impl MemoryResultCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn entries(&self) -> Vec<ResultCacheEntry> {
self.entries
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.values()
.cloned()
.collect()
}
}
impl ResultCache for MemoryResultCache {
fn get(
&self,
key: &CacheKey,
check_name: &str,
) -> Result<Option<ResultCacheEntry>, ResultCacheError> {
let entries = self
.entries
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
Ok(entries
.get(&entry_id(key, check_name))
.filter(|entry| entry.is_valid_for(key, check_name))
.cloned())
}
fn put(&self, entry: &ResultCacheEntry) -> Result<(), ResultCacheError> {
let key = entry.cache_key();
self.entries
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(entry_id(&key, &entry.check_name), entry.clone());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FsResultCache {
root: PathBuf,
}
impl FsResultCache {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
fn path_for(&self, key: &CacheKey, check_name: &str) -> PathBuf {
let id = entry_id(key, check_name);
self.root.join(&id[..2]).join(format!("{id}.json"))
}
}
impl ResultCache for FsResultCache {
fn get(
&self,
key: &CacheKey,
check_name: &str,
) -> Result<Option<ResultCacheEntry>, ResultCacheError> {
let path = self.path_for(key, check_name);
let mut file = match File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error.into()),
};
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let Ok(entry) = serde_json::from_slice::<ResultCacheEntry>(&bytes) else {
return Ok(None);
};
if entry.is_valid_for(key, check_name) {
Ok(Some(entry))
} else {
Ok(None)
}
}
fn put(&self, entry: &ResultCacheEntry) -> Result<(), ResultCacheError> {
let key = entry.cache_key();
let path = self.path_for(&key, &entry.check_name);
let bytes = serde_json::to_vec(entry).map_err(|error| {
std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string())
})?;
write_atomic(&path, &bytes)
}
}
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), ResultCacheError> {
let parent = path.parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"result cache entry path has no parent",
)
})?;
std::fs::create_dir_all(parent)?;
let unique = format!(
".{}.{}-{}.tmp",
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("entry"),
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0)
);
let tmp = parent.join(unique);
let written = write_tmp_then_rename(&tmp, path, bytes);
if written.is_err() {
let _cleanup = std::fs::remove_file(&tmp);
}
written
}
fn write_tmp_then_rename(tmp: &Path, dest: &Path, bytes: &[u8]) -> Result<(), ResultCacheError> {
let mut file = File::create(tmp)?;
file.write_all(bytes)?;
file.sync_all()?;
std::fs::rename(tmp, dest)?;
Ok(())
}