Files
ds-galo/openspec/changes/storage-orphan-reconciliation/design.md
T
iu 176a8d8ced 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
2026-10-11 10:08:31 +03:00

83 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design
## Context
См. `proposal.md` — Why. Текущее состояние, определяющее подход:
- `storage.Backend` умеет Put/Delete/Get/URL/ServeObject/Ping/EnsureBucket/Bucket, но не
умеет перечислять объекты.
- Сборка мусора (`photos.Worker`) управляется журналом `objects`: `ClaimGarbage` выбирает
строки без привязок и удаляет блоб. Блобы без строки в журнале воркеру невидимы.
- Ключи объектов — `objects/<object_id>/<role><ext>`, слой метаданных хранит их в
`objects.storage_key`.
- Есть два бэкенда хранения (`s3`, `fs`) и два бэкенда метаданных (`postgres`, `sqlite`)
за интерфейсами `storage.Backend` и доменными `Repository`.
## Goals / Non-Goals
**Goals:**
- Ручная сверка хранилища и журнала: инвентаризация обеих сторон, отчёт, безопасное удаление.
- Работа с любой комбинацией бэкендов хранения и метаданных.
- Безопасное поведение по умолчанию: ничего не удаляется без явного флага, свежие блобы
защищены.
**Non-Goals:**
- Автоматическая периодическая реконсиляция в воркере.
- Восстановление пропавших блобов (в журнале есть, в хранилище нет).
- Миграция блобов между бэкендами.
- Сверка префиксов, отличных от объектов.
## Decisions
### D1. Перечисление в `storage.Backend`
Добавляем `List(ctx context.Context, prefix string) ([]ObjectInfo, error)` где
`ObjectInfo{Key string; Size int64; ModTime time.Time}`.
- S3: `ListObjectsV2` с пагинацией по `ContinuationToken`, `Prefix`.
- ФС: обход корня (`filepath.WalkDir`), ключ — путь относительно корня, размер/время из
`os.Stat`.
- Альтернатива: не трогать интерфейс и добавить отдельный тип для реконсиляции — отвергнуто,
перечисление логично принадлежит бэкенду хранения.
### D2. Ключи журнала в метаданных
Добавляем `AllStorageKeys(ctx) ([]string, error)` в `photos.Repository`; реализации:
`SELECT storage_key FROM objects` (pg и sqlite). Контрактный набор покрывает метод.
### D3. Команда `cmd/reconcile`
Отдельная maintenance-команда, читает конфиг, открывает выбранные бэкенды
(`storage.Open` + бандл репозиториев), строит множества и печатает отчёт.
Флаги: `--delete` (bool), `--min-age` (duration, по умолчанию `1h`), `--sample` (число
примеров в отчёте). Область — фиксированный префикс `objects/`.
### D4. Классификация расхождений
- **Сироты** = ключи хранилища под `objects/`, которых нет в журнале; кандидаты на удаление.
- **Пропавшие** = ключи журнала, которых нет в хранилище; только отчёт.
- Сироты моложе `--min-age` не удаляются (защита от гонки с идущей загрузкой: блоб уже
записан, строка ещё не создана).
### D5. Безопасность
По умолчанию — только отчёт и нулевое воздействие. Удаление только по `--delete`, только
под `objects/`, только старше `--min-age`. Пропавшие никогда не «исправляются».
## Risks / Trade-offs
- [Гонка с загрузкой] → `--min-age` пропускает молодые блобы; рекомендуем запускать в
спокойный период.
- [Большие бакеты: память/время] → пагинация S3 и потоковый обход ФС; в отчёте лишь
примеры, не полный список.
- [Несогласованность формата ключей между бэкендами] → оба бэкенда отдают ключи
относительно корня/перфикса без ведущего слэша; контракт фиксируется тестами.
- [Удаление используемого блоба] → сверка выполняется вручную и в спокойный период; журнал
— источник истины, поэтому «сирота» по определению не имеет строки.
## Migration Plan
1. Добавить `List` в интерфейс — S3 и ФС реализуют его (компилятор требует оба).
2. Добавить `AllStorageKeys` в `photos.Repository` — обе реализации.
3. Добавить `cmd/reconcile` (аддитивно, ничего не меняет в работающем сервисе).
4. Данные не мигрируются; команда запускается вручную по необходимости.
## Open Questions
- Нужно ли со временем добавить восстановление пропавших блобов или отметку повреждённых
записей журнала.