# Proposal ## Why Слой хранения S3 сейчас знает о сущности «альбом»: ключи объектов имеют вид `albums/{album_id}/original/{photo_id}.{ext}` (`photos/service.go:72,114,115`). Это протекание модели метаданных в физический слой: перенос фото оставляет ключ в старом namespace, копия делит объекты под префиксом исходного альбома, а неиспользуемый `DeletePrefix` (`storage/storage.go:107`) создаёт латентный риск удаления живых объектов. Нужно развести слои: S3 хранит непрозрачные объекты, а сущности и их жизненный цикл живут в БД. ## What Changes - Ввести таблицу `objects` — физические блобы (ключ, mime, размер, габариты, checksum, состояние) и `photo_objects` — связка photo↔object с ролью (`original|thumb|preview`). - **BREAKING**: удалить `storage_key`, `thumb_key`, `preview_key` из `photos`; ключи фото выводятся через джойн к объекту-оригиналу. Миграция разрушительная (в базе только дев-данные). - Сделать ключи S3 album-free: `objects/{id}/{role}.{ext}`, где `id` — случайный идентификатор объекта. - Ввести явный GC через ledger-состояния объектов `pending | ready | deleting`: физический объект удаляется только когда на него не осталось ни одной связки. - Перенести генерацию вариантов в фоновый воркер: таблица `variant_jobs` (ровно-один-раз, ретраи с backoff), воркер также удаляет осиротевшие объекты и досасывает зависшие `pending`. SSE `photo.created` публикует воркер. - Удалить `DeletePrefix` из клиента хранения — entity-namespace больше нет. ## Capabilities ### New Capabilities - `media-objects`: физическое объектное хранилище с непрозрачными ключами, журнал объектов и их жизненный цикл, сборка мусора по отсутствию ссылок, привязка объектов к фото по ролям. ### Modified Capabilities - `photos`: уточняется семантика удаления (корзина не удаляет объекты хранилища) и разделение объектов между фото при переносе и копировании. ## Impact - Backend: `internal/photos/{service,store}.go`, `internal/storage/storage.go`, `internal/albums/*` (косвенно, через ключи обложки), wiring фонового воркера, `migrations/` (новая схема + удаление колонок). - Данные: разрушительная миграция `photos` (dev-only), новые таблицы. - Внешний JSON API не меняется: `thumb_url/preview_url/original_url` и `size/width/height/mime_type` по-прежнему отдаются, но выводятся из объектов. - Документация: `project.md` §10 (структура в S3).