Дельта-спеки перенесены в основные: phone-album-sync (+8 требований, 6 изменено, 1 удалено — «Удаление только собственных загрузок»), android-client. Change перемещён в openspec/changes/archive/2026-10-16-phone-album-two-way-sync.
480 lines
34 KiB
Markdown
480 lines
34 KiB
Markdown
# phone-album-sync Specification
|
||
|
||
## Purpose
|
||
|
||
Двусторонняя синхронизация альбома устройства с существующим серверным альбомом
|
||
(миррор состава): режимы A/B/C, материализация серверных фото на устройстве,
|
||
отслеживание изменений, идентичность и дедупликация, удаления в обе стороны,
|
||
разгрузка места и провенанс разгруженных превью.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Режимы синхронизации альбома устройства
|
||
|
||
Альбом устройства SHALL иметь выбираемый режим синхронизации: выключена (A — только
|
||
локально), копии оригиналов на сервере (B), перенос оригиналов на сервер (C). Пока
|
||
режим A, приложение SHALL показывать альбом локально и SHALL NOT обращаться к
|
||
серверу по этому альбому.
|
||
|
||
#### Scenario: Выключенная синхронизация
|
||
|
||
- **WHEN** у альбома устройства выбран режим A
|
||
- **THEN** приложение показывает альбом из локальных файлов и не отправляет запросы на сервер по этому альбому
|
||
|
||
#### Scenario: Копии оригиналов
|
||
|
||
- **WHEN** у альбома устройства выбран режим B
|
||
- **THEN** оригиналы остаются на устройстве, а их копии загружаются в целевой серверный альбом
|
||
|
||
#### Scenario: Перенос оригиналов
|
||
|
||
- **WHEN** у альбома устройства выбран режим C
|
||
- **THEN** оригиналы загружаются на сервер, а на устройстве вместо них остаются превью
|
||
|
||
### Requirement: Выбор целевого серверного альбома
|
||
|
||
При включении синхронизации пользователь SHALL выбрать серверный альбом назначения —
|
||
существующий или созданный на месте, с именем, предзаполненным именем альбома
|
||
устройства. Несколько альбомов устройства SHALL иметь возможность указывать на один
|
||
серверный альбом.
|
||
|
||
#### Scenario: Выбор цели
|
||
|
||
- **WHEN** пользователь включает синхронизацию альбома устройства
|
||
- **THEN** приложение предлагает выбрать существующий серверный альбом или создать новый с предзаполненным именем и сохраняет выбор
|
||
|
||
#### Scenario: Несколько источников в одну цель
|
||
|
||
- **WHEN** два альбома устройства указывают на один серверный альбом
|
||
- **THEN** оба синхронизируются в него, а связь каждого отражается отдельно
|
||
|
||
### Requirement: Отслеживание изменений на устройстве
|
||
|
||
Приложение SHALL отслеживать изменения содержимого синхронизируемого альбома
|
||
устройства — добавление, удаление, переименование, перенос между альбомами и
|
||
изменение содержимого — и SHALL хранить локальный индекс, позволяющий обнаружить
|
||
удаления, произошедшие вне приложения.
|
||
|
||
#### Scenario: Добавление
|
||
|
||
- **WHEN** в альбоме устройства появляется новое медиа
|
||
- **THEN** приложение фиксирует его как новое и ставит в очередь синхронизации
|
||
|
||
#### Scenario: Удаление вне приложения
|
||
|
||
- **WHEN** медиа удалено из альбома устройства другим приложением или системной галереей
|
||
- **THEN** приложение обнаруживает отсутствие по локальному индексу
|
||
|
||
#### Scenario: Переименование и перенос
|
||
|
||
- **WHEN** медиа переименовано или перемещено между альбомами устройства
|
||
- **THEN** приложение фиксирует изменение имени или принадлежности альбому
|
||
|
||
### Requirement: Синхронизация изменений с сервером
|
||
|
||
Для каждого отслеженного изменения приложение SHALL применить его к целевому
|
||
серверному альбому: загрузить новое содержимое, переименовать и перенести через
|
||
API, а изменение содержимого SHALL трактовать как новую загрузку с удалением
|
||
устаревшей собственной загрузки.
|
||
|
||
#### Scenario: Новое содержимое
|
||
|
||
- **WHEN** в альбоме устройства появилось медиа, которого нет на сервере
|
||
- **THEN** приложение загружает его в целевой серверный альбом
|
||
|
||
#### Scenario: Переименование доходит до сервера
|
||
|
||
- **WHEN** медиа переименовано на устройстве и уже синхронизировано
|
||
- **THEN** приложение переименовывает соответствующее серверное фото
|
||
|
||
#### Scenario: Изменение содержимого
|
||
|
||
- **WHEN** содержимое медиа изменено на устройстве
|
||
- **THEN** приложение загружает новую версию, а устаревшую собственную загрузку удаляет
|
||
|
||
### Requirement: Идентичность по контрольной сумме
|
||
|
||
Приложение SHALL сопоставлять локальный ассет с серверным фото по контрольной сумме
|
||
содержимого и SHALL NOT повторно загружать содержимое, уже присутствующее на
|
||
сервере.
|
||
|
||
#### Scenario: Повторная синхронизация
|
||
|
||
- **WHEN** синхронизация запускается повторно без изменений содержимого
|
||
- **THEN** приложение не создаёт новых загрузок
|
||
|
||
#### Scenario: Уже загруженное содержимое
|
||
|
||
- **WHEN** содержимое ассета уже присутствует на сервере (в том числе загружено ранее)
|
||
- **THEN** приложение связывает ассет с существующим серверным фото без загрузки
|
||
|
||
### Requirement: Отражение серверных изменений на устройстве
|
||
|
||
Приложение SHALL приводить локальное представление целевого серверного альбома в
|
||
соответствие с его составом: новое серверное фото SHALL материализоваться на устройстве
|
||
(оригинал в режиме копий, превью в режиме переноса), переименования, теги и подписи SHALL
|
||
отражаться, а серверное удаление SHALL убирать локальный элемент. Удаление снятого на
|
||
устройстве оригинала SHALL учитывать права сервера и системное согласие.
|
||
|
||
#### Scenario: Новое с другого устройства
|
||
|
||
- **WHEN** другое устройство добавляет фото в целевой серверный альбом
|
||
- **THEN** приложение материализует его в альбоме устройства — оригинал в режиме копий, превью в режиме переноса
|
||
|
||
#### Scenario: Удалено на сервере
|
||
|
||
- **WHEN** серверное фото удалено на сервере
|
||
- **THEN** приложение убирает соответствующий локальный элемент, а для снятого на устройстве оригинала запрашивается системное согласие на удаление
|
||
|
||
### Requirement: Разгрузка оригиналов в режиме переноса
|
||
|
||
В режиме C после того, как оригинал элемента оказывается на сервере (загружен этим
|
||
клиентом или скачан при двусторонней синхронизации), приложение SHALL автоматически
|
||
заменять оригинал на устройстве превью, без отдельного действия на экране; видео SHALL
|
||
заменяться постером-кадром. При включении режима C приложение SHALL запрашивать широкий
|
||
доступ к файлам, чтобы замена выполнялась без системного диалога согласия.
|
||
|
||
#### Scenario: Разгрузка фото
|
||
|
||
- **WHEN** оригинал фото успешно загружен
|
||
- **THEN** файл в галерее автоматически заменяется превью с сохранением даты съёмки и места в альбоме
|
||
|
||
#### Scenario: Разгрузка скачанного оригинала
|
||
|
||
- **WHEN** альбом переключён в режим переноса, а оригинал элемента был скачан с сервера
|
||
- **THEN** оригинал автоматически заменяется превью
|
||
|
||
#### Scenario: Разгрузка видео
|
||
|
||
- **WHEN** оригинал видео успешно загружен
|
||
- **THEN** видео в галерее автоматически заменяется статичным постером-кадром, а элемент в приложении остаётся видео с оригиналом на сервере
|
||
|
||
#### Scenario: Доступ к файлам для разгрузки
|
||
|
||
- **WHEN** пользователь включает режим переноса
|
||
- **THEN** приложение запрашивает широкий доступ к файлам, и разгрузка выполняется без системного диалога согласия
|
||
|
||
### Requirement: Провенанс разгруженных превью
|
||
|
||
Разгруженные превью, постеры видео и скачанные с сервера элементы SHALL содержать
|
||
метаданные провенанса, включая признак происхождения (загружено клиентом или скачано с
|
||
сервера), достаточные для восстановления связи с серверным альбомом и принадлежности
|
||
загрузок после переустановки приложения.
|
||
|
||
#### Scenario: Восстановление после переустановки
|
||
|
||
- **WHEN** приложение переустановлено или его данные очищены, а альбом устройства содержит разгруженные превью
|
||
- **THEN** приложение восстанавливает связь с серверным альбомом и принадлежность своих загрузок из метаданных превью
|
||
|
||
#### Scenario: Метка скачанного элемента
|
||
|
||
- **WHEN** метка провенанса восстанавливается у элемента, скачанного с сервера
|
||
- **THEN** элемент восстанавливается как скачанный, а не как собственная загрузка клиента
|
||
|
||
#### Scenario: Метка утрачена
|
||
|
||
- **WHEN** метаданные провенанса недоступны (например, файл перезаписан сторонним редактором)
|
||
- **THEN** элемент считается неопознанным, не удаляется и не загружается повторно
|
||
|
||
### Requirement: Возврат оригиналов и отвязка
|
||
|
||
При выходе из режима C приложение SHALL предложить вернуть оригиналы на устройство
|
||
и SHALL предупредить, если пользователь отказывается. Отвязка альбома и выключение
|
||
синхронизации SHALL NOT удалять фото с сервера, а скачанные с сервера файлы SHALL
|
||
оставаться в галерее как обычные. Выключение синхронизации SHALL быть явным и
|
||
устойчивым: восстановление происхождения SHALL NOT возобновлять связь, пока
|
||
пользователь не включит синхронизацию снова.
|
||
|
||
#### Scenario: Возврат оригиналов
|
||
|
||
- **WHEN** пользователь выходит из режима C и соглашается вернуть оригиналы
|
||
- **THEN** приложение возвращает оригиналы на устройство вместо превью
|
||
|
||
#### Scenario: Отказ от возврата
|
||
|
||
- **WHEN** пользователь выходит из режима C и отказывается возвращать оригиналы
|
||
- **THEN** приложение предупреждает, что на устройстве останутся только превью
|
||
|
||
#### Scenario: Отвязка
|
||
|
||
- **WHEN** пользователь отвязывает альбом устройства от серверного
|
||
- **THEN** фото на сервере сохраняются, а скачанные файлы остаются в галерее как обычные
|
||
|
||
#### Scenario: Выключение синхронизации
|
||
|
||
- **WHEN** пользователь выключает синхронизацию альбома устройства
|
||
- **THEN** связь с серверным альбомом снимается, а фото на сервере сохраняются
|
||
|
||
#### Scenario: Выключение не отменяется восстановлением
|
||
|
||
- **WHEN** пользователь выключил синхронизацию (или отвязал альбом), и затем выполняется проверка с восстановлением происхождения по меткам превью
|
||
- **THEN** связь не восстанавливается, а альбом остаётся локальным с выбранным режимом выключено
|
||
|
||
#### Scenario: Повторное включение
|
||
|
||
- **WHEN** пользователь снова включает синхронизацию после выключения
|
||
- **THEN** связь создаётся заново, а восстановление происхождения снова действует по обычным правилам
|
||
|
||
### Requirement: Автоматическая фоновая синхронизация
|
||
|
||
Синхронизация SHALL выполняться автоматически в обе стороны: при открытом приложении —
|
||
по событиям изменения, в фоне — периодически, с учётом ограничений сети, выбранных
|
||
пользователем. В режиме C разгрузка оригиналов SHALL выполняться автоматически после
|
||
того, как копия оказалась на сервере.
|
||
|
||
#### Scenario: Фоновая синхронизация
|
||
|
||
- **WHEN** приложение не открыто, а в альбоме устройства появились изменения
|
||
- **THEN** синхронизация выполняется в фоне при ближайшей возможности
|
||
|
||
#### Scenario: Фоновая загрузка серверных фото
|
||
|
||
- **WHEN** приложение не открыто, а в целевом серверном альбоме появилось новое фото
|
||
- **THEN** приложение материализует его на устройстве при ближайшей возможности
|
||
|
||
#### Scenario: Ограничение сети
|
||
|
||
- **WHEN** включено ограничение «только Wi-Fi», а доступна только сотовая сеть
|
||
- **THEN** синхронизация, включая скачивание серверных фото, не начинается до появления Wi-Fi
|
||
|
||
#### Scenario: Автоматическая разгрузка
|
||
|
||
- **WHEN** оригиналы загружены в режиме C
|
||
- **THEN** замена оригиналов превью выполняется автоматически, без отдельного подтверждения
|
||
|
||
### Requirement: Локальные превью
|
||
|
||
Приложение SHALL генерировать превью на устройстве и хранить их локально; при
|
||
просмотре элемента без локального оригинала приложение SHALL подгружать оригинал с
|
||
сервера.
|
||
|
||
#### Scenario: Сетка из превью
|
||
|
||
- **WHEN** открыт альбом устройства
|
||
- **THEN** сетка показывает локальные превью
|
||
|
||
#### Scenario: Просмотр без локального оригинала
|
||
|
||
- **WHEN** пользователь открывает элемент, чей оригинал разгружен на сервер
|
||
- **THEN** приложение подгружает оригинал с сервера для полноэкранного просмотра
|
||
|
||
### Requirement: Обнаружение потери серверной копии превью
|
||
|
||
При проверке синхронизации приложение SHALL сопоставлять серверное фото каждого
|
||
разгруженного превью с фото целевого альбома и при его отсутствии SHALL помечать
|
||
элемент как потерявший серверную копию, сохраняя превью и провенанс.
|
||
|
||
#### Scenario: Копии больше нет
|
||
|
||
- **WHEN** серверное фото разгруженного превью отсутствует в целевом альбоме
|
||
- **THEN** приложение помечает элемент как потерявший серверную копию
|
||
|
||
#### Scenario: Превью не трогается
|
||
|
||
- **WHEN** элемент помечен как потерявший серверную копию
|
||
- **THEN** превью-файл на устройстве сохраняется и не загружается повторно как оригинал
|
||
|
||
### Requirement: Восстановление связи при возврате серверного фото
|
||
|
||
При проверке синхронизации приложение SHALL снимать пометку с элемента, если его
|
||
серверное фото снова присутствует в целевом альбоме.
|
||
|
||
#### Scenario: Фото вернулось на сервер
|
||
|
||
- **WHEN** серверное фото помеченного элемента снова присутствует в целевом альбоме
|
||
- **THEN** элемент возвращается к обычному разгруженному состоянию с серверной копией
|
||
|
||
### Requirement: Удаление потерянного превью с устройства
|
||
|
||
Приложение SHALL позволять удалить потерявшее серверную копию превью с устройства;
|
||
удаление SHALL удалять превью-файл и запись локального индекса и SHALL NOT
|
||
обращаться к серверу, поскольку копии там уже нет.
|
||
|
||
#### Scenario: Удаление с устройства
|
||
|
||
- **WHEN** пользователь удаляет потерявшее серверную копию превью с устройства
|
||
- **THEN** превью-файл и запись локального индекса удаляются, а сервер не затрагивается
|
||
|
||
#### Scenario: Согласие системы
|
||
|
||
- **WHEN** превью-файл не принадлежит приложению
|
||
- **THEN** удаление выполняется после системного согласия на удаление
|
||
|
||
### Requirement: Повторная заливка вернувшегося оригинала
|
||
|
||
Если содержимое с исходной контрольной суммой потерявшего копию элемента снова
|
||
присутствует на устройстве, приложение SHALL загрузить его на сервер как новую
|
||
загрузку этого элемента.
|
||
|
||
#### Scenario: Вернулся в тот же альбом
|
||
|
||
- **WHEN** оригинал с исходной контрольной суммой снова появился в альбоме устройства
|
||
- **THEN** приложение загружает его в целевой серверный альбом
|
||
|
||
#### Scenario: Вернулся в другой альбом
|
||
|
||
- **WHEN** содержимое с исходной контрольной суммой присутствует в другом альбоме устройства
|
||
- **THEN** приложение загружает его в целевой серверный альбом
|
||
|
||
#### Scenario: Режим переноса
|
||
|
||
- **WHEN** загрузка вернувшегося оригинала завершена в режиме переноса
|
||
- **THEN** элемент снова проходит обычную разгрузку в превью
|
||
|
||
### Requirement: Ход и устойчивость восстановления оригиналов
|
||
|
||
При возврате оригиналов на устройство приложение SHALL выполнять операцию до конца
|
||
независимо от того, остаётся ли открыт экран альбома устройства, и SHALL сообщать ход:
|
||
сколько элементов восстановлено из скольких намечено.
|
||
|
||
#### Scenario: Уход с экрана
|
||
|
||
- **WHEN** восстановление оригиналов запущено и пользователь покидает экран альбома устройства
|
||
- **THEN** восстановление продолжается и завершается
|
||
|
||
#### Scenario: Ход операции
|
||
|
||
- **WHEN** восстановление оригиналов идёт
|
||
- **THEN** приложение знает и сообщает, сколько элементов уже восстановлено из намеченных
|
||
|
||
#### Scenario: Нечего восстанавливать
|
||
|
||
- **WHEN** среди элементов альбома нет превью с серверной копией
|
||
- **THEN** операция не запускается и ход не показывается
|
||
|
||
### Requirement: Материализация серверных фото на устройстве
|
||
|
||
При появлении фото в целевом серверном альбоме приложение SHALL материализовать его на
|
||
устройстве, записав файл в галерею (MediaStore) бакета альбома устройства: в режиме копий —
|
||
оригинал, в режиме переноса — превью, для видео — постер. Файл SHALL содержать метку
|
||
происхождения и дату съёмки серверного фото.
|
||
|
||
#### Scenario: Материализация в режиме копий
|
||
|
||
- **WHEN** в целевом серверном альбоме появилось новое фото, а альбом устройства в режиме копий
|
||
- **THEN** приложение скачивает оригинал и создаёт файл в бакете альбома устройства
|
||
|
||
#### Scenario: Материализация в режиме переноса
|
||
|
||
- **WHEN** в целевом серверном альбоме появилось новое фото, а альбом устройства в режиме переноса
|
||
- **THEN** приложение скачивает превью и создаёт файл в бакете альбома устройства
|
||
|
||
#### Scenario: Материализация видео
|
||
|
||
- **WHEN** в целевом серверном альбоме появилось видео
|
||
- **THEN** приложение создаёт в бакете постер-кадр, а оригинал остаётся на сервере
|
||
|
||
#### Scenario: Дата съёмки
|
||
|
||
- **WHEN** приложение материализует серверное фото
|
||
- **THEN** файл получает дату съёмки из серверных метаданных фото
|
||
|
||
### Requirement: Признак происхождения элемента
|
||
|
||
Каждый элемент альбома устройства SHALL иметь явный признак происхождения: загружено этим
|
||
клиентом или скачано с сервера. Политика удалений, разгрузка и восстановление SHALL
|
||
опираться на признак, а не на наличие серверной ссылки.
|
||
|
||
#### Scenario: Скачанный элемент не считается своей загрузкой
|
||
|
||
- **WHEN** элемент скачан с сервера при двусторонней синхронизации
|
||
- **THEN** он помечается как скачанный, а не как загруженный этим клиентом
|
||
|
||
### Requirement: Двустороннее удаление элементов
|
||
|
||
Удаление элемента в альбоме устройства SHALL удалять соответствующее фото на сервере
|
||
(мягко, в корзину), а серверное удаление SHALL убирать локальный элемент. Для
|
||
материализованного (серверного) элемента намерением считается только явное удаление в
|
||
приложении: пропавшая локальная копия SHALL NOT удалять серверное фото, а SHALL
|
||
материализоваться заново. Операция SHALL подчиняться правам сервера, а удаление файла,
|
||
снятого на устройстве, SHALL требовать системного согласия.
|
||
|
||
#### Scenario: Удаление на устройстве
|
||
|
||
- **WHEN** пользователь удаляет элемент синхронизированного альбома устройства
|
||
- **THEN** приложение помещает соответствующее серверное фото в корзину
|
||
|
||
#### Scenario: Удаление на сервере
|
||
|
||
- **WHEN** серверное фото помещено в корзину
|
||
- **THEN** приложение убирает соответствующий локальный элемент
|
||
|
||
#### Scenario: Пропала локальная копия серверного фото
|
||
|
||
- **WHEN** локальная копия материализованного серверного фото исчезла (очистка галереи, перемещение файла)
|
||
- **THEN** приложение материализует фото заново и SHALL NOT удалять его на сервере
|
||
|
||
#### Scenario: Явное удаление в приложении
|
||
|
||
- **WHEN** пользователь удаляет материализованное серверное фото из приложения
|
||
- **THEN** приложение помечает намерение и удаляет фото на сервере
|
||
|
||
#### Scenario: Нет прав на серверное удаление
|
||
|
||
- **WHEN** пользователь удаляет элемент, а роль на сервере не позволяет удалить это фото
|
||
- **THEN** серверное удаление отклоняется, приложение не считает операцию успешной и сообщает об этом
|
||
|
||
### Requirement: Журнал удалений и защита от эха
|
||
|
||
Локально удалённый элемент SHALL помечаться в журнале удалений, чтобы приложение SHALL NOT
|
||
материализовать его повторно до подтверждения серверного удаления. Повторные и эхо-события
|
||
удаления SHALL быть идемпотентны.
|
||
|
||
#### Scenario: Удаление ещё не подтверждено сервером
|
||
|
||
- **WHEN** элемент удалён на устройстве, а серверное удаление ещё не выполнено
|
||
- **THEN** при следующем проходе приложение не материализует этот элемент заново
|
||
|
||
#### Scenario: Эхо серверного события
|
||
|
||
- **WHEN** серверное событие удаления относится к уже удалённому локальному элементу
|
||
- **THEN** приложение ничего не меняет повторно
|
||
|
||
### Requirement: Восстановление элемента из корзины
|
||
|
||
Если серверное фото, удалённое с устройства, восстановлено из корзины сервера, приложение
|
||
SHALL снова материализовать соответствующий элемент на устройстве.
|
||
|
||
#### Scenario: Фото вернулось из корзины
|
||
|
||
- **WHEN** пользователь восстанавливает серверное фото из корзины
|
||
- **THEN** приложение материализует соответствующий элемент в альбоме устройства
|
||
|
||
### Requirement: Первый проход и ограничение скачивания
|
||
|
||
При включении синхронизации приложение SHALL предложить выбрать: скачать всё содержимое
|
||
целевого серверного альбома или только новое с этого момента. Скачивание SHALL подчиняться
|
||
ограничению «только Wi-Fi»; при ожидании подходящей сети материализация не начинается.
|
||
|
||
#### Scenario: Только новое
|
||
|
||
- **WHEN** пользователь выбирает «только новое» при включении синхронизации
|
||
- **THEN** уже существующее содержимое серверного альбома не скачивается, а материализуются только последующие появления
|
||
|
||
#### Scenario: Скачать всё
|
||
|
||
- **WHEN** пользователь выбирает «скачать всё» при включении синхронизации
|
||
- **THEN** приложение материализует весь текущий состав серверного альбома
|
||
|
||
#### Scenario: Ожидание WiFi
|
||
|
||
- **WHEN** включено ограничение «только Wi-Fi», а доступна только сотовая сеть
|
||
- **THEN** скачивание не начинается до появления Wi-Fi
|
||
|
||
### Requirement: Материализация при нескольких источниках
|
||
|
||
Когда несколько альбомов устройства указывают на один серверный альбом, приложение SHALL
|
||
материализовать серверное фото ровно в один из них, чтобы не создавать дубликат в галерее.
|
||
Выбор целевого бакета SHALL сохраняться. Содержимое, уже присутствующее на устройстве,
|
||
SHALL NOT дублироваться.
|
||
|
||
#### Scenario: Несколько источников в одну цель
|
||
|
||
- **WHEN** два альбома устройства синхронизируются с одним серверным альбомом, и в нём появляется фото
|
||
- **THEN** приложение создаёт файл ровно в одном из бакетов
|
||
|
||
#### Scenario: Содержимое уже на устройстве
|
||
|
||
- **WHEN** появившееся серверное фото по содержимому уже присутствует на устройстве
|
||
- **THEN** приложение связывает элемент с серверным фото, не создавая второй файл
|