Files
Oxicloud/src/application/ports/thumbnail_ports.rs
T
Diocrafts 2dde4da5cf feat: thumbnail dedup — store thumbnails by blob_hash instead of file_id (#233)
Thumbnails are now keyed by blob_hash on disk so identical files share
a single set of thumbnails (icon/preview/large). For 4000 duplicate
files with the same content, this reduces thumbnail storage from 12,000
files to just 3.

Changes:
- get_thumbnail_path() keys by blob_hash instead of file_id
- get_thumbnail(), get_cached_thumbnail(), generate_all_sizes_background()
  accept blob_hash parameter for disk dedup
- generate_all_sizes_background() fast path: if blob-hash thumbnails
  already exist on disk, skip image processing entirely and just
  populate moka cache for the new file_id
- delete_thumbnails() only invalidates moka cache (shared disk
  thumbnails must not be deleted when one file is removed)
- delete_blob_thumbnails() added for GC; garbage_collect() now cleans
  up orphaned thumbnail files alongside blob files
- External thumbnails (video frames) stored as ext-{file_id}.jpg
  since they are client-generated and not dedup-able
- ThumbnailPort trait updated with blob_hash parameters
- All handler call sites updated (file_handler, preview_handler)
- Tests updated for new signatures
2026-04-12 00:50:10 +02:00

121 lines
3.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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.
use crate::common::errors::DomainError;
use bytes::Bytes;
use std::path::{Path, PathBuf};
use std::sync::Arc;
/// 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] {
&[
ThumbnailSize::Icon,
ThumbnailSize::Preview,
ThumbnailSize::Large,
]
}
}
/// 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.
///
/// `blob_hash` is the content hash used as the disk storage key
/// (dedup: identical blobs share one set of thumbnails).
async fn get_thumbnail(
&self,
file_id: &str,
blob_hash: &str,
size: ThumbnailSize,
original_path: &Path,
) -> Result<Bytes, DomainError>;
/// Generate all thumbnail sizes for a file in the background.
///
/// `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,
);
/// Delete all thumbnails for a file.
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
/// Try to get a cached thumbnail without generating one.
///
/// Returns `None` if no cached thumbnail exists on disk or in memory.
/// `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>;
/// 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>;
/// Get cache statistics.
async fn get_stats(&self) -> ThumbnailStatsDto;
}