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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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
|
||||
|
||||
- Нужно ли со временем добавить восстановление пропавших блобов или отметку повреждённых
|
||||
записей журнала.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Сборка мусора воркера управляется журналом `objects` и никогда не сканирует хранилище.
|
||||
Поэтому блобы, у которых нет строки в журнале (после сброса БД в обход журнала,
|
||||
исторических миграций или ошибок), накапливаются и не удаляются никогда: воркер их не
|
||||
видит. Нужна ручная сверка «хранилище ∖ журнал».
|
||||
|
||||
## What Changes
|
||||
|
||||
- Добавить в `storage.Backend` перечисление объектов под префиксом (ключ, размер, время
|
||||
изменения); реализовать для S3 (`ListObjectsV2`) и локальной ФС (обход каталога).
|
||||
- Добавить в слой метаданных чтение всех ключей журнала `objects` (Postgres и SQLite).
|
||||
- Добавить maintenance-команду `cmd/reconcile`: по умолчанию — только отчёт (сироты и
|
||||
пропавшие), флаг `--delete` удаляет сироты с учётом минимального возраста
|
||||
(`--min-age`), область ограничена префиксом `objects/`.
|
||||
- Пропавшие ключи (есть в журнале, нет в хранилище) только сообщаются — автоматически не
|
||||
«исправляются».
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `storage-reconciliation`: ручная сверка объектного хранилища с журналом метаданных и удаление осиротевших блобов.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- Нет изменений требований существующих спек: поведение системы для клиента не меняется, добавляется отдельная maintenance-операция. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `backend/internal/storage` (интерфейс + перечисление для S3 и ФС).
|
||||
- `backend/internal/photos` (`Repository.AllStorageKeys`, реализации pg и sqlite).
|
||||
- Новый `backend/cmd/reconcile`.
|
||||
- `README.md`, `project.md`.
|
||||
- Не меняет API и существующие спеки; дополняет `media-objects` ручной сверкой к
|
||||
журнальной сборке мусора.
|
||||
@@ -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** она использует выбранные бэкенды хранения и метаданных без изменений кода
|
||||
@@ -0,0 +1,21 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Перечисление в хранилище
|
||||
|
||||
- [x] 1.1 Добавить `List(ctx, prefix)` (`[]ObjectInfo{Key,Size,ModTime}`) в `storage.Backend` и реализовать в `storage.S3` через `ListObjectsV2` с пагинацией; проверка — тест перечисления под префиксом (под `S3_ENDPOINT`, иначе пропуск).
|
||||
- [x] 1.2 Реализовать `List` в `storage.FS` обходом корня с относительными ключами; проверка — юнит-тест на временном каталоге.
|
||||
|
||||
## 2. Ключи журнала в метаданных
|
||||
|
||||
- [x] 2.1 Добавить `AllStorageKeys` в `photos.Repository` и реализовать в pg- и sqlite-store; проверка — контрактный тест на обоих бэкендах.
|
||||
|
||||
## 3. Команда reconcile
|
||||
|
||||
- [x] 3.1 Реализовать `cmd/reconcile`: инвентаризация обеих сторон, классификация сирот/пропавших, отчёт по умолчанию; проверка — юнит-тесты классификации.
|
||||
- [x] 3.2 Добавить флаги `--delete`, `--min-age`, `--sample` с ограничением области `objects/` и защитой свежих блобов; проверка — тест: молодой блоб не удаляется, старый сирота удаляется, пропавшие не трогаются.
|
||||
- [x] 3.3 Собрать бэкенды через конфиг (`storage.Open` + бандл репозиториев) для postgres/sqlite и s3/fs; проверка — запуск на fs и s3.
|
||||
|
||||
## 4. Документация и интеграция
|
||||
|
||||
- [x] 4.1 Обновить `README.md` и `project.md` с описанием команды и её безопасного использования; проверка — документированный запуск работает.
|
||||
- [x] 4.2 Интеграционная проверка: создать сироту, прогнать отчёт, затем `--delete`, убедиться в удалении и в том, что журнальные записи не затронуты.
|
||||
Reference in New Issue
Block a user