sync
This commit is contained in:
@@ -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`.
|
||||
|
||||
## Компоненты
|
||||
|
||||
214
docs/assets-delivery-platform.md
Normal file
214
docs/assets-delivery-platform.md
Normal 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 являются единственным реализованным типом ассетов.
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
60
docs/unpic-react-provider.md
Normal file
60
docs/unpic-react-provider.md
Normal 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.
|
||||
Reference in New Issue
Block a user