2026-02-08 13:40:23 +01:00
|
|
|
|
//! Thumbnail Port - Application layer abstraction for thumbnail generation.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! This module defines the port (trait) for thumbnail operations,
|
|
|
|
|
|
//! keeping the application and interface layers independent of specific
|
|
|
|
|
|
//! image processing implementations.
|
|
|
|
|
|
|
2026-02-14 01:29:34 +01:00
|
|
|
|
use crate::common::errors::DomainError;
|
2026-02-08 13:40:23 +01:00
|
|
|
|
use bytes::Bytes;
|
2026-02-14 01:29:34 +01:00
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
use std::sync::Arc;
|
2026-02-08 13:40:23 +01:00
|
|
|
|
|
|
|
|
|
|
/// Thumbnail sizes supported by the system.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
|
|
|
|
pub enum ThumbnailSize {
|
|
|
|
|
|
/// Small icon for file listings (150×150)
|
|
|
|
|
|
Icon,
|
|
|
|
|
|
/// Medium preview for gallery view (400×400)
|
|
|
|
|
|
Preview,
|
|
|
|
|
|
/// Large preview for detail view (800×800)
|
|
|
|
|
|
Large,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ThumbnailSize {
|
|
|
|
|
|
/// Get the maximum dimension for this size.
|
|
|
|
|
|
pub fn max_dimension(&self) -> u32 {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
ThumbnailSize::Icon => 150,
|
|
|
|
|
|
ThumbnailSize::Preview => 400,
|
|
|
|
|
|
ThumbnailSize::Large => 800,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Get the directory name for this size.
|
|
|
|
|
|
pub fn dir_name(&self) -> &'static str {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
ThumbnailSize::Icon => "icon",
|
|
|
|
|
|
ThumbnailSize::Preview => "preview",
|
|
|
|
|
|
ThumbnailSize::Large => "large",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Get all thumbnail sizes.
|
|
|
|
|
|
pub fn all() -> &'static [ThumbnailSize] {
|
2026-02-14 01:29:34 +01:00
|
|
|
|
&[
|
|
|
|
|
|
ThumbnailSize::Icon,
|
|
|
|
|
|
ThumbnailSize::Preview,
|
|
|
|
|
|
ThumbnailSize::Large,
|
|
|
|
|
|
]
|
2026-02-08 13:40:23 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Statistics about the thumbnail cache.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct ThumbnailStatsDto {
|
|
|
|
|
|
pub cached_thumbnails: usize,
|
|
|
|
|
|
pub cache_size_bytes: usize,
|
|
|
|
|
|
pub max_cache_bytes: usize,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Port for thumbnail generation and retrieval.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Implementations handle the actual image processing, caching,
|
|
|
|
|
|
/// and storage of thumbnails, while the application layer only
|
|
|
|
|
|
/// interacts through this abstraction.
|
|
|
|
|
|
pub trait ThumbnailPort: Send + Sync + 'static {
|
|
|
|
|
|
/// Check if a file is an image that can have thumbnails.
|
|
|
|
|
|
fn is_supported_image(&self, mime_type: &str) -> bool;
|
|
|
|
|
|
|
|
|
|
|
|
/// Get a thumbnail, generating it on-demand if needed.
|
|
|
|
|
|
///
|
2026-04-12 00:50:10 +02:00
|
|
|
|
/// `blob_hash` is the content hash used as the disk storage key
|
|
|
|
|
|
/// (dedup: identical blobs share one set of thumbnails).
|
2026-02-08 13:40:23 +01:00
|
|
|
|
async fn get_thumbnail(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
file_id: &str,
|
2026-04-12 00:50:10 +02:00
|
|
|
|
blob_hash: &str,
|
2026-02-08 13:40:23 +01:00
|
|
|
|
size: ThumbnailSize,
|
|
|
|
|
|
original_path: &Path,
|
|
|
|
|
|
) -> Result<Bytes, DomainError>;
|
|
|
|
|
|
|
|
|
|
|
|
/// Generate all thumbnail sizes for a file in the background.
|
|
|
|
|
|
///
|
2026-04-12 00:50:10 +02:00
|
|
|
|
/// `blob_hash` is the content hash used as the disk storage key.
|
|
|
|
|
|
/// If thumbnails already exist for this hash, only the moka cache
|
|
|
|
|
|
/// is populated (zero CPU for image processing).
|
|
|
|
|
|
fn generate_all_sizes_background(
|
|
|
|
|
|
self: Arc<Self>,
|
|
|
|
|
|
file_id: String,
|
|
|
|
|
|
blob_hash: String,
|
|
|
|
|
|
original_path: PathBuf,
|
|
|
|
|
|
);
|
2026-02-08 13:40:23 +01:00
|
|
|
|
|
|
|
|
|
|
/// Delete all thumbnails for a file.
|
|
|
|
|
|
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
|
|
|
|
|
|
|
2026-03-07 18:55:44 +01:00
|
|
|
|
/// Try to get a cached thumbnail without generating one.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Returns `None` if no cached thumbnail exists on disk or in memory.
|
2026-04-12 00:50:10 +02:00
|
|
|
|
/// `blob_hash` is used to locate the file on disk. If `None`, only
|
|
|
|
|
|
/// the in-memory moka cache is checked.
|
|
|
|
|
|
async fn get_cached_thumbnail(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
file_id: &str,
|
|
|
|
|
|
blob_hash: Option<&str>,
|
|
|
|
|
|
size: ThumbnailSize,
|
|
|
|
|
|
) -> Option<Bytes>;
|
2026-03-07 18:55:44 +01:00
|
|
|
|
|
|
|
|
|
|
/// Store an externally-generated thumbnail (e.g. client-side video frame).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Validates the image, re-encodes to WebP, and persists to cache.
|
|
|
|
|
|
async fn store_external_thumbnail(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
file_id: &str,
|
|
|
|
|
|
size: ThumbnailSize,
|
|
|
|
|
|
data: Bytes,
|
|
|
|
|
|
) -> Result<Bytes, DomainError>;
|
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
|
/// Get cache statistics.
|
|
|
|
|
|
async fn get_stats(&self) -> ThumbnailStatsDto;
|
|
|
|
|
|
}
|