chore(openspec): archive completed changes and sync main specs
- archive local-fs-storage-backend, sqlite-metadata-backend, storage-orphan-reconciliation, metadata-managed-jwt-secret, default-local-backends - sync specs: storage-backends, metadata-backends, storage-reconciliation, app-secrets; update photos/share-links - defaults reflected in specs: SQLite and local FS
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# Proposal
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Слои данных были по умолчанию внешними (`postgres` и `s3`), поэтому самодостаточный
|
||||||
|
локальный запуск без внешних сервисов требовал явной настройки. Нужно сделать локальные
|
||||||
|
бэкенды значением по умолчанию.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Сменить значения по умолчанию: `DB_BACKEND=sqlite` (`SQLITE_PATH=./galo.db`) и
|
||||||
|
`STORAGE_BACKEND=fs` (`FS_ROOT=./data`).
|
||||||
|
- Обновить `.env.example`, `project.md`, `README.md`.
|
||||||
|
- Поведение при явном выборе `postgres`/`s3` не меняется.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
<!-- нет -->
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `metadata-backends`: значением по умолчанию становится встроенная SQLite (ранее PostgreSQL).
|
||||||
|
- `storage-backends`: значением по умолчанию становится локальная ФС (ранее S3-совместимая).
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- `backend/internal/config` (значения по умолчанию) и тесты.
|
||||||
|
- `.env.example`, `project.md`, `README.md`.
|
||||||
|
- **BREAKING (поведение):** развёртывания без явных `DB_BACKEND`/`STORAGE_BACKEND`
|
||||||
|
переключатся на локальные бэкенды; для прежнего режима задайте `postgres`/`s3` явно.
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Spec Delta
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Выбор бэкенда метаданных
|
||||||
|
Система SHALL поддерживать выбор бэкенда слоя метаданных параметром конфигурации,
|
||||||
|
при этом встроенная SQLite SHALL быть значением по умолчанию.
|
||||||
|
|
||||||
|
#### Scenario: Выбор по умолчанию
|
||||||
|
- **WHEN** бэкенд метаданных не задан в конфигурации
|
||||||
|
- **THEN** система использует встроенную SQLite с путём по умолчанию
|
||||||
|
|
||||||
|
#### Scenario: Выбор PostgreSQL
|
||||||
|
- **WHEN** в конфигурации задан бэкенд PostgreSQL
|
||||||
|
- **THEN** метаданные читаются и пишутся во внешнюю PostgreSQL
|
||||||
|
|
||||||
|
#### Scenario: Выбор встроенной SQLite
|
||||||
|
- **WHEN** в конфигурации задан бэкенд SQLite
|
||||||
|
- **THEN** метаданные читаются и пишутся в локальный файл SQLite без внешней службы
|
||||||
|
|
||||||
|
#### Scenario: Неполная конфигурация
|
||||||
|
- **WHEN** конфигурация выбранного бэкенда неполна
|
||||||
|
- **THEN** система отказывается стартовать с явной ошибкой конфигурации
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# Spec Delta
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Выбор бэкенда хранилища
|
||||||
|
Система SHALL поддерживать выбор бэкенда объектного хранилища параметром конфигурации,
|
||||||
|
при этом локальная файловая система SHALL быть значением по умолчанию.
|
||||||
|
|
||||||
|
#### Scenario: Выбор по умолчанию
|
||||||
|
- **WHEN** бэкенд хранилища не задан в конфигурации
|
||||||
|
- **THEN** система хранит и отдаёт блобы из локальной файловой системы с корнем по умолчанию
|
||||||
|
|
||||||
|
#### Scenario: Выбор S3-совместимого бэкенда
|
||||||
|
- **WHEN** в конфигурации задан S3-совместимый бэкенд
|
||||||
|
- **THEN** система хранит и отдаёт блобы через S3-совместимое хранилище
|
||||||
|
|
||||||
|
#### Scenario: Выбор локальной ФС
|
||||||
|
- **WHEN** в конфигурации задан бэкенд локальной ФС
|
||||||
|
- **THEN** система хранит и отдаёт блобы из каталога локальной файловой системы
|
||||||
|
|
||||||
|
#### Scenario: Неполная конфигурация
|
||||||
|
- **WHEN** конфигурация выбранного бэкенда неполна
|
||||||
|
- **THEN** система отказывается стартовать с явной ошибкой конфигурации
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Tasks
|
||||||
|
|
||||||
|
## 1. Дефолты и документация
|
||||||
|
|
||||||
|
- [x] 1.1 Сменить значения по умолчанию в `config` (`DB_BACKEND=sqlite`, `SQLITE_PATH=./galo.db`, `STORAGE_BACKEND=fs`, `FS_ROOT=./data`) и обновить тесты конфига; проверка — `go test ./internal/config` зелёный.
|
||||||
|
- [x] 1.2 Обновить `.env.example`, `project.md`, `README.md`; проверка — запуск без переменных окружения стартует на SQLite+ФС и health показывает `db=ok, fs=ok`.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-11
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# app-secrets Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Хранение секретов приложения в слое метаданных с автогенерацией: единый стабильный
|
||||||
|
секрет на инсталляцию без обязательной настройки окружения.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Хранение секрета приложения
|
||||||
|
Слой метаданных SHALL хранить секрет подписи токенов приложения и SHALL возвращать один
|
||||||
|
и тот же секрет при последующих запусках.
|
||||||
|
|
||||||
|
#### Scenario: Первый запуск
|
||||||
|
- **WHEN** секрет ещё не сохранён
|
||||||
|
- **THEN** система генерирует случайный секрет и сохраняет его в слое метаданных
|
||||||
|
|
||||||
|
#### Scenario: Повторный запуск
|
||||||
|
- **WHEN** секрет уже сохранён
|
||||||
|
- **THEN** система использует сохранённый секрет и не генерирует новый
|
||||||
|
|
||||||
|
### Requirement: Отсутствие ручного секрета
|
||||||
|
Система SHALL работать без секрета, заданного в окружении, и SHALL не требовать его
|
||||||
|
наличия при старте.
|
||||||
|
|
||||||
|
#### Scenario: Секрет не задан в окружении
|
||||||
|
- **WHEN** переменная секрета не задана
|
||||||
|
- **THEN** система запускается, используя секрет из слоя метаданных
|
||||||
|
|
||||||
|
### Requirement: Конкурентный первый запуск
|
||||||
|
При одновременном первом запуске нескольких инстансов на общей базе метаданных система
|
||||||
|
SHALL обеспечивать единый сохранённый секрет для всех инстансов.
|
||||||
|
|
||||||
|
#### Scenario: Гонка первого запуска
|
||||||
|
- **WHEN** два инстанса одновременно обнаруживают отсутствие секрета
|
||||||
|
- **THEN** сохраняется ровно один секрет, и оба инстанса используют его
|
||||||
|
|
||||||
|
### Requirement: Использование секрета для подписи URL
|
||||||
|
При отсутствии отдельного секрета подписи URL система SHALL использовать секрет из слоя
|
||||||
|
метаданных.
|
||||||
|
|
||||||
|
#### Scenario: URL_SECRET не задан
|
||||||
|
- **WHEN** отдельный секрет подписи URL не задан
|
||||||
|
- **THEN** подпись URL объектов использует секрет приложения из слоя метаданных
|
||||||
|
|
||||||
|
#### Scenario: URL_SECRET задан
|
||||||
|
- **WHEN** отдельный секрет подписи URL задан
|
||||||
|
- **THEN** он используется независимо от секрета приложения
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# metadata-backends Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Выбор и работа слоя метаданных: внешняя PostgreSQL или встроенная SQLite, с
|
||||||
|
паритетным поведением для одного экземпляра приложения.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Выбор бэкенда метаданных
|
||||||
|
Система SHALL поддерживать выбор бэкенда слоя метаданных параметром конфигурации,
|
||||||
|
при этом встроенная SQLite SHALL быть значением по умолчанию.
|
||||||
|
|
||||||
|
#### Scenario: Выбор по умолчанию
|
||||||
|
- **WHEN** бэкенд метаданных не задан в конфигурации
|
||||||
|
- **THEN** система использует встроенную SQLite с путём по умолчанию
|
||||||
|
|
||||||
|
#### Scenario: Выбор PostgreSQL
|
||||||
|
- **WHEN** в конфигурации задан бэкенд PostgreSQL
|
||||||
|
- **THEN** метаданные читаются и пишутся во внешнюю PostgreSQL
|
||||||
|
|
||||||
|
#### Scenario: Выбор встроенной SQLite
|
||||||
|
- **WHEN** в конфигурации задан бэкенд SQLite
|
||||||
|
- **THEN** метаданные читаются и пишутся в локальный файл SQLite без внешней службы
|
||||||
|
|
||||||
|
#### Scenario: Неполная конфигурация
|
||||||
|
- **WHEN** конфигурация выбранного бэкенда неполна
|
||||||
|
- **THEN** система отказывается стартовать с явной ошибкой конфигурации
|
||||||
|
|
||||||
|
### Requirement: Паритет поведения бэкендов
|
||||||
|
Оба бэкенда метаданных SHALL обеспечивать одинаковое наблюдаемое поведение системы:
|
||||||
|
аутентификацию, альбомы, роли и доступ, фото, объектный журнал, сборку мусора и
|
||||||
|
очередь генерации вариантов.
|
||||||
|
|
||||||
|
#### Scenario: Одинаковое поведение API
|
||||||
|
- **WHEN** один и тот же сценарий выполняется на PostgreSQL и на SQLite
|
||||||
|
- **THEN** наблюдаемые результаты и ошибки API совпадают
|
||||||
|
|
||||||
|
#### Scenario: Соблюдение прав доступа
|
||||||
|
- **WHEN** на любом бэкенде участник без прав выполняет запрещённое действие
|
||||||
|
- **THEN** система отклоняет действие одинаковым образом
|
||||||
|
|
||||||
|
### Requirement: Встроенный одиночный экземпляр
|
||||||
|
Бэкенд SQLite SHALL работать как самодостаточный режим для одного экземпляра приложения,
|
||||||
|
и система SHALL отвергать или явно документировать использование одного файла SQLite
|
||||||
|
несколькими экземплярами.
|
||||||
|
|
||||||
|
#### Scenario: Один экземпляр
|
||||||
|
- **WHEN** приложение запущено в единственном экземпляре
|
||||||
|
- **THEN** SQLite корректно обслуживает всех пользователей этого экземпляра
|
||||||
|
|
||||||
|
#### Scenario: Совместная работа с файлом
|
||||||
|
- **WHEN** предполагается совместная работа нескольких экземпляров с одним файлом SQLite
|
||||||
|
- **THEN** конфигурация считает такой режим неподдерживаемым
|
||||||
|
|
||||||
|
### Requirement: Атомарность жизненного цикла объектов
|
||||||
|
На обоих бэкендах привязка готового объекта к фото и заявка объекта на удаление SHALL
|
||||||
|
быть атомарными и SHALL исключать привязку объекта, который собирается быть удалён, и
|
||||||
|
удаление используемого объекта.
|
||||||
|
|
||||||
|
#### Scenario: Конкурентная привязка и сборка мусора
|
||||||
|
- **WHEN** привязка объекта и заявка на его удаление происходят одновременно
|
||||||
|
- **THEN** система либо привязывает объект без удаления, либо удаляет его без привязки
|
||||||
|
|
||||||
|
#### Scenario: Повторная привязка
|
||||||
|
- **WHEN** объект уже привязан к фото в роли
|
||||||
|
- **THEN** повторная привязка приводит к согласованному состоянию без дублей
|
||||||
|
|
||||||
|
### Requirement: Устойчивость к перезапуску
|
||||||
|
Очередь генерации вариантов и сборка мусора SHALL восстанавливаться из состояния слоя
|
||||||
|
метаданных после перезапуска на обоих бэкендах, и система SHALL применять схему
|
||||||
|
метаданных при старте.
|
||||||
|
|
||||||
|
#### Scenario: Незавершённые задания
|
||||||
|
- **WHEN** процесс перезапускается с незавершёнными заданиями генерации вариантов
|
||||||
|
- **THEN** обработка заданий продолжается
|
||||||
|
|
||||||
|
#### Scenario: Применение схемы
|
||||||
|
- **WHEN** приложение стартует с выбранным бэкендом
|
||||||
|
- **THEN** схема метаданных выбранного бэкенда приведена к актуальному состоянию
|
||||||
@@ -32,18 +32,19 @@
|
|||||||
- **THEN** система возвращает не более лимита фото и курсор следующей страницы
|
- **THEN** система возвращает не более лимита фото и курсор следующей страницы
|
||||||
|
|
||||||
### Requirement: Получение фото по требованию
|
### Requirement: Получение фото по требованию
|
||||||
Клиент SHALL иметь возможность получить актуальные данные фото с presigned URL.
|
Клиент SHALL иметь возможность получить актуальные данные фото с подписанным URL,
|
||||||
|
обслуживаемым бэкендом, независимо от бэкенда хранения.
|
||||||
|
|
||||||
#### Scenario: Выдача свежего URL
|
#### Scenario: Выдача свежего URL
|
||||||
- **WHEN** клиент запрашивает фото по id
|
- **WHEN** клиент запрашивает фото по id
|
||||||
- **THEN** система возвращает свежий presigned URL (срок до 1 часа) для просмотра
|
- **THEN** система возвращает свежий подписанный URL бэкенда (срок до 1 часа) для просмотра
|
||||||
|
|
||||||
### Requirement: Скачивание одиночного фото
|
### Requirement: Скачивание одиночного фото
|
||||||
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
|
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
|
||||||
|
|
||||||
#### Scenario: Скачивание фото
|
#### Scenario: Скачивание фото
|
||||||
- **WHEN** участник запрашивает скачивание фото
|
- **WHEN** участник запрашивает скачивание фото
|
||||||
- **THEN** система возвращает presigned URL оригинала
|
- **THEN** система возвращает подписанный URL оригинала, обслуживаемый бэкендом
|
||||||
|
|
||||||
### Requirement: Удаление фото
|
### Requirement: Удаление фото
|
||||||
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото. Удаление SHALL помещать фото в корзину и SHALL не удалять объекты хранилища.
|
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото. Удаление SHALL помещать фото в корзину и SHALL не удалять объекты хранилища.
|
||||||
@@ -68,7 +69,6 @@
|
|||||||
- **WHEN** фото обработано, а обложка уже задана
|
- **WHEN** фото обработано, а обложка уже задана
|
||||||
- **THEN** система не меняет обложку
|
- **THEN** система не меняет обложку
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Переименование фото
|
### Requirement: Переименование фото
|
||||||
Автор фото, владелец и админ альбома SHALL иметь возможность изменить имя файла фото;
|
Автор фото, владелец и админ альбома SHALL иметь возможность изменить имя файла фото;
|
||||||
остальные SHALL получать отказ, а пустое имя SHALL отклоняться.
|
остальные SHALL получать отказ, а пустое имя SHALL отклоняться.
|
||||||
@@ -85,7 +85,6 @@
|
|||||||
- **WHEN** участник без прав переименовывает чужое фото
|
- **WHEN** участник без прав переименовывает чужое фото
|
||||||
- **THEN** система отклоняет запрос
|
- **THEN** система отклоняет запрос
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Ограничение размера загрузки
|
### Requirement: Ограничение размера загрузки
|
||||||
Система SHALL ограничивать размер загружаемых файлов и SHALL отвечать понятной ошибкой при
|
Система SHALL ограничивать размер загружаемых файлов и SHALL отвечать понятной ошибкой при
|
||||||
превышении лимита, а не ошибкой разбора формы.
|
превышении лимита, а не ошибкой разбора формы.
|
||||||
@@ -98,7 +97,6 @@
|
|||||||
- **WHEN** файл не превышает лимит
|
- **WHEN** файл не превышает лимит
|
||||||
- **THEN** система принимает multipart-загрузку и обрабатывает фото
|
- **THEN** система принимает multipart-загрузку и обрабатывает фото
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Метаданные фото (EXIF)
|
### Requirement: Метаданные фото (EXIF)
|
||||||
При загрузке фото система SHALL извлекать распознаваемые EXIF-атрибуты и SHALL возвращать их
|
При загрузке фото система SHALL извлекать распознаваемые EXIF-атрибуты и SHALL возвращать их
|
||||||
вместе с данными фото; при отсутствии метаданных SHALL возвращать пустой набор.
|
вместе с данными фото; при отсутствии метаданных SHALL возвращать пустой набор.
|
||||||
@@ -111,7 +109,6 @@
|
|||||||
- **WHEN** загружается фото без читаемых EXIF
|
- **WHEN** загружается фото без читаемых EXIF
|
||||||
- **THEN** данные фото не содержат атрибутов, а загрузка выполняется успешно
|
- **THEN** данные фото не содержат атрибутов, а загрузка выполняется успешно
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Перенос и копирование фото между альбомами
|
### Requirement: Перенос и копирование фото между альбомами
|
||||||
Пользователь с правом удаления в исходном альбоме и правом загрузки в целевом SHALL иметь
|
Пользователь с правом удаления в исходном альбоме и правом загрузки в целевом SHALL иметь
|
||||||
возможность перенести или скопировать фото в другой альбом. Операции SHALL изменять только
|
возможность перенести или скопировать фото в другой альбом. Операции SHALL изменять только
|
||||||
@@ -119,12 +116,11 @@
|
|||||||
|
|
||||||
#### Scenario: Перенос
|
#### Scenario: Перенос
|
||||||
- **WHEN** пользователь переносит фото в другой альбом
|
- **WHEN** пользователь переносит фото в другой альбом
|
||||||
- **THEN** фото исчезает из исходного альбома и появляется в целевом, объекты S3 не меняются
|
- **THEN** фото исчезает из исходного альбома и появляется в целевом, объекты хранилища не меняются
|
||||||
|
|
||||||
#### Scenario: Копирование
|
#### Scenario: Копирование
|
||||||
- **WHEN** пользователь копирует фото в другой альбом
|
- **WHEN** пользователь копирует фото в другой альбом
|
||||||
- **THEN** в целевом альбоме появляется новая запись, ссылающаяся на те же объекты S3
|
- **THEN** в целевом альбоме появляется новая запись, ссылающаяся на те же объекты хранилища
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Корзина фото
|
### Requirement: Корзина фото
|
||||||
Удаление фото SHALL помещать его в корзину (мягкое удаление). Восстановление SHALL возвращать
|
Удаление фото SHALL помещать его в корзину (мягкое удаление). Восстановление SHALL возвращать
|
||||||
@@ -147,7 +143,6 @@
|
|||||||
- **WHEN** на объект хранилища ссылается копия в другом альбоме
|
- **WHEN** на объект хранилища ссылается копия в другом альбоме
|
||||||
- **THEN** запись удаляется, а объект хранилища сохраняется
|
- **THEN** запись удаляется, а объект хранилища сохраняется
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Удаление альбома и восстановление фото без альбома
|
### Requirement: Удаление альбома и восстановление фото без альбома
|
||||||
Удаление альбома SHALL помещать его фото в корзину, не удаляя объекты хранилища. Если при
|
Удаление альбома SHALL помещать его фото в корзину, не удаляя объекты хранилища. Если при
|
||||||
восстановлении фото его альбом отсутствует, система SHALL предложить выбрать существующий
|
восстановлении фото его альбом отсутствует, система SHALL предложить выбрать существующий
|
||||||
@@ -161,7 +156,6 @@
|
|||||||
- **WHEN** пользователь восстанавливает фото, у которого нет альбома
|
- **WHEN** пользователь восстанавливает фото, у которого нет альбома
|
||||||
- **THEN** система предлагает выбрать альбом или создать новый и восстанавливает фото в него
|
- **THEN** система предлагает выбрать альбом или создать новый и восстанавливает фото в него
|
||||||
|
|
||||||
|
|
||||||
### Requirement: Метки фото
|
### Requirement: Метки фото
|
||||||
Пользователь SHALL присваивать фото свои метки (область — пользователь): создавать новые и
|
Пользователь SHALL присваивать фото свои метки (область — пользователь): создавать новые и
|
||||||
выбирать из списка с поиском; метки SHALL отдаваться вместе с фото и сниматься.
|
выбирать из списка с поиском; метки SHALL отдаваться вместе с фото и сниматься.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ View-only доступ к альбому по ссылке без авториз
|
|||||||
|
|
||||||
#### Scenario: Просмотр по действующей ссылке
|
#### Scenario: Просмотр по действующей ссылке
|
||||||
- **WHEN** аноним открывает ссылку с действующим токеном
|
- **WHEN** аноним открывает ссылку с действующим токеном
|
||||||
- **THEN** система возвращает фото альбома со свежими presigned URL
|
- **THEN** система возвращает фото альбома со свежими подписанными URL, обслуживаемыми бэкендом
|
||||||
|
|
||||||
#### Scenario: Истёкшая или отозванная ссылка
|
#### Scenario: Истёкшая или отозванная ссылка
|
||||||
- **WHEN** аноним открывает ссылку с истёкшим или отозванным токеном
|
- **WHEN** аноним открывает ссылку с истёкшим или отозванным токеном
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# storage-backends Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Выбор и работа объектного хранилища: S3-совместимое (Garage) или локальная файловая
|
||||||
|
система. Блобы всегда отдаются через бэкенд по подписанному URL, хранилище клиенту не
|
||||||
|
раскрывается.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Выбор бэкенда хранилища
|
||||||
|
Система SHALL поддерживать выбор бэкенда объектного хранилища параметром конфигурации,
|
||||||
|
при этом локальная файловая система SHALL быть значением по умолчанию.
|
||||||
|
|
||||||
|
#### Scenario: Выбор по умолчанию
|
||||||
|
- **WHEN** бэкенд хранилища не задан в конфигурации
|
||||||
|
- **THEN** система хранит и отдаёт блобы из локальной файловой системы с корнем по умолчанию
|
||||||
|
|
||||||
|
#### 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** состояние здоровья отражает деградацию
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# storage-reconciliation Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Ручная сверка объектного хранилища с журналом метаданных: инвентаризация обеих сторон,
|
||||||
|
отчёт о расхождениях и безопасное удаление осиротевших блобов.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Инвентаризация хранилища
|
||||||
|
Слой хранения SHALL перечислять объекты под заданным префиксом, возвращая ключ, размер и
|
||||||
|
время изменения, независимо от бэкенда (S3-совместимое или локальная ФС).
|
||||||
|
|
||||||
|
#### Scenario: Перечисление в S3-совместимом хранилище
|
||||||
|
- **WHEN** запрашивается список объектов под префиксом
|
||||||
|
- **THEN** возвращаются все ключи с этим префиксом с их размером и временем изменения
|
||||||
|
|
||||||
|
#### Scenario: Перечисление в локальной ФС
|
||||||
|
- **WHEN** хранилищем выбрана локальная ФС
|
||||||
|
- **THEN** возвращаются ключи всех блобов под префиксом с их размером и временем изменения
|
||||||
|
|
||||||
|
### Requirement: Инвентаризация журнала
|
||||||
|
Слой метаданных SHALL возвращать все ключи, зарегистрированные в журнале `objects`, на
|
||||||
|
обоих бэкендах (PostgreSQL и SQLite).
|
||||||
|
|
||||||
|
#### Scenario: Ключи журнала
|
||||||
|
- **WHEN** запрашиваются ключи журнала
|
||||||
|
- **THEN** возвращается множество ключей всех строк `objects`
|
||||||
|
|
||||||
|
### Requirement: Отчёт о расхождениях
|
||||||
|
По умолчанию сверка SHALL выполняться в режиме отчёта и SHALL не изменять хранилище;
|
||||||
|
отчёт SHALL различать сироты (в хранилище есть, в журнале нет) и пропавшие (в журнале
|
||||||
|
есть, в хранилище нет).
|
||||||
|
|
||||||
|
#### Scenario: Только отчёт
|
||||||
|
- **WHEN** сверка запущена без флага удаления
|
||||||
|
- **THEN** система выводит число и примеры сирот и пропавших, ничего не удаляя
|
||||||
|
|
||||||
|
#### Scenario: Пропавшие не восстанавливаются
|
||||||
|
- **WHEN** ключ есть в журнале, но отсутствует в хранилище
|
||||||
|
- **THEN** он сообщается как пропавший и не создаётся и не удаляется
|
||||||
|
|
||||||
|
### Requirement: Удаление сирот по явному запросу
|
||||||
|
Удаление сирот SHALL выполняться только по явному флагу, только для ключей под префиксом
|
||||||
|
объектов и только для блобов старше заданного минимального возраста.
|
||||||
|
|
||||||
|
#### Scenario: Удаление включено
|
||||||
|
- **WHEN** сверка запущена с флагом удаления
|
||||||
|
- **THEN** сироты старше минимального возраста удаляются из хранилища, а более молодые пропускаются
|
||||||
|
|
||||||
|
#### Scenario: Защита от гонки с загрузкой
|
||||||
|
- **WHEN** блоб создан недавно (моложе минимального возраста)
|
||||||
|
- **THEN** он не удаляется, даже если строки в журнале ещё нет
|
||||||
|
|
||||||
|
#### Scenario: Ограничение области
|
||||||
|
- **WHEN** удаление включено
|
||||||
|
- **THEN** удаляются только ключи под префиксом объектов, остальные не затрагиваются
|
||||||
|
|
||||||
|
### Requirement: Независимость от бэкендов
|
||||||
|
Сверка SHALL работать с любой комбинацией бэкенда хранения (S3 или ФС) и бэкенда
|
||||||
|
метаданных (PostgreSQL или SQLite).
|
||||||
|
|
||||||
|
#### Scenario: Комбинации бэкендов
|
||||||
|
- **WHEN** сверка запускается на выбранной конфигурации
|
||||||
|
- **THEN** она использует выбранные бэкенды хранения и метаданных без изменений кода
|
||||||
Reference in New Issue
Block a user