feat(storage): serve media through backend and add local FS backend

- introduce storage.Backend with S3 and local file-system implementations
- deliver all blobs via signed GET/HEAD /api/objects/<key> (Range, Content-Type)
- stream S3 objects through the backend; remove presigned URL path
- share shared HMAC Signer between backends
- remove S3_PUBLIC_URL; rename S3_PRESIGN_TTL to OBJECT_URL_TTL
- make storage label in health dynamic; nginx proxy_buffering off for objects
This commit is contained in:
iu
2026-10-11 09:15:03 +03:00
parent 7ae9bcf305
commit ec8d907565
28 changed files with 1714 additions and 65 deletions
+25 -6
View File
@@ -144,14 +144,19 @@ PG_PASSWORD=change_me
PG_DATABASE=shared_album
PG_SSLMODE=disable
# S3 (Garage)
# S3 (Garage) - внутреннее хранилище, клиенту не раскрывается
S3_ENDPOINT=http://s3.localnet:3900
S3_REGION=garage
S3_ACCESS_KEY=GKchange_me
S3_SECRET_KEY=change_me
S3_BUCKET=albums
S3_USE_PATH_STYLE=true
S3_PUBLIC_URL=http://s3.localnet:3900/albums
# Storage backend
STORAGE_BACKEND=s3 # s3 (default) | fs (local filesystem)
FS_ROOT=/var/lib/galo/objects # required when STORAGE_BACKEND=fs
URL_SECRET=change_me # signs object URLs; falls back to JWT_SECRET
OBJECT_URL_TTL=1h # lifetime of signed object URLs
# Auth
JWT_SECRET=change_me_long_random
@@ -339,6 +344,11 @@ colors: {
## 10. Слой хранения и слой метаданных
**Слой хранения выбирается параметром `STORAGE_BACKEND`:** `s3` (S3-совместимое
хранилище, по умолчанию) или `fs` (локальная файловая система, `FS_ROOT`). Оба
бэкенда реализуют единый интерфейс `storage.Backend` и используют одни и те же
непрозрачные ключи, поэтому переключение не меняет формат данных.
**S3 (Garage) — непрозрачное объектное хранилище.** Ключи не содержат сущностей
метаданных (альбома, фото). Каждый физический файл адресуется своим `object_id`:
@@ -350,6 +360,11 @@ colors: {
└── preview.webp
```
При `STORAGE_BACKEND=fs` те же ключи отображаются в файлы под `FS_ROOT`. Отдача
идёт через сам сервис: `GET /api/objects/<key>` с HMAC-подписью (`e`, `s`),
поддержкой Range и Content-Type. Один и тот же маршрут обслуживает авторизованных
пользователей и анонимные share-ссылки.
**Postgres — журнал истины.** Таблица `objects` описывает каждый блоб (ключ, mime,
размер, габариты, checksum, состояние `pending|ready|deleting`), таблица
`photo_objects` связывает фото с объектами по ролям (`original|thumb|preview`),
@@ -358,13 +373,16 @@ colors: {
Следствия:
- **Копирование** между альбомами — только новые строки `photos`+`photo_objects`,
ссылающиеся на те же объекты; операции с S3 не выполняются.
ссылающиеся на те же объекты; операции с хранилищем не выполняются.
- **Перенос** — только `UPDATE photos.album_id`; объекты не трогаются.
- **Удаление** фото удаляет строки; фоновый воркер удаляет блоб лишь когда на
объект не осталось ни одной ссылки (сборка мусора по ledger-состояниям).
Доступ: **presigned URL** со сроком 1 час (бакет приватный).
Garage поддерживает presigned URL через SigV4 query-string signatures.
Доступ: **только через бэкенд** — подписанный URL со сроком 1 час на
`/api/objects/<key>`, независимо от бэкенда хранилища. Для S3 (Garage) байты
стримятся из внутренней сети через бэкенд (ranged `GetObject`), presigned URL наружу
не выдаются; для ФС — чтение с диска. Переключение бэкенда не переносит существующие
блобы — перенос выполняется вручную.
## 11. API-эндпоинты
@@ -609,7 +627,8 @@ invites, photos, comments, likes, notifications. Применяются к pg.lo
Реализуй загрузку фото в Garage с генерацией трёх превью
(thumb 200px, preview 1200px, original) и извлечением EXIF.
Структура ключей: albums/{album_id}/{original|preview|thumb}/{photo_id}.{ext|webp}.
Отдавай фото через presigned URL (срок 1 час).
Отдавай фото через бэкенд: подписанный URL `/api/objects/<key>` (срок 1 час),
S3-байты стримятся из внутренней сети через бэкенд.
Эндпоинты загрузки, списка с cursor-пагинацией, удаления.
На фронтенде — Uploader с drag&drop и прогрессом, PhotoGrid с ленивой загрузкой,
Lightbox со свайпами.