- 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
83 lines
5.7 KiB
Markdown
83 lines
5.7 KiB
Markdown
# 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
|
||
|
||
- Нужно ли со временем добавить восстановление пропавших блобов или отметку повреждённых
|
||
записей журнала.
|