chore(openspec): архивировать add-orphaned-preview-handling и синхронизировать спеки
Новое требование в phone-album-sync о потере/возврате серверной копии у разгруженного превью, требование в android-client о метке потерянного элемента и действии удаления с устройства.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
См. `proposal.md` — Why. Ограничения, важные для подхода:
|
||||
|
||||
- Источник правды о связи «элемент устройства → серверное фото» — локальный индекс
|
||||
`device_assets` (`serverPhotoId`, `contentHash`) плюс метка провенанса в EXIF
|
||||
разгруженного превью.
|
||||
- `SyncCoordinator.reconcile` уже получает живой набор фото целевого альбома
|
||||
(`serverChecksums` → `livePhotoIds`) на каждый проход; отдельный запрос не нужен.
|
||||
- `SyncPlanner.plan` сейчас намеренно пропускает записи `PREVIEW` в цикле загрузки,
|
||||
чтобы превью не уехало как оригинал.
|
||||
- `LocalAssetState` персистится строкой в Room; `GallerySyncPolicy.decide` — `when`
|
||||
по этому enum, поэтому новое значение компилятор заставит обработать везде.
|
||||
- Фото события серверного альбома есть (`AlbumEvent.PhotoDeleted`), но экран
|
||||
альбома устройства на поток серверного альбома не подписан.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Замечать, что у разгруженного превью пропала серверная копия, и отражать это
|
||||
состоянием и меткой.
|
||||
- Дать пользователю убрать такой элемент с устройства.
|
||||
- Восстанавливать связь/заливать заново, когда содержимое возвращается.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Не менять поведение зеркального «серверного представления» альбома и других
|
||||
устройств (см. требование «Отражение серверных изменений на устройстве»).
|
||||
- Не различать «удалили из альбома» и «переместили в другой альбом сервера»
|
||||
(см. решение 3).
|
||||
- Не трогать серверный API, схему БД и серверные миграции.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Отдельное состояние `ORPHANED`, а не флаг
|
||||
|
||||
**Решение:** добавить `LocalAssetState.ORPHANED` — локально превью, серверного фото
|
||||
в целевом альбоме нет.
|
||||
|
||||
**Почему:** состояние уже модель переходов (`LOCAL/UPLOADED/AWAITING_FREE/PREVIEW/
|
||||
SERVER_ONLY`), а `GallerySyncPolicy.decide` — исчерпывающий `when`, так что новый
|
||||
случай нельзя забыть. Флаг пришлось бы сверять во всех тех же точках вручную.
|
||||
|
||||
**Альтернатива:** булев `serverCopyLost` рядом с `state = PREVIEW` — меньше правок
|
||||
enum, но плодит комбинации состояния и флага и не защищён компилятором.
|
||||
|
||||
### 2. Детекция при проверке синхронизации, а не по SSE
|
||||
|
||||
**Решение:** в `reconcile` сверять `index.serverPhotoId` с `livePhotoIds`:
|
||||
отсутствие → `ORPHANED`, присутствие → снять пометку. Тот же код исполняется при
|
||||
открытии альбома, старте и фоне (`SyncCheck`), поэтому покрывает оффлайн и
|
||||
пропущенные события.
|
||||
|
||||
**Альтернатива:** подписать экран альбома устройства на поток серверного альбома и
|
||||
реагировать на `photo.deleted` сразу. Даёт мгновенность, но только при открытом
|
||||
экране и живом SSE, требует новой связи между потоком и `device_assets`, и всё
|
||||
равно не снимает необходимость reconcile-проверки. Можно добавить позже поверх.
|
||||
|
||||
### 3. Не различать «удалили» и «переместили» (v1)
|
||||
|
||||
**Решение:** если фото нет в целевом альбоме — элемент считается потерявшим копию,
|
||||
даже если на сервере то же содержимое лежит в другом альбоме. При возврате
|
||||
оригинала заливка выполняется.
|
||||
|
||||
**Почему:** `serverChecksums` ограничен целевым альбомом; различение требует
|
||||
кросс-альбомного поиска по контрольной сумме на сервере. Для v1 простота и
|
||||
предсказуемость важнее; цена — возможный дубликат, если фото именно переместили, а
|
||||
оригинал вернулся на устройство.
|
||||
|
||||
**Альтернатива:** перед заливкой искать hash по всем альбомам сервера и связывать
|
||||
вместо загрузки. Дороже и усложняет reconcile; оставлено на будущее.
|
||||
|
||||
### 4. Планировщик: `ORPHANED` пропускается как `PREVIEW`
|
||||
|
||||
**Решение:** в `SyncPlanner.plan` (цикл загрузки) пропускать и `ORPHANED`, и
|
||||
`PREVIEW`. `GallerySyncPolicy.decide` для `ORPHANED` возвращает `NONE` во всех
|
||||
режимах, `RestoreManager.restoreTargets` его не берёт.
|
||||
|
||||
**Почему:** превью — не оригинал, его нельзя ни заливать как оригинал, ни
|
||||
восстанавливать с сервера (там пусто). Иначе получаем ложную загрузку или
|
||||
бесконечный `RESTORE`.
|
||||
|
||||
**Альтернатива:** хранить `ORPHANED` как `PREVIEW` с нулевым `serverPhotoId` —
|
||||
теряется `originalHash`/`serverPhotoId`, нужные для возврата и обратной пометки.
|
||||
|
||||
### 5. Возврат оригинала — по исходной контрольной сумме
|
||||
|
||||
**Решение:** повторная заливка срабатывает, когда hash из индекса/метки
|
||||
(`originalHash`) снова присутствует среди локальных ассетов альбома или в другом
|
||||
альбоме устройства (`hashesOutside`). Заливка идёт обычным путём (новое серверное
|
||||
фото), в режиме C элемент снова разгружается.
|
||||
|
||||
**Почему:** hash уже хранится и переживает переустановку (метка EXIF), а
|
||||
`hashesOutside` уже считает содержимое в других альбомах.
|
||||
|
||||
**Альтернатива:** реагировать только на новый файл в том же альбоме — не поймает
|
||||
копию, лежащую в другом альбоме устройства.
|
||||
|
||||
### 6. Удаление с устройства — через системное согласие
|
||||
|
||||
**Решение:** действие «Удалить с устройства» удаляет превью-файл и запись индекса,
|
||||
переиспользуя существующий путь delete-consent (`SyncRepository.deleteConsent`,
|
||||
`MediaStore.createDeleteRequest`), к серверу не обращается.
|
||||
|
||||
**Почему:** превью записано поверх элемента галереи, которым приложение не владеет,
|
||||
поэтому тихое удаление невозможно; это тот же механизм, что и у удаления альбома.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Дубликат при перемещении фото на сервере] → принято в v1 (решение 3); при
|
||||
необходимости добавляется кросс-альбомная проверка hash перед заливкой.
|
||||
- [Возврат оригинала не ловится, если hash не посчитался] → `contentHash` может быть
|
||||
null; тогда пометка и возврат опираются на провенанс-метку, а элемент остаётся
|
||||
`ORPHANED` до ручного удаления.
|
||||
- [Ошибка детекции при частичной загрузке списка сервера] → `serverChecksums`
|
||||
следует за пагинацией; если запрос неполный, возможна ложная пометка. Митигация:
|
||||
пометка снимается при следующем успешном проходе, файл не удаляется автоматически.
|
||||
- [Забытые `when` по enum] → компилятор ловит исчерпывающие `when`; остальные
|
||||
сравнения со строкой состояния — ручные, их покрывают тесты.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Схема БД и серверный API не меняются. Значение `ORPHANED` появляется только для
|
||||
целевого набора условий; ранее помеченных как `PREVIEW` элементов это не касается,
|
||||
пока серверное фото живо. Откат — удаление значения из enum и обработчиков; данные
|
||||
в Room (строки состояний) не мигрируют. Существующие разгруженные превью без
|
||||
серверной копии получат `ORPHANED` при первом же проходе синхронизации.
|
||||
Reference in New Issue
Block a user