use std::path::{Path, PathBuf};
use localcache::{CacheEngine, CacheOptions, CacheStatus, ChangeDetectionMode};
use serde::{Serialize, de::DeserializeOwned};
pub(crate) const NAMESPACE_IMAGE: &str = "image";
pub(crate) const NAMESPACE_VIDEO: &str = "video";
pub(crate) const IMAGE_PAYLOAD_VERSION: u32 = 1;
pub(crate) const VIDEO_PAYLOAD_VERSION: u32 = 1;
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("cache engine error: {0}")]
Engine(#[from] localcache::LocalFileCacheError),
#[error("thumbnail generation failed: {0}")]
ThumbnailGenerationFailed(String),
#[error("I/O error for '{path}': {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("v1 cache migration failed: {0}")]
Migration(String),
}
impl CacheError {
pub(crate) fn io(path: &Path, source: std::io::Error) -> Self {
CacheError::Io {
path: path.to_string_lossy().into_owned(),
source,
}
}
}
pub type Result<T> = std::result::Result<T, CacheError>;
#[derive(Debug, Clone)]
pub enum DbLocation {
Custom(PathBuf),
AppCache(Option<String>),
WorkDir(Option<String>),
}
impl Default for DbLocation {
fn default() -> Self {
Self::WorkDir(None)
}
}
impl DbLocation {
pub fn resolve(&self) -> PathBuf {
match self {
Self::Custom(p) => p.clone(),
Self::AppCache(name) => {
let base = std::env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".cache"))
.unwrap_or_else(|_| PathBuf::from(".cache"))
});
let app = std::env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
.unwrap_or_else(|| "app".to_string());
base.join(app).join(name.as_deref().unwrap_or("cache.db"))
}
Self::WorkDir(name) => {
PathBuf::from(format!("./{}", name.as_deref().unwrap_or("cache.db")))
}
}
}
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub db_location: DbLocation,
pub read_conns: u32,
pub thumbnail_dir: Option<PathBuf>,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
db_location: DbLocation::default(),
read_conns: num_cpus(),
thumbnail_dir: None,
}
}
}
fn num_cpus() -> u32 {
std::thread::available_parallelism()
.map(|n| n.get() as u32)
.unwrap_or(4)
}
pub(crate) fn ensure_db_dir(options: &CacheOptions) -> Result<()> {
if let Some(parent) = options.database_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| CacheError::io(parent, e))?;
}
Ok(())
}
pub(crate) fn cache_options(
config: &CacheConfig,
namespace: &str,
payload_version: u32,
) -> CacheOptions {
CacheOptions {
database_path: config.db_location.resolve(),
change_detection_mode: ChangeDetectionMode::MetadataThenFullHash,
namespace: namespace.to_owned(),
payload_version,
..CacheOptions::default()
}
}
pub(crate) fn ensure_schema<T>(options: &CacheOptions) -> Result<()>
where
T: Serialize + DeserializeOwned,
{
let _engine: CacheEngine<T> = CacheEngine::open(options.clone())?;
Ok(())
}
pub(crate) fn read_pool_size(config: &CacheConfig) -> usize {
(config.read_conns.max(1)) as usize
}
pub(crate) fn is_fresh(status: &CacheStatus) -> bool {
matches!(status, CacheStatus::Fresh)
}