Short IDs become the canonical identifier in URLs (/i/:short_id), MinIO/R2 storage keys, and all API responses. Hash-based deduplication is preserved. Includes two-phase Alembic migration (003 adds nullable column, 004 enforces NOT NULL) with a backfill script to copy storage objects and populate short_id for existing images. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3.3 KiB
Markdown
116 lines
3.3 KiB
Markdown
# Contract: Image API (Short ID Update)
|
|
|
|
## ImageRecord Response Schema
|
|
|
|
All image endpoints return this shape. `short_id` is a new field; all other fields are unchanged.
|
|
|
|
```json
|
|
{
|
|
"id": "7343d164-80bb-473b-b239-717f2842ae4e",
|
|
"short_id": "xK7mN2pQ",
|
|
"hash": "163dec08460650439f1e7439721e8e566aff7d8aaad60cf451e7d3518a334a23",
|
|
"filename": "image.gif",
|
|
"mime_type": "image/gif",
|
|
"size_bytes": 1957149,
|
|
"width": 265,
|
|
"height": 199,
|
|
"storage_key": "xK7mN2pQ",
|
|
"thumbnail_key": "xK7mN2pQ-thumb",
|
|
"file_url": "https://cdn.reactbin.juggalol.com/xK7mN2pQ",
|
|
"thumbnail_url": "https://cdn.reactbin.juggalol.com/xK7mN2pQ-thumb",
|
|
"created_at": "2026-05-09T02:46:29.520296+00:00",
|
|
"tags": ["kfc"]
|
|
}
|
|
```
|
|
|
|
**Constraints**:
|
|
- `short_id`: exactly 8 alphanumeric characters `[a-zA-Z0-9]{8}`
|
|
- `storage_key`: equals `short_id` (post-migration)
|
|
- `thumbnail_key`: equals `{short_id}-thumb` or `null` if no thumbnail exists
|
|
- `file_url`: `{cdn_base}/{short_id}` when CDN is configured; `/api/v1/images/{short_id}/file` otherwise
|
|
- `thumbnail_url`: `{cdn_base}/{short_id}-thumb` or `null`
|
|
|
|
---
|
|
|
|
## Route Changes
|
|
|
|
All routes that previously accepted `{image_id}` as a UUID now accept `{short_id}` as an 8-character alphanumeric string.
|
|
|
|
### GET /api/v1/images/{short_id}
|
|
|
|
Fetch a single image by short ID.
|
|
|
|
- **Path param**: `short_id` — 8-char alphanumeric string
|
|
- **Response 200**: ImageRecord
|
|
- **Response 404**: `{"detail": "Image not found", "code": "image_not_found"}`
|
|
- **Response 422**: `{"detail": "Invalid image ID", "code": "invalid_short_id"}` if param is not 8 alphanumeric chars
|
|
|
|
### PATCH /api/v1/images/{short_id}/tags
|
|
|
|
Update tags on an image. Auth required.
|
|
|
|
- **Path param**: `short_id` — 8-char alphanumeric string
|
|
- **Body**: `{"tags": ["tag1", "tag2"]}`
|
|
- **Response 200**: ImageRecord (updated)
|
|
- **Response 404/422**: same shape as above
|
|
|
|
### DELETE /api/v1/images/{short_id}
|
|
|
|
Delete an image and its storage objects. Auth required.
|
|
|
|
- **Path param**: `short_id` — 8-char alphanumeric string
|
|
- **Response 204**: no body
|
|
- **Response 404**: error envelope
|
|
|
|
### GET /api/v1/images/{short_id}/file
|
|
|
|
Serve the raw image file (proxy mode, when CDN is not configured).
|
|
|
|
- **Path param**: `short_id`
|
|
- **Response 200**: raw image bytes with correct `Content-Type`
|
|
|
|
### GET /api/v1/images/{short_id}/thumbnail
|
|
|
|
Serve the thumbnail (proxy mode).
|
|
|
|
- **Path param**: `short_id`
|
|
- **Response 200**: WebP bytes or original image if no thumbnail
|
|
|
|
### POST /api/v1/images (upload — unchanged route, updated response)
|
|
|
|
- **Response**: ImageRecord with `short_id` populated
|
|
|
|
---
|
|
|
|
## Frontend Route Change
|
|
|
|
| Old route | New route |
|
|
|-----------------|--------------|
|
|
| `/images/:id` | `/i/:id` |
|
|
|
|
The `:id` segment now contains the `short_id` value (8 alphanumeric chars) rather than a UUID.
|
|
|
|
---
|
|
|
|
## ImageRecord TypeScript Interface (updated)
|
|
|
|
```typescript
|
|
export interface ImageRecord {
|
|
id: string; // UUID — retained, not used for routing
|
|
short_id: string; // NEW — 8-char base62, used for all routing and API calls
|
|
hash: string;
|
|
filename: string;
|
|
mime_type: string;
|
|
size_bytes: number;
|
|
width: number;
|
|
height: number;
|
|
storage_key: string;
|
|
thumbnail_key: string | null;
|
|
file_url: string;
|
|
thumbnail_url: string | null;
|
|
created_at: string;
|
|
tags: string[];
|
|
duplicate?: boolean;
|
|
}
|
|
```
|