This commit is contained in:
2026-05-12 07:54:32 +03:00
parent 0faa8b9d2d
commit d49449c30c
187 changed files with 4826 additions and 5884 deletions

View File

@@ -4,6 +4,8 @@
`image-platform` - отдельный control plane для своего Cloudinary-like image pipeline.
Проект эволюционирует в Assets Delivery Platform: изображения остаются первым production-ready vertical slice, а будущие типы ассетов будут добавляться отдельными модулями со своими worker-пайплайнами. Концепция закреплена в `docs/assets-delivery-platform.md`.
Проект отвечает за metadata, S3 artifacts, variants, allowlist, presets и генерацию изображений через внешний `imgproxy`.
## Компоненты

View File

@@ -0,0 +1,214 @@
# Assets Delivery Platform
## Концепция
`image-platform` эволюционирует из платформы для изображений в Assets Delivery Platform.
Платформа должна быть control plane и delivery layer для загрузки, обработки, версионирования и доставки ассетов. Первый поддерживаемый тип ассетов - изображения. Текущие наработки по image pipeline остаются основой первого production-ready vertical slice.
## Продуктовая цель
Пользователь должен иметь возможность управлять ассетами через кабинет и программно через API.
Клиентский backend должен уметь без участия UI:
- загрузить изображение;
- выбрать preset обработки;
- запустить build;
- получить статус обработки;
- получить готовые delivery URL;
- использовать публичные URL в приложении, CMS, магазине или любом другом сервисе.
Платформа должна быть не только оптимизатором изображений, а headless-сервисом доставки ассетов с UI для управления.
## Первый vertical slice: Images
Первый модуль платформы - изображения.
В рамках images сохраняются текущие архитектурные решения:
- `Gateway` как публичный image origin;
- read-through delivery flow;
- `Backend` как orchestration/control plane;
- `PostgreSQL` как источник правды для metadata, statuses и variants;
- `S3/MinIO` как хранилище originals и generated variants;
- `RabbitMQ` как очередь задач;
- `Worker` как исполнитель image processing;
- внешний `imgproxy` как CPU-heavy image processor;
- versioned immutable public URLs;
- presets и variants;
- `f=auto` с negotiation по `Accept` header.
Текущий публичный URL для managed images остаётся базовым delivery contract:
```text
GET /images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto
```
## Кабинет пользователя
На первом этапе кабинет показывает только раздел работы с изображениями.
Пользователь должен иметь возможность:
- загружать изображения;
- создавать и редактировать image presets;
- смотреть список загруженных изображений;
- смотреть версии, variants, статусы обработки и готовые URL;
- выпускать API-ключи для проекта.
В будущем кабинет расширяется разделами:
- Images;
- Videos;
- Sprites;
- Fonts;
- другие типы ассетов при появлении продуктовой необходимости.
## Projects
`Project` становится основной областью изоляции.
К проекту должны относиться:
- assets;
- presets;
- builds;
- variants/results;
- API keys;
- лимиты;
- настройки delivery;
- allowlist источников;
- usage/billing metrics, если они появятся.
На первом этапе можно развивать images внутри проекта, не создавая преждевременно универсальную модель для всех типов ассетов.
## API Keys
Для каждого проекта пользователь может выпускать API-ключи.
API-ключ нужен для server-side интеграций, где backend клиента программно добавляет файлы, запускает обработку и получает результаты.
Ключ должен храниться безопасно:
- secret показывается пользователю только при создании;
- в базе хранится hash секрета;
- в UI отображается только prefix/identifier;
- ключ можно отозвать;
- ключ может иметь scopes;
- желательно хранить дату последнего использования.
Базовые scopes:
```text
assets:read
assets:write
assets:delete
presets:read
presets:write
builds:read
builds:write
```
Delivery URLs остаются публичными и не требуют `Authorization`, если конкретный проект не включает приватный delivery mode.
## Headless API
Платформа должна предоставлять публичный management API для backend-клиентов.
Минимальный image API первого этапа:
```text
POST /api/v1/images
GET /api/v1/images
GET /api/v1/images/{id}
DELETE /api/v1/images/{id}
POST /api/v1/images/{id}/builds
GET /api/v1/images/{id}/builds
GET /api/v1/images/{id}/results
GET /api/v1/image-presets
POST /api/v1/project-api-keys
GET /api/v1/project-api-keys
DELETE /api/v1/project-api-keys/{id}
```
API должен поддерживать загрузку файла напрямую, а не только регистрацию внешнего `sourceUrl`.
Пример server-side сценария:
```text
1. Backend клиента отправляет изображение в API платформы с project API key.
2. Платформа сохраняет original, создаёт asset и version.
3. Backend клиента указывает preset или запускает build.
4. Worker генерирует variants/results.
5. Backend клиента получает готовые public delivery URL.
```
## Builds и Results
`Build` описывает запуск обработки ассета по preset или transform config.
`Result` или `Variant` описывает готовый артефакт, который можно доставлять через public URL.
Для images текущая сущность `image_variants` уже выполняет роль результата обработки. При развитии API можно добавить explicit build layer, не ломая текущий read-through delivery flow.
## Realtime transforms и cropping
Платформа должна поддерживать два режима обработки изображений:
- preset builds - заранее заданные и ограниченные variants;
- realtime transforms - динамические resize/crop/format/quality операции через delivery URL.
Realtime crop должен быть ограничен правилами проекта и preset/custom transform config, чтобы пользователь не мог бесконтрольно создавать произвольные дорогие трансформации.
Первый запрос на dynamic transform может генерировать результат через worker/imgproxy и сохранять его в storage/cache. Следующие запросы должны отдавать уже готовый артефакт.
Пример будущего dynamic transform URL:
```text
GET /images/{assetId}/v{version}/custom?w=800&h=600&fit=fill&crop=center&f=auto&q=80
```
Параметры, влияющие на bytes, должны входить в deterministic variant hash и S3 key.
## Workers
Для каждого типа ассетов предусматривается специализированный worker.
Общий orchestration остаётся в backend, database, queue и storage. Worker конкретного типа отвечает за инструменты обработки этого типа.
Планируемая модель:
```text
image-worker -> imgproxy / sharp / imagemagick
video-worker -> ffmpeg
font-worker -> fonttools / subset tools
sprite-worker -> svg/css sprite builder
```
На первом этапе реализуется и развивается `image-worker`. Остальные worker'ы добавляются только при появлении соответствующих продуктовых задач.
## Архитектурный принцип
Не нужно преждевременно строить универсальный engine для всех возможных ассетов.
Правильное направление:
- делать images как первый полноценный модуль;
- общие сущности называть так, чтобы они не блокировали будущие типы ассетов;
- выносить в общий слой только реально общие части: projects, API keys, queue orchestration, storage contract, statuses, delivery concepts;
- типоспецифичную обработку держать внутри конкретного модуля.
Примеры naming direction:
```text
Project вместо ImageProject
ProjectApiKey вместо ImageApiKey
ProcessingJob вместо ImageWorkerJob
AssetBuild вместо ImageBuild, если build станет общим понятием
```
При этом текущие `image_assets`, `image_asset_versions` и `image_variants` могут оставаться конкретными image-таблицами, пока images являются единственным реализованным типом ассетов.

View File

@@ -63,7 +63,7 @@ Final signed URL:
## Security rules
- Не отдавать `IMGPROXY_KEY` и `IMGPROXY_SALT` в браузер.
- Source URL валидировать в Backend/worker.
- Source URL валидировать в Backend/worker для managed assets и в Gateway для remote source mode.
- Разрешать только `http` и `https`.
- Запрещать localhost, private IP, loopback, link-local.
- Source host должен быть разрешён mock allowlist `SOURCE_ALLOWED_HOSTS`; таблица `allowed_image_hosts` остаётся для будущего CRUD.

View File

@@ -12,7 +12,9 @@ Next.js custom loader получает только:
Поэтому public image URL должен принимать эти параметры напрямую и сам запускать read-through генерацию при miss.
## Loader config
## Remote source loader config
Remote source mode нужен для сценария, где consumer project уже имеет изображение в `public` или внешний image URL и хочет получить `srcset` без предварительной регистрации asset.
В Next.js приложении используется `loaderFile`:
@@ -32,14 +34,14 @@ module.exports = {
```js
"use client"
const imageBaseUrl = process.env.NEXT_PUBLIC_IMAGE_PLATFORM_URL
import { createImagePlatformNextLoader } from "@image-platform/client"
export default function imagePlatformLoader({ src, width, quality }) {
const normalizedSrc = src.startsWith("/") ? src.slice(1) : src
const q = quality || 80
return `${imageBaseUrl}/images/${normalizedSrc}?w=${width}&q=${q}&f=auto`
}
export default createImagePlatformNextLoader({
baseUrl: process.env.NEXT_PUBLIC_IMAGE_PLATFORM_URL,
preset: "card",
project: process.env.NEXT_PUBLIC_IMAGE_PLATFORM_PROJECT,
sourceBaseUrl: process.env.NEXT_PUBLIC_SITE_ORIGIN,
})
```
Пример использования:
@@ -48,12 +50,28 @@ export default function imagePlatformLoader({ src, width, quality }) {
import Image from "next/image"
export function ProductCard() {
return <Image src="asset_123/v4/card" width={640} height={420} alt="Product" />
return <Image src="/images/product.jpg" width={640} height={420} alt="Product" />
}
```
Если `src` относительный, `sourceBaseUrl` превращает его в абсолютный source URL, например `https://site.example.com/images/product.jpg`. Если `src` уже абсолютный, он передаётся как есть.
## Public URL
Remote source URL:
```text
GET /p/{project}/remote/{preset}?src={sourceUrl}&w={width}&q={quality}&f=auto
```
Пример:
```text
https://img.example.com/p/acme/remote/card?src=https%3A%2F%2Fsite.example.com%2Fimages%2Fproduct.jpg&w=640&q=80&f=auto
```
Managed asset URL:
```text
GET /images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto
```
@@ -66,10 +84,12 @@ Route реализован в Fastify Gateway. Для `card` ширина дол
https://img.example.com/images/asset_123/v4/card?w=640&q=80&f=auto
```
`src` не должен быть source URL. Это должен быть versioned platform identifier, например `asset_123/v4/card`. Source URL хранится в PostgreSQL и не раскрывается в public image URL.
Для managed asset `src` не должен быть source URL. Это должен быть versioned platform identifier, например `asset_123/v4/card`. Source URL хранится в PostgreSQL и не раскрывается в public image URL.
`v{version}` меняется при обновлении source image. Старые URL можно кэшировать как immutable без purge.
Для remote source `src` является исходным URL. Этот режим не immutable по умолчанию: Gateway отдаёт `GATEWAY_REMOTE_CACHE_CONTROL`, потому что источник может быть mutable.
## Format auto
`f=auto` выбирает output format по `Accept` header:

View File

@@ -0,0 +1,60 @@
# @unpic/react Provider
`@unpic/react` интегрируется через base component и custom transformer из `@image-platform/client`.
## Remote source mode
Remote source mode принимает обычный image `src` из проекта или внешний URL и генерирует responsive `srcset` через Gateway:
```text
GET /p/{project}/remote/{preset}?src={sourceUrl}&w={width}&q={quality}&f=auto
```
## Usage
```tsx
import { Image } from "@unpic/react/base"
import { imagePlatformUnpicTransformer } from "@image-platform/client"
export function ProductImage() {
return (
<Image
alt="Product"
layout="constrained"
options={{
baseUrl: "https://img.example.com",
preset: "card",
project: "acme",
sourceBaseUrl: "https://site.example.com",
}}
src="/images/product.jpg"
transformer={imagePlatformUnpicTransformer}
width={640}
height={420}
/>
)
}
```
Если `src` относительный, `sourceBaseUrl` превращает его в абсолютный source URL. Если `src` уже абсолютный, он используется без изменений.
## Breakpoints
Static presets принимают только разрешённые widths. Для `card` это `320`, `640`, `960`, поэтому consumer должен передать compatible `breakpoints` или использовать Next/Unpic config, который не генерирует лишние widths.
```tsx
<Image
alt="Product"
breakpoints={[320, 640, 960]}
height={420}
layout="constrained"
options={{ baseUrl: "https://img.example.com", preset: "card", project: "acme", sourceBaseUrl: "https://site.example.com" }}
src="/images/product.jpg"
transformer={imagePlatformUnpicTransformer}
width={640}
/>
```
## Auth
Image delivery URL публичный и не использует `Authorization`. Management API tokens нужны только server-side для создания assets, versions и variants.