f2d35ca792
- JWT secret auto-generates and persists to <STORAGE_PATH>/.jwt_secret - Remove setup token: first admin setup is open until system initialized - Fix schema.sql: move CREATE EXTENSION pg_trgm/ltree to top - Update login UI and auth.js to remove setup token fields
5.8 KiB
Executable File
5.8 KiB
Executable File
08 - Thumbnails and Transcoding
OxiCloud provides two image optimization features:
- Thumbnails: on-demand generation of WebP thumbnails in 3 sizes, with background pre-generation on upload
- Image Transcoding: automatic JPEG/PNG/GIF → WebP conversion based on browser
Acceptheader
Both features use multi-level caching (memory LRU + disk) and are non-blocking.
Thumbnails
Architecture
| Layer | Component | File |
|---|---|---|
| Application Port | ThumbnailPort trait, ThumbnailSize enum | src/application/ports/thumbnail_ports.rs |
| Infrastructure | ThumbnailService | src/infrastructure/services/thumbnail_service.rs |
| Interfaces | Integrated in FileHandler | src/interfaces/api/handlers/file_handler.rs |
Thumbnail Sizes
| Size | Dimensions | Directory Name |
|---|---|---|
Icon |
150x150 | icon |
Preview |
400x400 | preview |
Large |
800x800 | large |
Supported Formats
Input: image/jpeg, image/jpg, image/png, image/gif, image/webp
Output: always WebP (using Lanczos3 resize filter)
Storage Layout
<storage_path>/
.thumbnails/
icon/
<file_id>.webp
preview/
<file_id>.webp
large/
<file_id>.webp
REST API
| Method | Path | Description |
|---|---|---|
GET |
/api/files/{id}/thumbnail/{size} |
Get thumbnail (size = icon / preview / large) |
POST |
/api/files/upload |
Upload file + auto-generate thumbnails for images |
Response headers for thumbnail GET:
Content-Type: image/webpCache-Control: public, max-age=31536000, immutableETag: "thumb-{id}-{size}"
Generation Flow
- On upload (
upload_file_with_thumbnails): after successful file upload, if the MIME type is a supported image, thumbnails for all 3 sizes are generated in a backgroundtokio::spawntask - On GET (lazy): if a thumbnail doesn't exist on disk, it's generated on-demand, cached in memory and on disk
Caching
- Memory LRU cache: configurable max entries and max bytes
- Disk cache: persistent WebP files in
.thumbnails/ - Cache lookup order: memory → disk → generate
Port Trait
#[async_trait]
pub trait ThumbnailPort: Send + Sync + 'static {
fn is_supported_image(&self, mime_type: &str) -> bool;
async fn get_thumbnail(&self, file_id: &str, size: ThumbnailSize, original_path: &Path) -> Result<Bytes, DomainError>;
fn generate_all_sizes_background(self: Arc<Self>, file_id: String, original_path: PathBuf);
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
async fn get_stats(&self) -> ThumbnailStatsDto;
}
Image Transcoding
Architecture
| Layer | Component | File |
|---|---|---|
| Application Port | ImageTranscodePort trait, OutputFormat, BrowserCapabilities | src/application/ports/transcode_ports.rs |
| Infrastructure | ImageTranscodeService | src/infrastructure/services/image_transcode_service.rs |
| Interfaces | Integrated in download handler | src/interfaces/api/handlers/file_handler.rs |
How It Works
- Download handler reads the
Acceptheader from the HTTP request BrowserCapabilities::from_accept_header()determines if the browser supports WebP- If the file is a transcodable image (JPEG/PNG/GIF, ≤5 MB) and the browser supports WebP: check memory cache → check disk cache → transcode on miss
- If WebP output is larger than the original, serve the original instead (smart skip)
Constants
| Constant | Value | Description |
|---|---|---|
MAX_TRANSCODE_SIZE |
5 MB | Files above this skip transcoding |
Supported Input Formats
image/jpeg, image/jpg, image/png, image/gif
Not transcoded: image/webp (already optimal), image/svg+xml, image/bmp
Storage Layout
<storage_path>/
.transcoded/
webp/
<file_id>.webp
Caching
- Memory LRU cache: configurable max entries and max bytes
- Disk cache: persistent WebP files in
.transcoded/webp/ - Disk writes are fire-and-forget via
tokio::spawn(non-blocking) invalidate(file_id)evicts from both memory and disk
Port Trait
#[async_trait]
pub trait ImageTranscodePort: Send + Sync + 'static {
fn can_transcode(&self, mime_type: &str) -> bool;
fn should_transcode(&self, mime_type: &str, file_size: u64) -> bool;
async fn get_transcoded(&self, file_id: &str, original_content: &[u8], original_mime: &str, target_format: OutputFormat)
-> Result<(Bytes, String, bool), DomainError>;
async fn invalidate(&self, file_id: &str);
async fn get_stats(&self) -> TranscodeStatsDto;
async fn clear_cache(&self) -> Result<(), DomainError>;
}
Browser Detection
pub struct BrowserCapabilities {
pub supports_webp: bool,
pub supports_avif: bool, // reserved for future
}
impl BrowserCapabilities {
pub fn from_accept_header(accept: Option<&str>) -> Self;
pub fn best_format(&self) -> Option<OutputFormat>;
}
Statistics
pub struct TranscodeStatsDto {
pub cache_hits: u64,
pub disk_hits: u64,
pub transcodes: u64,
pub bytes_saved: u64,
pub transcode_errors: u64,
}
Example Flow
Client: GET /api/files/abc-123/download
Accept: image/webp, image/png, */*
Server: 1. File is "photo.jpg" (800KB) → can transcode yes, should transcode yes
2. Check memory cache → miss
3. Check disk cache (.transcoded/webp/abc-123.webp) → miss
4. Transcode JPEG → WebP (600KB) → smaller yes
5. Cache in memory + async write to disk
6. Respond with WebP (saves 200KB, 25% reduction)
Dependencies
image = "0.25"(withjpeg,png,gif,webpfeatures) -- used by both thumbnail and transcode- Thumbnail resize:
imageops::resizewithFilterType::Lanczos3 - WebP encoding: via
imagecrate's WebP encoder