Skip to content

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 from odp_files

Query Parameters:

ParameterTypeDescription
widthintegerTarget width in pixels
heightintegerTarget height in pixels
fitstringResize fit mode (see below)
qualityintegerOutput quality 1–100
formatstringOutput format (see below)
withoutEnlargement'true'Do not upscale images smaller than target dimensions

Fit Modes

ValueDescription
cover(Default) Crop to fill the target box, preserving aspect ratio
containFit within the target box, letterboxed if needed
fillStretch to exactly fill dimensions (ignores aspect ratio)
insideResize to fit within dimensions without cropping
outsideResize so that the image covers the dimensions without cropping

Output Formats

ValueMIME Type
jpg / jpegimage/jpeg
pngimage/png
webpimage/webp
avifimage/avif
tiffimage/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/jpeg
  • image/jpg
  • image/png
  • image/webp
  • image/avif
  • image/tiff
  • image/gif
  • image/svg+xml

All other file types are served raw without transforms.


Examples

Original image:

GET /assets/550e8400-e29b-41d4-a716-446655440000

Resize to 800px wide, cover fit, WebP:

GET /assets/550e8400-e29b-41d4-a716-446655440000?width=800&fit=cover&format=webp

Thumbnail 200×200, quality 80:

GET /assets/550e8400-e29b-41d4-a716-446655440000?width=200&height=200&quality=80

Convert to AVIF without upscaling:

GET /assets/550e8400-e29b-41d4-a716-446655440000?format=avif&withoutEnlargement=true

Response Headers

HeaderValueDescription
Content-TypeMIME typeFile type (original or converted)
Content-Dispositioninline; filename="..."Original filename from odp_files.filename_download
Cache-Controlpublic, max-age=<seconds>Derived from ASSETS_CACHE_TTL

Business Logic

Transform Pipeline

  1. Fetch file record from odp_files
  2. Determine storage driver from file.storage
  3. Check if file MIME type supports transforms
  4. If transforms requested and format supported: a. Generate cache filename: {base}{suffix}{ext} where suffix = __<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
  5. If no transforms or unsupported format: stream raw file

Sharp Pipeline

readStream → rotate (EXIF auto-rotate) → resize → format/quality → write to cache

Concurrency 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

ColumnTypeDescription
idUUID PK
namestring(255)Folder name
parentUUID FK → odp_foldersParent folder (null = root)

Folders support infinite nesting via the self-referential parent foreign key.


Files Data Model

odp_files

ColumnTypeDescription
idUUID PK
storagestring(255)Storage driver name (e.g., local, s3)
filename_diskstring(255)Filename on disk (internal, UUID-based)
filename_downloadstring(255)Original filename for download
titlestring(255)Display title
typestring(255)MIME type
folderUUID FK → odp_foldersFolder organization
uploaded_byUUID FK → odp_users
created_ontimestamp
modified_byUUID FK → odp_users
modified_ontimestamp
charsetstring(50)Character encoding (text files)
filesizebigintFile size in bytes
widthintegerImage width in pixels
heightintegerImage height in pixels
durationintegerVideo/audio duration in seconds
embedstring(200)Embed provider (for video embeds)
descriptiontextOptional description
locationstring(200)Geographic location (photo metadata)
tagstext (JSON)Array of tag strings
metadatatext (JSON)EXIF/XMP metadata
focal_point_xintegerFocal point X coordinate
focal_point_yintegerFocal point Y coordinate
tus_idstring(255)TUS upload ID (during upload)
tus_datatext (JSON)TUS upload metadata

Configuration

VariableDefaultDescription
ASSETS_CACHE_TTL30mCache-Control max-age for served assets
ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION6000Max input image dimension (pixels)
ASSETS_TRANSFORM_MAX_CONCURRENT25Max concurrent Sharp transform operations
ASSETS_TRANSFORM_TIMEOUT30Transform timeout in seconds
ASSETS_INVALID_IMAGE_SENSITIVITY_LEVELwarningSharp failOn level: none, truncated, error, warning
STORAGE_LOCAL_ROOT./uploadsLocal storage root

ODP Internal API Documentation