use std::path::{Path, PathBuf};
use std::sync::Arc;
use localcache::{CacheEntry, CacheStatus, ConnectionPool, ReadPool};
use rayon::prelude::*;
use crate::core::engine::{
CacheConfig, DbLocation, IMAGE_PAYLOAD_VERSION, NAMESPACE_IMAGE, Result, cache_options,
ensure_db_dir, ensure_schema, is_fresh, read_pool_size,
};
use crate::core::payload::ImagePayload;
use crate::core::thumbnail::{generate_image_thumbnail, thumbnail_dest};
use crate::types::{
CacheRead, DirCacheSummary, ImageCacheEntry, ImageFeatures, LookupResult, UpsertImageRequest,
};
#[derive(Debug, Clone, Default)]
pub struct ImageCacheConfig {
pub cache_config: CacheConfig,
}
#[derive(Clone)]
pub struct ImageCacheWriter {
write: ConnectionPool<ImagePayload>,
read: ReadPool<ImagePayload>,
config: Arc<ImageCacheConfig>,
}
impl ImageCacheWriter {
pub fn as_session(config: ImageCacheConfig) -> Result<Self> {
let options = cache_options(&config.cache_config, NAMESPACE_IMAGE, IMAGE_PAYLOAD_VERSION);
ensure_db_dir(&options)?;
let write = ConnectionPool::open(options.clone())?;
write.with(|e| e.purge_stale_versions())?;
let read = ReadPool::open(options, read_pool_size(&config.cache_config))?;
Ok(Self {
write,
read,
config: Arc::new(config),
})
}
pub fn onetime(location: DbLocation) -> Result<Self> {
Self::as_session(ImageCacheConfig {
cache_config: CacheConfig {
db_location: location,
..CacheConfig::default()
},
})
}
pub fn upsert(&self, req: UpsertImageRequest) -> Result<()> {
let status = self.write.check_status(&req.path)?;
self.write_payload(&req, status)
}
pub fn upsert_all(&self, reqs: Vec<UpsertImageRequest>) -> Vec<(PathBuf, Result<()>)> {
let prepared: Vec<(UpsertImageRequest, Result<Prepared>)> = reqs
.into_par_iter()
.map(|req| {
let prep = self.prepare(&req);
(req, prep)
})
.collect();
prepared
.into_iter()
.map(|(req, prep)| {
let path = req.path.clone();
let result = prep.and_then(|p| self.commit(&req, p));
(path, result)
})
.collect()
}
pub fn delete(&self, path: &Path) -> Result<bool> {
Ok(self.write.remove(path)?)
}
pub fn delete_in_dir(&self, dir: &Path) -> Result<usize> {
let entries = self.read.query_run(|q| q.path_in_dir(dir, false))?;
let mut removed = 0;
for entry in entries {
if let Some(thumb) = &entry.payload.thumbnail_path {
let _ = std::fs::remove_file(thumb);
}
if self.write.remove(&entry.path)? {
removed += 1;
}
}
Ok(removed)
}
pub fn list_paths(&self) -> Result<Vec<String>> {
let keys = self.write.with(|e| e.keys(None))?;
Ok(keys
.into_iter()
.map(|p| p.to_string_lossy().into_owned())
.collect())
}
pub fn as_reader(&self) -> ImageCacheReader {
ImageCacheReader {
read: self.read.clone(),
}
}
pub fn lookup(&self, path: &Path) -> Result<LookupResult<ImageCacheEntry>> {
self.as_reader().lookup(path)
}
fn prepare(&self, req: &UpsertImageRequest) -> Result<Prepared> {
let status = self.read.check_status(&req.path)?;
let thumbnail = self.ensure_thumbnail(&req.path, &status)?;
Ok(Prepared { status, thumbnail })
}
fn commit(&self, req: &UpsertImageRequest, prep: Prepared) -> Result<()> {
let existing = if is_fresh(&prep.status) {
self.write.get(&req.path)?.map(|e| e.payload)
} else {
None
};
if is_fresh(&prep.status)
&& req.clip_vector.is_none()
&& let Some(p) = &existing
&& (prep.thumbnail.is_none() || p.thumbnail_path == prep.thumbnail)
{
return Ok(());
}
let mut payload = existing.unwrap_or_default();
if let Some(t) = prep.thumbnail {
payload.thumbnail_path = Some(t);
}
if let Some(v) = &req.clip_vector {
payload.clip_vector = Some(v.clone());
}
self.write.set(&req.path, &payload)?;
Ok(())
}
fn write_payload(&self, req: &UpsertImageRequest, status: CacheStatus) -> Result<()> {
let thumbnail = self.ensure_thumbnail(&req.path, &status)?;
self.commit(req, Prepared { status, thumbnail })
}
fn ensure_thumbnail(&self, path: &Path, status: &CacheStatus) -> Result<Option<String>> {
let Some(thumb_dir) = &self.config.cache_config.thumbnail_dir else {
return Ok(None);
};
let dest = thumbnail_dest(thumb_dir, path)?;
if !dest.exists() || !is_fresh(status) {
generate_image_thumbnail(path, &dest)?;
}
Ok(Some(dest.to_string_lossy().into_owned()))
}
}
struct Prepared {
status: CacheStatus,
thumbnail: Option<String>,
}
#[derive(Clone)]
pub struct ImageCacheReader {
read: ReadPool<ImagePayload>,
}
impl ImageCacheReader {
pub fn as_session(config: ImageCacheConfig) -> Result<Self> {
let options = cache_options(&config.cache_config, NAMESPACE_IMAGE, IMAGE_PAYLOAD_VERSION);
ensure_db_dir(&options)?;
ensure_schema::<ImagePayload>(&options)?;
let read = ReadPool::open(options, read_pool_size(&config.cache_config))?;
Ok(Self { read })
}
pub fn onetime(location: DbLocation) -> Result<Self> {
Self::as_session(ImageCacheConfig {
cache_config: CacheConfig {
db_location: location,
..CacheConfig::default()
},
})
}
pub fn lookup(&self, path: &Path) -> Result<LookupResult<ImageCacheEntry>> {
let canonical = match path.canonicalize() {
Ok(p) => p,
Err(_) => return Ok(LookupResult::Miss),
};
match self.read.check_status(&canonical)? {
CacheStatus::Missing => Ok(LookupResult::Miss),
CacheStatus::Stale => Ok(LookupResult::Invalidated),
CacheStatus::Fresh => match self.read.get(&canonical)? {
None => Ok(LookupResult::Miss),
Some(entry) => Ok(LookupResult::Hit(to_image_entry(entry))),
},
}
}
pub fn lookup_all(
&self,
paths: &[&Path],
) -> Vec<(PathBuf, Result<LookupResult<ImageCacheEntry>>)> {
paths
.par_iter()
.map(|p| (p.to_path_buf(), self.lookup(p)))
.collect()
}
pub fn check(&self, path: &Path) -> Result<bool> {
Ok(is_fresh(&self.read.check_status(path)?))
}
pub fn list_paths(&self) -> Result<Vec<String>> {
Ok(self
.read
.keys(None)?
.into_iter()
.map(|p| p.to_string_lossy().into_owned())
.collect())
}
pub fn all(&self) -> Result<Vec<Result<ImageCacheEntry>>> {
let entries = self.read.query_run(|q| q)?;
Ok(entries.into_iter().map(|e| Ok(to_image_entry(e))).collect())
}
pub fn summarize_by_dir(&self) -> Result<Vec<DirCacheSummary>> {
summarize_entries(self.read.list_entries()?)
}
pub fn all_in_dir(&self, path: &Path) -> Result<Vec<Result<ImageCacheEntry>>> {
let dir = dir_of(path);
let entries = self.read.query_run(|q| q.path_in_dir(dir, false))?;
Ok(entries.into_iter().map(|e| Ok(to_image_entry(e))).collect())
}
pub fn all_in_dir_and_sub_dirs(&self, path: &Path) -> Result<Vec<Result<ImageCacheEntry>>> {
let dir = dir_of(path);
let entries = self.read.query_run(|q| q.path_in_dir(dir, true))?;
Ok(entries.into_iter().map(|e| Ok(to_image_entry(e))).collect())
}
}
impl CacheRead for ImageCacheReader {
fn check(&self, path: &Path) -> Result<bool> {
ImageCacheReader::check(self, path)
}
fn check_all(&self, paths: &[&Path]) -> Vec<(PathBuf, Result<bool>)> {
paths
.par_iter()
.map(|p| (p.to_path_buf(), self.check(p)))
.collect()
}
fn list_paths(&self) -> Result<Vec<String>> {
ImageCacheReader::list_paths(self)
}
}
pub(crate) fn summarize_entries(
entries: Vec<localcache::EntryInfo>,
) -> Result<Vec<DirCacheSummary>> {
use std::collections::BTreeMap;
let mut map: BTreeMap<PathBuf, (usize, u64, i64)> = BTreeMap::new();
for e in entries {
let dir = e.path.parent().map(|p| p.to_path_buf()).unwrap_or_default();
let agg = map.entry(dir).or_insert((0, 0, 0));
agg.0 += 1;
agg.1 += e.metadata.file_size;
agg.2 = agg.2.max(e.updated_at);
}
Ok(map
.into_iter()
.map(
|(dir, (file_count, total_size, latest_cached_at))| DirCacheSummary {
dir_path: dir.to_string_lossy().into_owned(),
file_count,
total_size,
latest_cached_at,
},
)
.collect())
}
fn dir_of(path: &Path) -> &Path {
if path.is_dir() {
path
} else {
path.parent().unwrap_or(path)
}
}
fn to_image_entry(entry: CacheEntry<ImagePayload>) -> ImageCacheEntry {
ImageCacheEntry {
path: entry.path.to_string_lossy().into_owned(),
thumbnail_path: entry.payload.thumbnail_path,
features: entry
.payload
.clip_vector
.map(|v| ImageFeatures { clip_vector: v }),
}
}