# Design ## Context См. `proposal.md` — Why. Текущее состояние, определяющее подход: - `storage.Backend` умеет Put/Delete/Get/URL/ServeObject/Ping/EnsureBucket/Bucket, но не умеет перечислять объекты. - Сборка мусора (`photos.Worker`) управляется журналом `objects`: `ClaimGarbage` выбирает строки без привязок и удаляет блоб. Блобы без строки в журнале воркеру невидимы. - Ключи объектов — `objects//`, слой метаданных хранит их в `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 - Нужно ли со временем добавить восстановление пропавших блобов или отметку повреждённых записей журнала.