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
@@ -0,0 +1,108 @@
# Spec Delta
## Purpose
Выбор и работа объектного хранилища: S3-совместимое (Garage) или локальная файловая
система. Блобы всегда отдаются через бэкенд по подписанному URL, хранилище клиенту не
раскрывается.
## ADDED Requirements
### Requirement: Выбор бэкенда хранилища
Система SHALL поддерживать выбор бэкенда объектного хранилища параметром конфигурации,
при этом S3-совместимый бэкенд SHALL оставаться значением по умолчанию.
#### Scenario: Выбор S3-совместимого бэкенда
- **WHEN** в конфигурации задан S3-совместимый бэкенд
- **THEN** система хранит и отдаёт блобы через S3-совместимое хранилище
#### Scenario: Выбор локальной ФС
- **WHEN** в конфигурации задан бэкенд локальной ФС
- **THEN** система хранит и отдаёт блобы из каталога локальной файловой системы
#### Scenario: Неполная конфигурация
- **WHEN** конфигурация выбранного бэкенда неполна
- **THEN** система отказывается стартовать с явной ошибкой конфигурации
### Requirement: Единая абстракция хранилища
Операции записи, чтения, удаления и проверки доступности SHALL выполняться через единый
интерфейс хранилища, одинаковый для всех бэкендов; ключи объектов SHALL оставаться
непрозрачными и независимыми от бэкенда.
#### Scenario: Ключи переносимы между бэкендами
- **WHEN** один и тот же ключ объекта используется на разных бэкендах
- **THEN** он адресует тот же логический блоб без изменения формата
#### Scenario: Удаление объекта
- **WHEN** система удаляет объект через интерфейс хранилища
- **THEN** блоб удаляется выбранным бэкендом
### Requirement: Отдача блобов через бэкенд
Клиент SHALL получать любой блоб по URL, обслуживаемому бэкендом, с ограниченным сроком
действия; ни один бэкенд SHALL не отдавать клиенту ссылку напрямую на объектное
хранилище. Для анонимного доступа по share-ссылке подписанный URL SHALL быть действителен
без пользовательской сессии.
#### Scenario: Свежий URL просмотра
- **WHEN** клиент запрашивает фото
- **THEN** система возвращает подписанный URL бэкенда со сроком действия до 1 часа
#### Scenario: Доступ по share-ссылке
- **WHEN** аноним открывает альбом по действующей share-ссылке
- **THEN** полученные подписанные URL позволяют загрузить фото без сессии
#### Scenario: Истёкшая подпись
- **WHEN** клиент обращается к URL с истёкшей или неверной подписью
- **THEN** система отказывает в доступе
### Requirement: Хранилище не раскрывается клиенту
Адрес и учётные данные объектного хранилища SHALL оставаться внутренними; клиент SHALL
взаимодействовать с медиа только через бэкенд, независимо от бэкенда хранения.
#### Scenario: S3-совместимый бэкенд
- **WHEN** хранилищем выбран S3-совместимый бэкенд
- **THEN** выдаваемый клиенту URL указывает на бэкенд, а не на S3-эндпоинт
#### Scenario: Локальная ФС
- **WHEN** хранилищем выбрана локальная ФС
- **THEN** выдаваемый клиенту URL указывает на бэкенд
#### Scenario: Хранилище недоступно извне
- **WHEN** объектное хранилище недоступно из внешней сети
- **THEN** просмотр и скачивание медиа продолжают работать
### Requirement: Локальное хранение на ФС
Бэкенд локальной ФС SHALL хранить блобы в пределах заданного корневого каталога по
непрозрачным ключам объектов и SHALL отвергать ключи, выводящие за пределы корня.
#### Scenario: Запись и чтение блоба
- **WHEN** система сохраняет объект с ключом
- **THEN** блоб размещается под корневым каталогом и позже читается по тому же ключу
#### Scenario: Попытка выхода из корня
- **WHEN** ключ содержит элементы обхода каталога
- **THEN** система отвергает операцию и не обращается за пределы корня
### Requirement: Диапазонные запросы и тип содержимого
При отдаче блоба система SHALL возвращать корректный тип содержимого и SHALL
поддерживать диапазонные запросы для докачки и больших файлов.
#### Scenario: Запрос диапазона
- **WHEN** клиент запрашивает часть блоба через диапазон
- **THEN** система возвращает запрошенный диапазон и соответствующий статус
#### Scenario: Тип содержимого
- **WHEN** клиент загружает блоб
- **THEN** ответ содержит тип содержимого, соответствующий объекту
### Requirement: Доступность выбранного хранилища
Система SHALL сообщать о доступности выбранного бэкенда хранилища и отражать его в
состоянии здоровья сервиса.
#### Scenario: Хранилище доступно
- **WHEN** выбранный бэкенд отвечает на проверку
- **THEN** состоянии здоровья отражает хранилище как доступное
#### Scenario: Хранилище недоступно
- **WHEN** выбранный бэкенд не отвечает на проверку
- **THEN** состояние здоровья отражает деградацию