Files
iu 3a44844ff9 chore(openspec): архивировать fix-album-sync-churn-and-degradation и синхронизировать спеки
В спеку phone-album-sync добавлены требования о роли локального файла,
подтверждении новой версии до удаления устаревшей и разгрузке без
незавершённой загрузки; уточнены синхронизация изменений, провенанс и
возврат оригиналов.
2026-10-20 20:35:52 +03:00

7.4 KiB

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 при кэшировании хешей (сверка контрольной суммы закрывает риск).