Новое требование в phone-album-sync о потере/возврате серверной копии у разгруженного превью, требование в android-client о метке потерянного элемента и действии удаления с устройства.
131 lines
9.8 KiB
Markdown
131 lines
9.8 KiB
Markdown
# 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` при первом же проходе синхронизации.
|