Appearance
Assets Serving & Image Transforms
Overview
The /assets/:id endpoint serves files from ODP's storage with optional on-the-fly image transformations (resize, format conversion, quality adjustment). Transformed images are cached as separate files to avoid re-processing on subsequent requests.
Image transformations use Sharp, a high-performance Node.js image processing library.
Endpoint
GET /assets/:id
Serve a file by its UUID with optional image transforms.
Auth required: No (public access, subject to storage permissions)
URL Parameters:
id— File UUID fromodp_files
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
width | integer | Target width in pixels |
height | integer | Target height in pixels |
fit | string | Resize fit mode (see below) |
quality | integer | Output quality 1–100 |
format | string | Output format (see below) |
withoutEnlargement | 'true' | Do not upscale images smaller than target dimensions |
Fit Modes
| Value | Description |
|---|---|
cover | (Default) Crop to fill the target box, preserving aspect ratio |
contain | Fit within the target box, letterboxed if needed |
fill | Stretch to exactly fill dimensions (ignores aspect ratio) |
inside | Resize to fit within dimensions without cropping |
outside | Resize so that the image covers the dimensions without cropping |
Output Formats
| Value | MIME Type |
|---|---|
jpg / jpeg | image/jpeg |
png | image/png |
webp | image/webp |
avif | image/avif |
tiff | image/tiff |
Format conversion only applies to supported image types. Non-image files (PDFs, videos, etc.) are served as-is.
Supported Input Formats for Transforms
Transforms are only applied to the following MIME types (defined by SUPPORTED_IMAGE_TRANSFORM_FORMATS):
image/jpegimage/jpgimage/pngimage/webpimage/avifimage/tiffimage/gifimage/svg+xml
All other file types are served raw without transforms.
Examples
Original image:
GET /assets/550e8400-e29b-41d4-a716-446655440000Resize to 800px wide, cover fit, WebP:
GET /assets/550e8400-e29b-41d4-a716-446655440000?width=800&fit=cover&format=webpThumbnail 200×200, quality 80:
GET /assets/550e8400-e29b-41d4-a716-446655440000?width=200&height=200&quality=80Convert to AVIF without upscaling:
GET /assets/550e8400-e29b-41d4-a716-446655440000?format=avif&withoutEnlargement=trueResponse Headers
| Header | Value | Description |
|---|---|---|
Content-Type | MIME type | File type (original or converted) |
Content-Disposition | inline; filename="..." | Original filename from odp_files.filename_download |
Cache-Control | public, max-age=<seconds> | Derived from ASSETS_CACHE_TTL |
Business Logic
Transform Pipeline
- Fetch file record from
odp_files - Determine storage driver from
file.storage - Check if file MIME type supports transforms
- If transforms requested and format supported: a. Generate cache filename:
{base}{suffix}{ext}wheresuffix = __<md5-of-transforms-params>[:12]b. Check if cached file exists in storage c. If cached: serve directly d. If not cached: run Sharp pipeline → write to storage → serve from cache - If no transforms or unsupported format: stream raw file
Sharp Pipeline
readStream → rotate (EXIF auto-rotate) → resize → format/quality → write to cacheConcurrency Control
Sharp's counters() (queue + processing) is checked against ASSETS_TRANSFORM_MAX_CONCURRENT. If the limit is exceeded, the request fails with "Server too busy to process image transformations".
Transform Timeout
The Sharp pipeline has a configurable timeout (ASSETS_TRANSFORM_TIMEOUT seconds). If exceeded, an error is thrown and the incomplete cache file is deleted.
Cache File Naming
Cache files are stored in the same storage location as the original file with a deterministic name:
{original_basename}__{md5(transforms)[:12]}.{output_ext}Example: photo__a1b2c3d4e5f6.webp
The MD5 is computed from the JSON serialization of the TransformOptions object.
File Folders
Files can be organized into folders using odp_files.folder (UUID reference to odp_folders).
odp_folders
| Column | Type | Description |
|---|---|---|
id | UUID PK | |
name | string(255) | Folder name |
parent | UUID FK → odp_folders | Parent folder (null = root) |
Folders support infinite nesting via the self-referential parent foreign key.
Files Data Model
odp_files
| Column | Type | Description |
|---|---|---|
id | UUID PK | |
storage | string(255) | Storage driver name (e.g., local, s3) |
filename_disk | string(255) | Filename on disk (internal, UUID-based) |
filename_download | string(255) | Original filename for download |
title | string(255) | Display title |
type | string(255) | MIME type |
folder | UUID FK → odp_folders | Folder organization |
uploaded_by | UUID FK → odp_users | |
created_on | timestamp | |
modified_by | UUID FK → odp_users | |
modified_on | timestamp | |
charset | string(50) | Character encoding (text files) |
filesize | bigint | File size in bytes |
width | integer | Image width in pixels |
height | integer | Image height in pixels |
duration | integer | Video/audio duration in seconds |
embed | string(200) | Embed provider (for video embeds) |
description | text | Optional description |
location | string(200) | Geographic location (photo metadata) |
tags | text (JSON) | Array of tag strings |
metadata | text (JSON) | EXIF/XMP metadata |
focal_point_x | integer | Focal point X coordinate |
focal_point_y | integer | Focal point Y coordinate |
tus_id | string(255) | TUS upload ID (during upload) |
tus_data | text (JSON) | TUS upload metadata |
Configuration
| Variable | Default | Description |
|---|---|---|
ASSETS_CACHE_TTL | 30m | Cache-Control max-age for served assets |
ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION | 6000 | Max input image dimension (pixels) |
ASSETS_TRANSFORM_MAX_CONCURRENT | 25 | Max concurrent Sharp transform operations |
ASSETS_TRANSFORM_TIMEOUT | 30 | Transform timeout in seconds |
ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL | warning | Sharp failOn level: none, truncated, error, warning |
STORAGE_LOCAL_ROOT | ./uploads | Local storage root |