/* wearechat - generic multiplayer game with voip Copyright (C) 2025 metamuffin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use crate::{helper::ReadWrite, packets::Resource}; use anyhow::Result; use log::info; use redb::{Database, TableDefinition}; use std::{ collections::HashMap, env::var, fs::{File, create_dir_all, rename}, io::{Read, Write}, marker::PhantomData, path::{Path, PathBuf}, sync::Mutex, }; const T_ENTRIES: TableDefinition<[u8; 32], &[u8]> = TableDefinition::new("e"); pub enum ResourceStore { Redb(Database), Filesystem(PathBuf), Memory(Mutex>>), } impl ResourceStore { pub fn new_env() -> Result { if var("WEARECHAT_RES_CACHE_USE_REDB").is_ok() { ResourceStore::new_redb( &xdg::BaseDirectories::with_prefix("wearechat")? .place_cache_file("resources.db")?, ) } else { ResourceStore::new_filesystem( &xdg::BaseDirectories::with_prefix("wearechat")? .create_cache_directory("resources")?, ) } } pub fn new_filesystem(path: &Path) -> Result { info!("using filesystem resource cache in {path:?}"); create_dir_all(path)?; Ok(Self::Filesystem(path.to_owned())) } pub fn new_redb(path: &Path) -> Result { info!("initializing redb resource cache..."); let db = Database::create(path)?; let txn = db.begin_write()?; txn.open_table(T_ENTRIES)?; txn.commit()?; info!("done"); Ok(Self::Redb(db)) } pub fn new_memory() -> Self { Self::Memory(HashMap::new().into()) } pub fn get(&self, key: Resource) -> Result> { self.get_raw(Resource(key.0, PhantomData))? .map(|b| T::read(&mut b.as_slice())) .transpose() } pub fn set(&self, value: &T) -> Result> { Ok(Resource(self.set_raw(&value.write_alloc())?.0, PhantomData)) } pub fn get_raw(&self, key: Resource) -> Result>> { match self { ResourceStore::Redb(database) => { let txn = database.begin_read()?; let ent = txn.open_table(T_ENTRIES)?; match ent.get(key.0)? { Some(x) => Ok(Some(x.value().to_vec())), None => Ok(None), } } ResourceStore::Memory(map) => Ok(map.lock().unwrap().get(&key).map(|x| x.to_vec())), ResourceStore::Filesystem(root) => { let path = fs_cache_path(&root, key); if path.exists() { let mut buf = Vec::new(); File::open(path)?.read_to_end(&mut buf)?; Ok(Some(buf)) } else { Ok(None) } } } } pub fn set_raw(&self, value: &[u8]) -> Result { let key = Resource(resource_hash(value), PhantomData); match self { ResourceStore::Redb(database) => { let txn = database.begin_write()?; let mut ent = txn.open_table(T_ENTRIES)?; ent.insert(key.0, value)?; drop(ent); txn.commit()?; } ResourceStore::Memory(map) => { map.lock().unwrap().insert(key, value.to_vec()); } ResourceStore::Filesystem(root) => { let path = fs_cache_path(&root, key); let path_temp = path.with_extension("part"); File::create(&path_temp)?.write_all(value)?; rename(path_temp, path)?; } } Ok(key) } pub fn iter(&self, mut cb: impl FnMut(&[u8])) -> Result<()> { match self { ResourceStore::Redb(_database) => todo!(), ResourceStore::Filesystem(_root) => todo!(), ResourceStore::Memory(mutex) => { mutex.lock().unwrap().values().for_each(|v| cb(v)); Ok(()) } } } } pub fn resource_hash(x: &[u8]) -> [u8; 32] { let mut hasher = blake3::Hasher::new(); hasher.update(x); hasher.finalize().into() } fn fs_cache_path(path: &Path, res: Resource) -> PathBuf { path.join(format!( "{:016x}{:016x}{:016x}{:016x}", u64::from_be_bytes(res.0[0..8].try_into().unwrap()), u64::from_be_bytes(res.0[8..16].try_into().unwrap()), u64::from_be_bytes(res.0[16..24].try_into().unwrap()), u64::from_be_bytes(res.0[24..32].try_into().unwrap()), )) }