Files
ds-galo/openspec/changes/fix-album-sync-churn-and-degradation/proposal.md
T
iu 46d54b4b12 chore(openspec): предложить fix-album-sync-churn-and-degradation и уточнить спеку phone-album-sync
Изменение описывает три дефекта двусторонней синхронизации: повторную
загрузку собственного превью, удаление серверной копии до подтверждения
замены и повторную разгрузку только что восстановленного оригинала.
2026-10-20 20:32:20 +03:00

71 lines
7.4 KiB
Markdown

# Proposal
## Why
Режим переноса (C) может залить на сервер **превью вместо оригинала** и тут же удалить
устаревшую серверную копию, то есть молча уничтожить оригинал. Причина — планировщик не
отличает содержимое от роли локального файла: когда состояние элемента в локальном индексе
расходится с фактической ролью (в индексе «оригинал», на диске превью), смена роли читается
как «содержимое изменилось» и запускается цикл «удалил → залил → удалил».
Реальный инцидент: альбом устройства U2, режим переноса, серверный альбом `N2`. Из 20 фото
пять перезаливались по кругу (20:03 → 20:08 → 20:42 → 21:24), а два потеряли оригиналы —
`IMG_20261004_163151.jpg` (3 437 694 Б, 3072x4096 → 259 282 Б, 1200x1600) и
`IMG_20261009_153136.jpg` (2 226 043 Б → 192 651 Б). Лечиться это будет только сейчас:
у первого уцелевший оригинал лежит в корзине сервера, у второго — в корзине и в альбоме
«Первый». Побочно тот же цикл оставляет на телефоне элементы, которых на сервере уже нет:
в приложении альбом `N2` показывает 26 плиток вместо 20, шесть из них пустые.
## What Changes
- **Роль не является содержимым.** Приложение SHALL отличать созданное им превью (в том числе
по метке провенанса) от нового оригинала: превью никогда не загружается как новая версия и
не влечёт удаления серверной копии, а расхождение состояния в индексе выправляется.
Настоящее изменение содержимого, как и раньше, синхронизируется новой версией.
- **Удаление только после подтверждения.** Устаревшая собственная загрузка удаляется только
после того, как новая версия подтверждена сервером: сообщённая им контрольная сумма
принятого объекта совпала с отправленной. При расхождении загрузка отклоняется вместе с
принятой копией, прежняя серверная копия остаётся в альбоме, а замена повторяется при
следующей синхронизации. Если сервер контрольную сумму не сообщил (например, старая версия
инстанса), загрузка остаётся состоявшейся, но прежняя копия не удаляется.
- **Контрольная сумма в ответе на загрузку.** Сервер SHALL отдавать контрольную сумму
оригинала в данных созданного фото — как требует существующее требование способности
`photos`; сегодня ответ на загрузку её не содержит (дефект сервера, а не изменение API).
- **Разгрузка не гоняется с загрузкой.** Оригинал не заменяется превью, пока по этому
элементу есть незавершённая загрузка.
- **Возврат оригинала устойчив.** После возврата оригиналов на устройство элемент не
разгружается превью повторно, пока альбом снова не переведён в режим переноса.
- **Разовое восстановление двух пострадавших фото** из корзины сервера (и альбома «Первый»
для второго) с удалением деградировавших копий.
- **BREAKING**: нет. Успешные сценарии (загрузка нового, разгрузка, повторная синхронизация,
настоящее изменение содержимого) сохраняют поведение; меняются только защитные условия.
## Capabilities
### New Capabilities
_Нет — поведение относится к существующей способности синхронизации альбома устройства._
### Modified Capabilities
- `phone-album-sync`: различение роли и содержимого (метка превью против нового оригинала),
разгрузка только без незавершённой загрузки, удаление устаревшей загрузки только после
подтверждения новой версии, устойчивый возврат оригиналов.
## Impact
- **Код (Android):** `data/sync/SyncPlan` (различение роли и содержимого, отложенная замена),
`data/sync/SyncCoordinator` (выправление состояния, разгрузка без незавершённых загрузок,
постановка замены), `data/sync/LocalAssetProvider` (метка превью рядом с хешем),
`data/upload/UploadWorker` (сверка принятой контрольной суммы, удаление устаревшей копии
после подтверждения, без разгрузки вне координатора), `data/sync/RestoreManager` (состояние
после возврата оригинала), `data/db` (поле «заменяемое фото» в задании загрузки).
- **Схема Room:** версия 8 → 9, `ALTER TABLE upload_jobs ADD COLUMN replacesPhotoId TEXT`.
- **API и бэкенд:** не затрагиваются — используются существующие `DELETE /api/photos/{id}`
(мягко, в корзину), `POST /api/photos/{id}/restore` и `original_checksum` в ответе загрузки.
- **Данные инстанса:** разовое восстановление двух оригиналов и удаление деградировавших
копий (операция, а не постоянное поведение).
- **Не затрагивается:** серверная модель версий/идентичности, дедупликация вложения, чистка
кэша альбома на телефоне (пустые плитки удалённых фото — отдельная задача), доверие к
метаданным MediaStore при кэшировании хешей (сверка контрольной суммы закрывает риск).