use std::sync::Arc;
use fraiseql_error::Result;
use super::TransformParams;
#[cfg(test)]
use super::{ImageTransformer, TransformOutput};
use crate::backend::LocalBackend;
pub struct TransformCache {
backend: Arc<LocalBackend>,
}
impl TransformCache {
#[must_use]
pub const fn new(backend: Arc<LocalBackend>) -> Self {
Self { backend }
}
#[must_use]
pub fn build_cache_key(key: &str, params: &TransformParams) -> String {
let width = params.width.map_or_else(|| "auto".to_string(), |w| w.to_string());
let height = params.height.map_or_else(|| "auto".to_string(), |h| h.to_string());
let format = params
.format
.as_ref()
.map_or_else(|| "original".to_string(), |f| format!("{f:?}").to_lowercase());
let quality = params.quality.map_or_else(|| "default".to_string(), |q| q.to_string());
format!("_transforms/{key}/{width}_{height}_{format}_{quality}")
}
#[cfg(test)]
pub async fn get_or_transform(
&self,
key: &str,
source_data: &[u8],
params: &TransformParams,
) -> Result<Option<TransformOutput>> {
use sha2::{Digest, Sha256};
let cache_key = Self::build_cache_key(key, params);
let mut hasher = Sha256::new();
hasher.update(source_data);
let source_hash = format!("{:x}", hasher.finalize());
if let Ok(cached_data) = self.backend.download(&cache_key).await {
let metadata_key = format!("{}_meta", cache_key);
if let Ok(metadata) = self.backend.download(&metadata_key).await {
if let Ok(metadata_str) = String::from_utf8(metadata) {
if metadata_str == source_hash {
if let Ok(cached) = serde_json::from_slice::<TransformOutput>(&cached_data)
{
return Ok(Some(cached));
}
}
}
}
}
let output = ImageTransformer::transform(source_data, params)?;
let serialized = serde_json::to_vec(&output)?;
self.backend.upload(&cache_key, &serialized, "application/octet-stream").await?;
let metadata_key = format!("{}_meta", cache_key);
self.backend.upload(&metadata_key, source_hash.as_bytes(), "text/plain").await?;
Ok(Some(output))
}
#[cfg(test)]
pub async fn get_source(&self, key: &str) -> Result<Vec<u8>> {
self.backend.download(key).await
}
pub async fn invalidate(&self, key: &str) -> Result<()> {
let invalidation_key = format!("_transforms/{}/_invalidated", key);
let timestamp = chrono::Utc::now().to_rfc3339();
self.backend
.upload(&invalidation_key, timestamp.as_bytes(), "text/plain")
.await?;
Ok(())
}
}