Files
reactbin/specs/003-upload-thumbnails/contracts/api.md
agatha f953c88984 Feat: Pre-generate WebP thumbnails on upload for faster library load
- Add Pillow dependency and thumbnail.py with generate_thumbnail() — produces
  WebP ≤400px, preserves aspect ratio, never upscales, handles GIF frame 0
- Alembic migration 002 adds nullable thumbnail_key column to images table
- Upload route generates thumbnail via asyncio.to_thread (non-blocking),
  stores at {hash}-thumb; failure is tolerated and upload succeeds with null key
- New GET /api/v1/images/{id}/thumbnail endpoint: serves WebP thumbnail or
  falls back to original for pre-feature images; ETag + immutable cache headers
- Delete route cleans up thumbnail storage object alongside original
- Library grid switches from /file to /thumbnail for all image src bindings
- 59 tests passing (46 existing + 13 new across unit, upload, serving, delete)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:26:16 +00:00

2.2 KiB

API Contract: Upload Thumbnails

Branch: 003-upload-thumbnails | Date: 2026-05-03


New endpoint

GET /api/v1/images/{image_id}/thumbnail

Returns the thumbnail content for the given image. If no thumbnail was generated (image pre-dates the feature or generation failed), falls back to the full-size original.

Path parameters

Parameter Type Description
image_id UUID Unique identifier of the image

Responses

200 OK — Thumbnail (or original fallback) content

Header Value Notes
Content-Type image/webp Always WebP when a thumbnail exists; original mime_type when falling back to the original
ETag "{sha256-hex}" Same hash as the original image — content is immutable
Cache-Control public, max-age=31536000, immutable Safe: thumbnail content never changes

Body: raw image bytes (WebP thumbnail, or original bytes as fallback).

404 Not Found — Image not found

{ "detail": "Image not found", "code": "image_not_found" }

500 Internal Server Error — Storage retrieval failure

{ "detail": "Failed to retrieve image content", "code": "storage_error" }

Changed endpoint: POST /api/v1/images

The upload response body gains one new field:

Field Type Notes
thumbnail_key string | null S3 key of the generated thumbnail. null if generation failed.

All existing fields are unchanged.

Example response (new field only shown):

{
  "id": "...",
  "thumbnail_key": "abc123…-thumb",
  ...
}

Changed endpoint: GET /api/v1/images and GET /api/v1/images/{id}

Both metadata responses gain the same thumbnail_key field (string | null).


UI contract

The Angular ImageService gains one new method:

getThumbnailUrl(id: string): string
  → '/api/v1/images/{id}/thumbnail'

The ImageRecord interface gains:

thumbnail_key: string | null;

The library grid component uses getThumbnailUrl(image.id) as the src for every grid cell. The detail component continues using getFileUrl(image.id).