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,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** система возвращает не более лимита фото и курсор следующей страницы
|
||||
|
||||
### Requirement: Получение фото по требованию
|
||||
Клиент SHALL иметь возможность получить актуальные данные фото с presigned URL.
|
||||
Клиент SHALL иметь возможность получить актуальные данные фото с подписанным URL,
|
||||
обслуживаемым бэкендом, независимо от бэкенда хранения.
|
||||
|
||||
#### Scenario: Выдача свежего URL
|
||||
- **WHEN** клиент запрашивает фото по id
|
||||
- **THEN** система возвращает свежий presigned URL (срок до 1 часа) для просмотра
|
||||
- **THEN** система возвращает свежий подписанный URL бэкенда (срок до 1 часа) для просмотра
|
||||
|
||||
### Requirement: Скачивание одиночного фото
|
||||
Любой участник, включая viewer, SHALL иметь возможность скачать одиночное фото.
|
||||
|
||||
#### Scenario: Скачивание фото
|
||||
- **WHEN** участник запрашивает скачивание фото
|
||||
- **THEN** система возвращает presigned URL оригинала
|
||||
- **THEN** система возвращает подписанный URL оригинала, обслуживаемый бэкендом
|
||||
|
||||
### Requirement: Удаление фото
|
||||
Автор SHALL иметь возможность удалить своё фото; владелец и админ SHALL иметь возможность удалить любое фото. Удаление SHALL помещать фото в корзину и SHALL не удалять объекты хранилища.
|
||||
@@ -68,7 +69,6 @@
|
||||
- **WHEN** фото обработано, а обложка уже задана
|
||||
- **THEN** система не меняет обложку
|
||||
|
||||
|
||||
### Requirement: Переименование фото
|
||||
Автор фото, владелец и админ альбома SHALL иметь возможность изменить имя файла фото;
|
||||
остальные SHALL получать отказ, а пустое имя SHALL отклоняться.
|
||||
@@ -85,7 +85,6 @@
|
||||
- **WHEN** участник без прав переименовывает чужое фото
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
|
||||
### Requirement: Ограничение размера загрузки
|
||||
Система SHALL ограничивать размер загружаемых файлов и SHALL отвечать понятной ошибкой при
|
||||
превышении лимита, а не ошибкой разбора формы.
|
||||
@@ -98,7 +97,6 @@
|
||||
- **WHEN** файл не превышает лимит
|
||||
- **THEN** система принимает multipart-загрузку и обрабатывает фото
|
||||
|
||||
|
||||
### Requirement: Метаданные фото (EXIF)
|
||||
При загрузке фото система SHALL извлекать распознаваемые EXIF-атрибуты и SHALL возвращать их
|
||||
вместе с данными фото; при отсутствии метаданных SHALL возвращать пустой набор.
|
||||
@@ -111,7 +109,6 @@
|
||||
- **WHEN** загружается фото без читаемых EXIF
|
||||
- **THEN** данные фото не содержат атрибутов, а загрузка выполняется успешно
|
||||
|
||||
|
||||
### Requirement: Перенос и копирование фото между альбомами
|
||||
Пользователь с правом удаления в исходном альбоме и правом загрузки в целевом SHALL иметь
|
||||
возможность перенести или скопировать фото в другой альбом. Операции SHALL изменять только
|
||||
@@ -119,12 +116,11 @@
|
||||
|
||||
#### Scenario: Перенос
|
||||
- **WHEN** пользователь переносит фото в другой альбом
|
||||
- **THEN** фото исчезает из исходного альбома и появляется в целевом, объекты S3 не меняются
|
||||
- **THEN** фото исчезает из исходного альбома и появляется в целевом, объекты хранилища не меняются
|
||||
|
||||
#### Scenario: Копирование
|
||||
- **WHEN** пользователь копирует фото в другой альбом
|
||||
- **THEN** в целевом альбоме появляется новая запись, ссылающаяся на те же объекты S3
|
||||
|
||||
- **THEN** в целевом альбоме появляется новая запись, ссылающаяся на те же объекты хранилища
|
||||
|
||||
### Requirement: Корзина фото
|
||||
Удаление фото SHALL помещать его в корзину (мягкое удаление). Восстановление SHALL возвращать
|
||||
@@ -147,7 +143,6 @@
|
||||
- **WHEN** на объект хранилища ссылается копия в другом альбоме
|
||||
- **THEN** запись удаляется, а объект хранилища сохраняется
|
||||
|
||||
|
||||
### Requirement: Удаление альбома и восстановление фото без альбома
|
||||
Удаление альбома SHALL помещать его фото в корзину, не удаляя объекты хранилища. Если при
|
||||
восстановлении фото его альбом отсутствует, система SHALL предложить выбрать существующий
|
||||
@@ -161,7 +156,6 @@
|
||||
- **WHEN** пользователь восстанавливает фото, у которого нет альбома
|
||||
- **THEN** система предлагает выбрать альбом или создать новый и восстанавливает фото в него
|
||||
|
||||
|
||||
### Requirement: Метки фото
|
||||
Пользователь SHALL присваивать фото свои метки (область — пользователь): создавать новые и
|
||||
выбирать из списка с поиском; метки SHALL отдаваться вместе с фото и сниматься.
|
||||
|
||||
@@ -25,7 +25,7 @@ View-only доступ к альбому по ссылке без авториз
|
||||
|
||||
#### Scenario: Просмотр по действующей ссылке
|
||||
- **WHEN** аноним открывает ссылку с действующим токеном
|
||||
- **THEN** система возвращает фото альбома со свежими presigned URL
|
||||
- **THEN** система возвращает фото альбома со свежими подписанными URL, обслуживаемыми бэкендом
|
||||
|
||||
#### Scenario: Истёкшая или отозванная ссылка
|
||||
- **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