feat: добавить генерацию image variants

- добавлен shared config presets, custom transforms и allowlist hosts
- реализованы Backend endpoints для assets, presets и variants
- добавлена orchestration через PostgreSQL, RabbitMQ, S3 и worker
- обновлён Gateway read-through flow с L1 cache и корректным Vary: Accept
- добавлена миграция resize_mode для variants lookup
- обновлены dev scripts, env template, lockfile и документация
This commit is contained in:
2026-05-05 13:25:28 +03:00
parent bcadb85a83
commit 1c0e8277a3
59 changed files with 3526 additions and 143 deletions

View File

@@ -1,6 +1,6 @@
# Черновик Backend Контракта
Это черновик будущего бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger и placeholder для internal image ensure.
Это черновик бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger, `POST /api/assets` и internal image ensure MVP.
## System
@@ -20,7 +20,7 @@ NestJS backend отдаёт JSON, metadata, statuses и URLs. Public image origi
POST /api/internal/images/ensure
```
Текущий статус реализации - endpoint зарезервирован и возвращает `501 Not Implemented`. Gateway `/images/*` пока тоже возвращает placeholder `501 Not Implemented`.
Текущий статус реализации - MVP реализован: Backend создаёт/переиспользует variant row, публикует RabbitMQ job, ждёт `ready` до timeout и возвращает image bytes из S3.
Request body:
@@ -30,7 +30,10 @@ Request body:
"version": 4,
"preset": "card",
"width": 640,
"height": 0,
"quality": 80,
"requestedFormat": "auto",
"resize": "fit",
"format": "webp"
}
```
@@ -39,13 +42,16 @@ Query params:
| Param | Описание |
|---|---|
| `w` | целевая ширина, должна быть разрешена preset или округлена до ближайшей разрешённой |
| `q` | качество, должно быть из allowlist качества |
| `w` | целевая ширина; обязательна для responsive presets и custom, запрещена/фиксирована для fixed presets |
| `h` | целевая высота; используется для custom, для fixed preset берётся из config |
| `q` | качество; если не передано, берётся из preset, иначе должно входить в allowlist preset |
| `f` | `auto`, `avif`, `webp`, `jpg`; для Next/image по умолчанию `auto` |
| `fit` | `fit` или `fill`; используется только для custom transforms |
Responsibilities:
- проверить `assetId` и `preset`;
- проверить `assetId` и статический preset/custom transform config;
- нормализовать width, height, resize, quality и format;
- вычислить deterministic `variantHash`;
- проверить PostgreSQL и S3;
- если variant готов в S3, вернуть bytes или stream metadata для Gateway;
@@ -69,9 +75,8 @@ ETag: "..."
|---|---|
| `400` | некорректные query params |
| `404` | asset или preset не найден |
| `409` | variant уже генерируется и sync ожидание отключено |
| `422` | source image нельзя обработать |
| `502` | external imgproxy недоступен |
| `502` | external imgproxy недоступен или S3 object отсутствует после `ready` |
| `504` | generation не завершилась до `IMAGE_ENSURE_WAIT_MS` |
Public endpoint реализуется в Fastify Gateway, internal ensure endpoint - в Backend:
@@ -82,6 +87,14 @@ client -> CDN -> Gateway /images/* -> Backend ensure -> RabbitMQ -> Worker -> im
Важно: `/images/*` не должен жить в NestJS `/api`. Gateway остаётся public image origin, Backend остаётся управлением процессов.
Public image URL versioned: `/images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto`.
Примеры:
```text
/images/asset_demo/v1/card?w=640&q=80&f=auto
/images/asset_demo/v1/avatar?f=auto
/images/asset_demo/v1/custom?w=777&h=333&q=72&fit=fill&f=webp
```
## Allowed Hosts
```text
@@ -94,63 +107,152 @@ DELETE /allowed-hosts/:id
## Assets
```text
GET /assets
POST /assets
GET /assets/:id
GET /assets
GET /assets/:publicId
GET /assets/:publicId/variants
POST /assets/:publicId/variants
DELETE /assets/:id
```
Сейчас реализованы `POST /assets`, `GET /assets`, `GET /assets/:publicId`, `GET /assets/:publicId/variants`, `POST /assets/:publicId/variants`.
`POST /assets` request:
```json
{
"sourceUrl": "https://example.com/photo.jpg"
"sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg",
"publicId": "asset_demo"
}
```
Response:
```json
{
"id": "59fcf4f6-9891-4df4-8bb7-a4dbe570bb66",
"publicId": "asset_demo",
"version": 1,
"sourceHost": "storage.yandexcloud.net",
"imageBasePath": "/images/asset_demo/v1/card"
}
```
Responsibilities:
- validate source URL;
- check `allowed_image_hosts`;
- create or reuse `image_assets` row;
- check mock allowlist `SOURCE_ALLOWED_HOSTS`, если `SOURCE_HOST_ALLOW_ALL=false`;
- создать `image_assets` row;
- создать `image_asset_versions` row версии `1`;
- optionally save original to S3 later.
`GET /assets` response:
```json
{
"assets": [
{
"id": "59fcf4f6-9891-4df4-8bb7-a4dbe570bb66",
"publicId": "asset_demo",
"currentVersion": 1,
"status": "active",
"sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg",
"sourceHost": "storage.yandexcloud.net",
"createdAt": "2026-05-05T12:00:00.000Z",
"updatedAt": "2026-05-05T12:00:00.000Z"
}
]
}
```
`GET /assets/:publicId/variants` возвращает rows из `image_variants` с public Gateway URL, S3 key и status.
## Variants
```text
GET /assets/:id/variants
POST /assets/:id/variants
GET /assets/:publicId/variants
POST /assets/:publicId/variants
POST /variants/:id/regenerate
DELETE /variants/:id
```
`POST /assets/:id/variants` request:
`POST /assets/:publicId/variants` request:
```json
{
"preset": "card",
"mode": "single",
"format": "webp",
"width": 640
"width": 640,
"quality": 80
}
```
Response if ready:
Family generation:
```json
{
"id": "variant_123",
"status": "ready",
"url": "http://localhost:8888/images/asset_123/v4/card?w=640&q=80&f=webp"
"preset": "card",
"mode": "family"
}
```
Response if generation is async:
Для `family` Backend создаёт набор variants по разрешённым widths/formats static preset и ставит RabbitMQ jobs. Endpoint не ждёт image bytes, в отличие от internal ensure.
Response:
```json
{
"id": "variant_123",
"status": "pending",
"url": null
"publicId": "asset_demo",
"version": 1,
"variants": [
{
"id": "variant_123",
"preset": "card",
"version": 1,
"width": 640,
"height": 0,
"resize": "fit",
"quality": 80,
"requestedFormat": "webp",
"format": "webp",
"status": "pending",
"url": "http://localhost:8888/images/asset_demo/v1/card?w=640&q=80&f=webp",
"s3Key": "variants/asset_demo/v1/abc.webp"
}
]
}
```
## Presets
```text
GET /presets
```
Возвращает static presets, custom transform limits и mock allowlist source hosts.
```json
{
"presets": [
{
"name": "card",
"mode": "responsive",
"formats": ["avif", "webp", "jpg"],
"qualities": [75, 80],
"quality": 80,
"resize": "fit",
"widths": [320, 640, 960]
}
],
"custom": {
"enabled": true,
"formats": ["avif", "webp", "jpg", "png"],
"maxWidth": 4096,
"maxHeight": 4096,
"quality": 80
},
"allowedSourceHosts": ["storage.yandexcloud.net"]
}
```