From 8638d1bbfc1fa7980b6924bded708fe2c29678a3 Mon Sep 17 00:00:00 2001 From: Iu Date: Sun, 11 Oct 2026 10:19:44 +0300 Subject: [PATCH] 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 --- .../.openspec.yaml | 0 .../proposal.md | 30 +++++ .../specs/metadata-backends/spec.md | 23 ++++ .../specs/storage-backends/spec.md | 23 ++++ .../tasks.md | 6 + .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/photos/spec.md | 0 .../specs/share-links/spec.md | 0 .../specs/storage-backends/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/app-secrets/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/metadata-backends/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 2 + .../design.md | 0 .../proposal.md | 0 .../specs/storage-reconciliation/spec.md | 0 .../tasks.md | 0 openspec/specs/app-secrets/spec.md | 47 ++++++++ openspec/specs/metadata-backends/spec.md | 79 +++++++++++++ openspec/specs/photos/spec.md | 18 +-- openspec/specs/share-links/spec.md | 2 +- openspec/specs/storage-backends/spec.md | 111 ++++++++++++++++++ openspec/specs/storage-reconciliation/spec.md | 64 ++++++++++ 33 files changed, 392 insertions(+), 13 deletions(-) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-default-local-backends}/.openspec.yaml (100%) create mode 100644 openspec/changes/archive/2026-10-11-default-local-backends/proposal.md create mode 100644 openspec/changes/archive/2026-10-11-default-local-backends/specs/metadata-backends/spec.md create mode 100644 openspec/changes/archive/2026-10-11-default-local-backends/specs/storage-backends/spec.md create mode 100644 openspec/changes/archive/2026-10-11-default-local-backends/tasks.md rename openspec/changes/{metadata-managed-jwt-secret => archive/2026-10-11-local-fs-storage-backend}/.openspec.yaml (100%) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-local-fs-storage-backend}/design.md (100%) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-local-fs-storage-backend}/proposal.md (100%) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-local-fs-storage-backend}/specs/photos/spec.md (100%) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-local-fs-storage-backend}/specs/share-links/spec.md (100%) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-local-fs-storage-backend}/specs/storage-backends/spec.md (100%) rename openspec/changes/{local-fs-storage-backend => archive/2026-10-11-local-fs-storage-backend}/tasks.md (100%) rename openspec/changes/{sqlite-metadata-backend => archive/2026-10-11-metadata-managed-jwt-secret}/.openspec.yaml (100%) rename openspec/changes/{metadata-managed-jwt-secret => archive/2026-10-11-metadata-managed-jwt-secret}/design.md (100%) rename openspec/changes/{metadata-managed-jwt-secret => archive/2026-10-11-metadata-managed-jwt-secret}/proposal.md (100%) rename openspec/changes/{metadata-managed-jwt-secret => archive/2026-10-11-metadata-managed-jwt-secret}/specs/app-secrets/spec.md (100%) rename openspec/changes/{metadata-managed-jwt-secret => archive/2026-10-11-metadata-managed-jwt-secret}/tasks.md (100%) rename openspec/changes/{storage-orphan-reconciliation => archive/2026-10-11-sqlite-metadata-backend}/.openspec.yaml (100%) rename openspec/changes/{sqlite-metadata-backend => archive/2026-10-11-sqlite-metadata-backend}/design.md (100%) rename openspec/changes/{sqlite-metadata-backend => archive/2026-10-11-sqlite-metadata-backend}/proposal.md (100%) rename openspec/changes/{sqlite-metadata-backend => archive/2026-10-11-sqlite-metadata-backend}/specs/metadata-backends/spec.md (100%) rename openspec/changes/{sqlite-metadata-backend => archive/2026-10-11-sqlite-metadata-backend}/tasks.md (100%) create mode 100644 openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/.openspec.yaml rename openspec/changes/{storage-orphan-reconciliation => archive/2026-10-11-storage-orphan-reconciliation}/design.md (100%) rename openspec/changes/{storage-orphan-reconciliation => archive/2026-10-11-storage-orphan-reconciliation}/proposal.md (100%) rename openspec/changes/{storage-orphan-reconciliation => archive/2026-10-11-storage-orphan-reconciliation}/specs/storage-reconciliation/spec.md (100%) rename openspec/changes/{storage-orphan-reconciliation => archive/2026-10-11-storage-orphan-reconciliation}/tasks.md (100%) create mode 100644 openspec/specs/app-secrets/spec.md create mode 100644 openspec/specs/metadata-backends/spec.md create mode 100644 openspec/specs/storage-backends/spec.md create mode 100644 openspec/specs/storage-reconciliation/spec.md diff --git a/openspec/changes/local-fs-storage-backend/.openspec.yaml b/openspec/changes/archive/2026-10-11-default-local-backends/.openspec.yaml similarity index 100% rename from openspec/changes/local-fs-storage-backend/.openspec.yaml rename to openspec/changes/archive/2026-10-11-default-local-backends/.openspec.yaml diff --git a/openspec/changes/archive/2026-10-11-default-local-backends/proposal.md b/openspec/changes/archive/2026-10-11-default-local-backends/proposal.md new file mode 100644 index 0000000..f8a6815 --- /dev/null +++ b/openspec/changes/archive/2026-10-11-default-local-backends/proposal.md @@ -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` явно. diff --git a/openspec/changes/archive/2026-10-11-default-local-backends/specs/metadata-backends/spec.md b/openspec/changes/archive/2026-10-11-default-local-backends/specs/metadata-backends/spec.md new file mode 100644 index 0000000..2534818 --- /dev/null +++ b/openspec/changes/archive/2026-10-11-default-local-backends/specs/metadata-backends/spec.md @@ -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** система отказывается стартовать с явной ошибкой конфигурации diff --git a/openspec/changes/archive/2026-10-11-default-local-backends/specs/storage-backends/spec.md b/openspec/changes/archive/2026-10-11-default-local-backends/specs/storage-backends/spec.md new file mode 100644 index 0000000..c4af319 --- /dev/null +++ b/openspec/changes/archive/2026-10-11-default-local-backends/specs/storage-backends/spec.md @@ -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** система отказывается стартовать с явной ошибкой конфигурации diff --git a/openspec/changes/archive/2026-10-11-default-local-backends/tasks.md b/openspec/changes/archive/2026-10-11-default-local-backends/tasks.md new file mode 100644 index 0000000..eeb6ffb --- /dev/null +++ b/openspec/changes/archive/2026-10-11-default-local-backends/tasks.md @@ -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`. diff --git a/openspec/changes/metadata-managed-jwt-secret/.openspec.yaml b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/.openspec.yaml similarity index 100% rename from openspec/changes/metadata-managed-jwt-secret/.openspec.yaml rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/.openspec.yaml diff --git a/openspec/changes/local-fs-storage-backend/design.md b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/design.md similarity index 100% rename from openspec/changes/local-fs-storage-backend/design.md rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/design.md diff --git a/openspec/changes/local-fs-storage-backend/proposal.md b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/proposal.md similarity index 100% rename from openspec/changes/local-fs-storage-backend/proposal.md rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/proposal.md diff --git a/openspec/changes/local-fs-storage-backend/specs/photos/spec.md b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/specs/photos/spec.md similarity index 100% rename from openspec/changes/local-fs-storage-backend/specs/photos/spec.md rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/specs/photos/spec.md diff --git a/openspec/changes/local-fs-storage-backend/specs/share-links/spec.md b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/specs/share-links/spec.md similarity index 100% rename from openspec/changes/local-fs-storage-backend/specs/share-links/spec.md rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/specs/share-links/spec.md diff --git a/openspec/changes/local-fs-storage-backend/specs/storage-backends/spec.md b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/specs/storage-backends/spec.md similarity index 100% rename from openspec/changes/local-fs-storage-backend/specs/storage-backends/spec.md rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/specs/storage-backends/spec.md diff --git a/openspec/changes/local-fs-storage-backend/tasks.md b/openspec/changes/archive/2026-10-11-local-fs-storage-backend/tasks.md similarity index 100% rename from openspec/changes/local-fs-storage-backend/tasks.md rename to openspec/changes/archive/2026-10-11-local-fs-storage-backend/tasks.md diff --git a/openspec/changes/sqlite-metadata-backend/.openspec.yaml b/openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/.openspec.yaml similarity index 100% rename from openspec/changes/sqlite-metadata-backend/.openspec.yaml rename to openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/.openspec.yaml diff --git a/openspec/changes/metadata-managed-jwt-secret/design.md b/openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/design.md similarity index 100% rename from openspec/changes/metadata-managed-jwt-secret/design.md rename to openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/design.md diff --git a/openspec/changes/metadata-managed-jwt-secret/proposal.md b/openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/proposal.md similarity index 100% rename from openspec/changes/metadata-managed-jwt-secret/proposal.md rename to openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/proposal.md diff --git a/openspec/changes/metadata-managed-jwt-secret/specs/app-secrets/spec.md b/openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/specs/app-secrets/spec.md similarity index 100% rename from openspec/changes/metadata-managed-jwt-secret/specs/app-secrets/spec.md rename to openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/specs/app-secrets/spec.md diff --git a/openspec/changes/metadata-managed-jwt-secret/tasks.md b/openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/tasks.md similarity index 100% rename from openspec/changes/metadata-managed-jwt-secret/tasks.md rename to openspec/changes/archive/2026-10-11-metadata-managed-jwt-secret/tasks.md diff --git a/openspec/changes/storage-orphan-reconciliation/.openspec.yaml b/openspec/changes/archive/2026-10-11-sqlite-metadata-backend/.openspec.yaml similarity index 100% rename from openspec/changes/storage-orphan-reconciliation/.openspec.yaml rename to openspec/changes/archive/2026-10-11-sqlite-metadata-backend/.openspec.yaml diff --git a/openspec/changes/sqlite-metadata-backend/design.md b/openspec/changes/archive/2026-10-11-sqlite-metadata-backend/design.md similarity index 100% rename from openspec/changes/sqlite-metadata-backend/design.md rename to openspec/changes/archive/2026-10-11-sqlite-metadata-backend/design.md diff --git a/openspec/changes/sqlite-metadata-backend/proposal.md b/openspec/changes/archive/2026-10-11-sqlite-metadata-backend/proposal.md similarity index 100% rename from openspec/changes/sqlite-metadata-backend/proposal.md rename to openspec/changes/archive/2026-10-11-sqlite-metadata-backend/proposal.md diff --git a/openspec/changes/sqlite-metadata-backend/specs/metadata-backends/spec.md b/openspec/changes/archive/2026-10-11-sqlite-metadata-backend/specs/metadata-backends/spec.md similarity index 100% rename from openspec/changes/sqlite-metadata-backend/specs/metadata-backends/spec.md rename to openspec/changes/archive/2026-10-11-sqlite-metadata-backend/specs/metadata-backends/spec.md diff --git a/openspec/changes/sqlite-metadata-backend/tasks.md b/openspec/changes/archive/2026-10-11-sqlite-metadata-backend/tasks.md similarity index 100% rename from openspec/changes/sqlite-metadata-backend/tasks.md rename to openspec/changes/archive/2026-10-11-sqlite-metadata-backend/tasks.md diff --git a/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/.openspec.yaml b/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/.openspec.yaml new file mode 100644 index 0000000..a369219 --- /dev/null +++ b/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-11 diff --git a/openspec/changes/storage-orphan-reconciliation/design.md b/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/design.md similarity index 100% rename from openspec/changes/storage-orphan-reconciliation/design.md rename to openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/design.md diff --git a/openspec/changes/storage-orphan-reconciliation/proposal.md b/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/proposal.md similarity index 100% rename from openspec/changes/storage-orphan-reconciliation/proposal.md rename to openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/proposal.md diff --git a/openspec/changes/storage-orphan-reconciliation/specs/storage-reconciliation/spec.md b/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/specs/storage-reconciliation/spec.md similarity index 100% rename from openspec/changes/storage-orphan-reconciliation/specs/storage-reconciliation/spec.md rename to openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/specs/storage-reconciliation/spec.md diff --git a/openspec/changes/storage-orphan-reconciliation/tasks.md b/openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/tasks.md similarity index 100% rename from openspec/changes/storage-orphan-reconciliation/tasks.md rename to openspec/changes/archive/2026-10-11-storage-orphan-reconciliation/tasks.md diff --git a/openspec/specs/app-secrets/spec.md b/openspec/specs/app-secrets/spec.md new file mode 100644 index 0000000..14db192 --- /dev/null +++ b/openspec/specs/app-secrets/spec.md @@ -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** он используется независимо от секрета приложения diff --git a/openspec/specs/metadata-backends/spec.md b/openspec/specs/metadata-backends/spec.md new file mode 100644 index 0000000..467b6a9 --- /dev/null +++ b/openspec/specs/metadata-backends/spec.md @@ -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** схема метаданных выбранного бэкенда приведена к актуальному состоянию diff --git a/openspec/specs/photos/spec.md b/openspec/specs/photos/spec.md index ba8aebb..7e63513 100644 --- a/openspec/specs/photos/spec.md +++ b/openspec/specs/photos/spec.md @@ -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 отдаваться вместе с фото и сниматься. diff --git a/openspec/specs/share-links/spec.md b/openspec/specs/share-links/spec.md index 8c5305f..1c046a7 100644 --- a/openspec/specs/share-links/spec.md +++ b/openspec/specs/share-links/spec.md @@ -25,7 +25,7 @@ View-only доступ к альбому по ссылке без авториз #### Scenario: Просмотр по действующей ссылке - **WHEN** аноним открывает ссылку с действующим токеном -- **THEN** система возвращает фото альбома со свежими presigned URL +- **THEN** система возвращает фото альбома со свежими подписанными URL, обслуживаемыми бэкендом #### Scenario: Истёкшая или отозванная ссылка - **WHEN** аноним открывает ссылку с истёкшим или отозванным токеном diff --git a/openspec/specs/storage-backends/spec.md b/openspec/specs/storage-backends/spec.md new file mode 100644 index 0000000..9a70b83 --- /dev/null +++ b/openspec/specs/storage-backends/spec.md @@ -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** состояние здоровья отражает деградацию diff --git a/openspec/specs/storage-reconciliation/spec.md b/openspec/specs/storage-reconciliation/spec.md new file mode 100644 index 0000000..26ec452 --- /dev/null +++ b/openspec/specs/storage-reconciliation/spec.md @@ -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** она использует выбранные бэкенды хранения и метаданных без изменений кода