diff options
| author | metamuffin <metamuffin@disroot.org> | 2025-12-08 19:53:12 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2025-12-08 19:53:12 +0100 |
| commit | 6edf0fd93abf7e58b4c0974e3d3e54bcf8517946 (patch) | |
| tree | 32577db9d987897d4037ba9af0084b95b55e145c /cache/src | |
| parent | e4584a8135584e6591bac7d5397cf227cf3cff92 (diff) | |
| download | jellything-6edf0fd93abf7e58b4c0974e3d3e54bcf8517946.tar jellything-6edf0fd93abf7e58b4c0974e3d3e54bcf8517946.tar.bz2 jellything-6edf0fd93abf7e58b4c0974e3d3e54bcf8517946.tar.zst | |
human-readable cache keys
Diffstat (limited to 'cache/src')
| -rw-r--r-- | cache/src/backends/filesystem.rs | 20 | ||||
| -rw-r--r-- | cache/src/backends/mod.rs | 5 | ||||
| -rw-r--r-- | cache/src/helper.rs | 46 | ||||
| -rw-r--r-- | cache/src/key.rs | 83 | ||||
| -rw-r--r-- | cache/src/lib.rs | 47 |
5 files changed, 85 insertions, 116 deletions
diff --git a/cache/src/backends/filesystem.rs b/cache/src/backends/filesystem.rs index 39fb7a2..9a9db9c 100644 --- a/cache/src/backends/filesystem.rs +++ b/cache/src/backends/filesystem.rs @@ -4,12 +4,11 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{CacheKey, Config, backends::CacheStorage}; +use crate::{Config, backends::CacheStorage}; use anyhow::Result; -use base64::Engine; use rand::random; use std::{ - fs::{File, rename}, + fs::{File, create_dir_all, rename}, io::{ErrorKind, Read, Write}, path::PathBuf, }; @@ -20,25 +19,22 @@ impl Filesystem { pub fn new(config: &Config) -> Self { Self(config.path.clone()) } - fn path(&self, key: CacheKey) -> PathBuf { - let filename = base64::engine::general_purpose::URL_SAFE.encode(key.0); - let filename = &filename[..30]; // 180 bits - self.0.join(filename) - } fn temp_path(&self) -> PathBuf { self.0.join(format!("temp-{:016x}", random::<u128>())) } } impl CacheStorage for Filesystem { - fn store(&self, key: CacheKey, value: &[u8]) -> Result<()> { + fn store(&self, key: String, value: &[u8]) -> Result<()> { let temp = self.temp_path(); + let out = self.0.join(&key); + create_dir_all(out.parent().unwrap())?; File::create(&temp)?.write_all(value)?; - rename(temp, self.path(key))?; + rename(temp, out)?; Ok(()) } - fn read(&self, key: CacheKey) -> Result<Option<Vec<u8>>> { - match File::open(self.path(key)) { + fn read(&self, key: &str) -> Result<Option<Vec<u8>>> { + match File::open(self.0.join(key)) { Ok(mut f) => { let mut data = Vec::new(); f.read_to_end(&mut data)?; diff --git a/cache/src/backends/mod.rs b/cache/src/backends/mod.rs index 370c5ab..6b7dac3 100644 --- a/cache/src/backends/mod.rs +++ b/cache/src/backends/mod.rs @@ -5,10 +5,9 @@ */ pub mod filesystem; -use crate::CacheKey; use anyhow::Result; pub(crate) trait CacheStorage: Send + Sync + 'static { - fn store(&self, key: CacheKey, value: &[u8]) -> Result<()>; - fn read(&self, key: CacheKey) -> Result<Option<Vec<u8>>>; + fn store(&self, key: String, value: &[u8]) -> Result<()>; + fn read(&self, key: &str) -> Result<Option<Vec<u8>>>; } diff --git a/cache/src/helper.rs b/cache/src/helper.rs new file mode 100644 index 0000000..8f73e1e --- /dev/null +++ b/cache/src/helper.rs @@ -0,0 +1,46 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ + +use base64::{Engine, prelude::BASE64_URL_SAFE}; +use sha2::Sha256; +use std::{ + fmt::Display, + hash::{Hash, Hasher}, +}; + +pub struct HashKey<T>(pub T); +impl<T: Hash> Display for HashKey<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use sha2::Digest; + struct ShaHasher(Sha256); + impl Hasher for ShaHasher { + fn finish(&self) -> u64 { + unreachable!() + } + fn write(&mut self, bytes: &[u8]) { + self.0.update(bytes); + } + } + let mut d = ShaHasher(sha2::Sha256::new()); + self.0.hash(&mut d); + let d = d.0.finalize(); + f.write_str(&BASE64_URL_SAFE.encode(d)) + } +} + +pub struct EscapeKey<T>(pub T); +impl<T: Display> Display for EscapeKey<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + percent_encoding::utf8_percent_encode( + &self.0.to_string(), + percent_encoding::NON_ALPHANUMERIC, + ) + ) + } +} diff --git a/cache/src/key.rs b/cache/src/key.rs deleted file mode 100644 index d8ca510..0000000 --- a/cache/src/key.rs +++ /dev/null @@ -1,83 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2025 metamuffin <metamuffin.org> -*/ -use crate::{CACHE_GENERATION_BUCKET_COUNT, CONF}; -use anyhow::bail; -use base64::{Engine, prelude::BASE64_URL_SAFE}; -use sha2::Sha256; -use std::{ - fmt::Display, - hash::{Hash, Hasher}, - str::FromStr, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct CacheKey(pub [u8; 32]); - -impl CacheKey { - pub fn new(ty: CacheContentType, seed: impl Hash) -> Self { - use sha2::Digest; - struct ShaHasher(Sha256); - impl Hasher for ShaHasher { - fn finish(&self) -> u64 { - unreachable!() - } - fn write(&mut self, bytes: &[u8]) { - self.0.update(bytes); - } - } - let mut d = ShaHasher(sha2::Sha256::new()); - d.0.update(CONF.secret.as_bytes()); - seed.hash(&mut d); - let d = d.0.finalize(); - let mut key: [u8; 32] = d.as_slice().try_into().unwrap(); - key[0] = ty as u8; - Self(key) - } - pub fn new_json(seed: impl Hash) -> Self { - Self::new(CacheContentType::Json, seed) - } - pub fn new_image(seed: impl Hash) -> Self { - Self::new(CacheContentType::Image, seed) - } - pub fn content_type(&self) -> CacheContentType { - match self.0[0] { - 1 => CacheContentType::Image, - 2 => CacheContentType::Json, - _ => CacheContentType::Unknown, - } - } - pub(super) fn bucket(&self) -> usize { - (self.0[1] as usize - | ((self.0[2] as usize) << 8) - | ((self.0[3] as usize) << 16) - | ((self.0[4] as usize) << 24)) - % CACHE_GENERATION_BUCKET_COUNT - } -} - -impl Display for CacheKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&BASE64_URL_SAFE.encode(self.0)) - } -} -impl FromStr for CacheKey { - type Err = anyhow::Error; - fn from_str(s: &str) -> Result<Self, Self::Err> { - let mut out = [0; 32]; - let size = BASE64_URL_SAFE.decode_slice(s, &mut out)?; - if size != out.len() { - bail!("cache key parse invalid size") - } - Ok(Self(out)) - } -} - -#[repr(u8)] -pub enum CacheContentType { - Unknown = 0, - Image = 1, - Json = 2, -} diff --git a/cache/src/lib.rs b/cache/src/lib.rs index fbda2cf..20e1424 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -3,17 +3,17 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -pub mod backends; -pub mod key; +mod backends; +mod helper; use crate::backends::{CacheStorage, filesystem::Filesystem}; use anyhow::{Context, Result, anyhow}; -pub use key::*; -use log::info; +use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::{ any::Any, collections::{BTreeMap, HashMap}, + hash::{DefaultHasher, Hash, Hasher}, path::PathBuf, sync::{ Arc, LazyLock, Mutex, OnceLock, RwLock, @@ -22,11 +22,12 @@ use std::{ time::Instant, }; +pub use helper::{EscapeKey, HashKey}; + #[derive(Debug, Deserialize)] pub struct Config { path: PathBuf, max_in_memory_cache_size: usize, - secret: String, } const CACHE_GENERATION_BUCKET_COUNT: usize = 1024; @@ -54,23 +55,29 @@ pub fn init_cache() -> Result<()> { Ok(()) } -pub fn cache(key: CacheKey, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<Vec<u8>> { +fn bucket(key: &str) -> usize { + let mut h = DefaultHasher::new(); + key.hash(&mut h); + h.finish() as usize % CACHE_GENERATION_BUCKET_COUNT +} + +pub fn cache(key: &str, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<Vec<u8>> { // we need a lock even if it exists since somebody might be still in the process of writing. let already_within = WITHIN_CACHE_FILE.with(|a| a.swap(true, Ordering::Relaxed)); let _guard = if already_within { // TODO stupid hack to avoid deadlock for nested calls; not locking is fine but might cause double-generating - CACHE_GENERATION_LOCKS[key.bucket()].try_lock().ok() + CACHE_GENERATION_LOCKS[bucket(key)].try_lock().ok() } else { - CACHE_GENERATION_LOCKS[key.bucket()].lock().ok() + CACHE_GENERATION_LOCKS[bucket(key)].lock().ok() }; let store = CACHE_STORE.get().unwrap(); - let out = match store.read(key)? { + let out = match store.read(&key)? { Some(x) => x, None => { let value = generate()?; - store.store(key, &value)?; + store.store(key.to_owned(), &value)?; value } }; @@ -82,11 +89,11 @@ pub fn cache(key: CacheKey, generate: impl FnOnce() -> Result<Vec<u8>>) -> Resul Ok(out) } -pub fn cache_read(key: CacheKey) -> Result<Option<Vec<u8>>> { +pub fn cache_read(key: &str) -> Result<Option<Vec<u8>>> { CACHE_STORE.get().unwrap().read(key) } -pub fn cache_store(key: CacheKey, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<CacheKey> { - cache(key, generate)?; +pub fn cache_store(key: String, generate: impl FnOnce() -> Result<Vec<u8>>) -> Result<String> { + cache(&key, generate)?; Ok(key) } @@ -95,18 +102,22 @@ pub struct InMemoryCacheEntry { last_access: Instant, object: Arc<dyn Any + Send + Sync + 'static>, } -pub static CACHE_IN_MEMORY_OBJECTS: LazyLock<RwLock<HashMap<CacheKey, InMemoryCacheEntry>>> = +pub static CACHE_IN_MEMORY_OBJECTS: LazyLock<RwLock<HashMap<String, InMemoryCacheEntry>>> = LazyLock::new(|| RwLock::new(HashMap::new())); pub static CACHE_IN_MEMORY_SIZE: AtomicUsize = AtomicUsize::new(0); -pub fn cache_memory<Fun, T>(key: CacheKey, mut generate: Fun) -> Result<Arc<T>, anyhow::Error> +pub fn cache_memory<Fun, T>(key: &str, mut generate: Fun) -> Result<Arc<T>, anyhow::Error> where Fun: FnMut() -> Result<T, anyhow::Error>, T: Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, { + if !key.ends_with(".json") { + warn!("cache_memory key not ending in .json: {key:?}") + } + { let mut g = CACHE_IN_MEMORY_OBJECTS.write().unwrap(); - if let Some(entry) = g.get_mut(&key) { + if let Some(entry) = g.get_mut(key) { entry.last_access = Instant::now(); let object = entry .object @@ -117,7 +128,7 @@ where } } - let data = cache(key, move || { + let data = cache(&key, move || { let object = generate()?; Ok(serde_json::to_vec(&object)?) })?; @@ -128,7 +139,7 @@ where { let mut g = CACHE_IN_MEMORY_OBJECTS.write().unwrap(); g.insert( - key, + key.to_owned(), InMemoryCacheEntry { size, last_access: Instant::now(), |