- 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>
2.2 KiB
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).