Files
iu 8638d1bbfc 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
2026-10-11 10:19:44 +03:00

5.7 KiB
Raw Permalink Blame History

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

  • Нужно ли со временем добавить восстановление пропавших блобов или отметку повреждённых записей журнала.