aboutsummaryrefslogtreecommitdiff
path: root/cache/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-12-08 19:53:12 +0100
committermetamuffin <metamuffin@disroot.org>2025-12-08 19:53:12 +0100
commit6edf0fd93abf7e58b4c0974e3d3e54bcf8517946 (patch)
tree32577db9d987897d4037ba9af0084b95b55e145c /cache/src
parente4584a8135584e6591bac7d5397cf227cf3cff92 (diff)
downloadjellything-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.rs20
-rw-r--r--cache/src/backends/mod.rs5
-rw-r--r--cache/src/helper.rs46
-rw-r--r--cache/src/key.rs83
-rw-r--r--cache/src/lib.rs47
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(),