5.9 KiB
5.9 KiB
Design
Context
См. proposal.md для мотивации. Технические факты, формирующие подход:
- Генерация вариантов —
photos/pipeline.go:buildVariants:thumb = Fit(200,200),preview = Fit(1200,1200), WebP q82. Оба кропа вписывают по длинной стороне. - Метаданные имеют две реализации (Postgres и SQLite) с общей контрактной проверкой
internal/repo/contract; SQL дублируется (photos/store.go,photos/sqlite_store.go). - Роли заданы жёстко:
photo_objects.role CHECK (role IN ('original','thumb','preview'))(migrations/000008,migrations/sqlite/000001). - Варианты генерирует фоновый воркер из очереди
variant_jobs; лишние объекты удаляются сборщиком мусора по отсутствию ссылок (photos/worker.go). - Фронтенд-сетка берёт только
thumb_url(PhotoGrid.svelte:78); обложки — толькоcover_thumb_url(albums/store.goджойнитrole='thumb').
Goals / Non-Goals
Goals:
- Сетка фото и обзор альбомов показывают изображение без апскейла при любом выбранном размере.
- Единое поведение на Postgres и SQLite; контрактные тесты покрывают новую роль.
- Бэкфилл существующих фото без ручных операций.
Non-Goals:
- Динамоческое ресайзирование на лету / image-proxy.
- Иные форматы (AVIF) или согласование форматов.
- Изменение
preview(просмотрщик) и оригинала. - Замер точной плотности DPR-пикселей и «идеальный» выбор варианта — допускается запас.
Decisions
- Набор вариантов.
thumb = Fill(200,200)(квадрат),medium = Fill(800,800)(квадрат),preview = Fit(1200,1200). Причина: плитка сетки квадратная (aspect-square object-cover), квадратный кроп исключает лишний апскейл короткой стороны; просмотрщик не квадратный, поэтомуpreviewостаётся вписанным. Альтернатива — оставитьthumbвписанным: отклонена, короткая сторона выходит < 200 и мылит уже мелкие плитки. - Роль
mediumвphoto_objects. Роли маппятся 1:1 на поля API и на привязки; расширяем ограничение значениемmedium. Альтернатива — отдельная таблица вариантов: избыточна для одного размера. srcset+sizes. Ширины200w/800w/1200w;sizesберётся из фактической ширины сетки (bind:clientWidthна<ul>, делённой на число колонок). Альтернатива — оценка по100vw: неточна из-за рейла и правой панели, ведёт к перекачке 1200.- Бэкфилл через очередь. Миграция делает
INSERT ... SELECT id FROM photosвvariant_jobs; воркер пересоздаёт роли и перепривязывает, старые объекты уходят в GC по refcount. Альтернатива — воркер, пропускающий уже готовые роли: требует версионирования вариантов; альтернатива — отдельныйcmd/backfill: лишний код. Выбран самый простой путь. - Обложка. Джойн medium-варианта обложки с откатом на
thumb, если medium ещё не готов; отдельного поля «preview обложки» не вводим.
Risks / Trade-offs
- [Перегенерация
previewвпустую при бэкфилле] → воркер работает батчами и параллельно (настраивается); лишние объекты удаляются сборщиком; разовая стоимость. - [Рост хранилища на один объект 800px на фото] → WebP q82 скромного размера; приемлемо.
- [Неточный
sizes] → запас в пользу резкости; при отсутствииmedium_urlвариант просто не попадает вsrcset, сетка падает на доступные (thumb/preview). - [SQLite не умеет
ALTER CHECK] → миграция пересоздаётphoto_objects; на больших таблицах копирование строк. Приемлемо для текущего масштаба, выполняется в окне обслуживания.
Migration Plan
- Применить миграцию: расширить
CHECKроли, налитьvariant_jobsдля существующих фото. - Выкатить код: воркер генерирует
mediumи квадратныйthumb; API отдаёт новые поля. - Воркер разбирает очередь; сетка показывает
srcsetс откатом на доступные варианты. - Откат: сначала код (API перестаёт отдавать medium, воркер не генерирует); затем down-миграция
восстанавливает
CHECK, предварительно удалив привязкиmedium.
Open Questions
Нет.