feat(metadata): SQLite backend, storage reconciliation and local defaults

- pluggable per-domain Repository interfaces; PostgreSQL and SQLite impls
- SQLite on modernc.org/sqlite (WAL, busy_timeout, foreign_keys, immediate tx)
- separate migrations/sqlite and db.MigrateSQLite; store.NewPostgres/NewSQLite
- cmd/reconcile compares object storage with the journal (report, then --delete)
- storage.Backend.List and photos.Repository.AllStorageKeys
- DB_BACKEND/SQLITE_PATH and STORAGE_BACKEND/FS_ROOT selectable; local defaults
  (sqlite ./galo.db, fs ./data)
- Makefile reconcile/reconcile-delete; docs and .env.example updated
This commit is contained in:
iu
2026-10-11 10:08:31 +03:00
parent ec8d907565
commit 176a8d8ced
69 changed files with 4174 additions and 95 deletions
@@ -0,0 +1,65 @@
# Spec Delta
## Purpose
Ручная сверка объектного хранилища с журналом метаданных: инвентаризация обеих сторон,
отчёт о расхождениях и безопасное удаление осиротевших блобов.
## ADDED 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** она использует выбранные бэкенды хранения и метаданных без изменений кода