# 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 трактоваться как новая загрузка, а устаревшая собственная загрузка SHALL удаляться после подтверждения новой версии и SHALL NOT удаляться из-за смены локальной роли файла (оригинал ↔ превью). #### Scenario: Новое содержимое - **WHEN** в альбоме устройства появилось медиа, которого нет на сервере - **THEN** приложение загружает его в целевой серверный альбом #### Scenario: Переименование доходит до сервера - **WHEN** медиа переименовано на устройстве и уже синхронизировано - **THEN** приложение переименовывает соответствующее серверное фото #### Scenario: Изменение содержимого - **WHEN** содержимое медиа изменено на устройстве - **THEN** приложение загружает новую версию и после её подтверждения удаляет устаревшую собственную загрузку ### Requirement: Подтверждение новой версии до удаления устаревшей Устаревшая собственная загрузка элемента SHALL удаляться только после подтверждения новой версии: сообщённая сервером контрольная сумма принятого объекта SHALL совпадать с отправленной. При расхождении приложение SHALL отклонить загрузку, удалив принятую копию, и SHALL NOT считать элемент загруженным. Если сервер контрольную сумму не сообщил, загрузка SHALL считаться состоявшейся, но устаревшая копия SHALL NOT удаляться, а замена SHALL повториться при следующей синхронизации. #### Scenario: Новая версия подтверждена - **WHEN** новая версия загружена, а сообщённая сервером контрольная сумма совпала с отправленной - **THEN** приложение удаляет устаревшую собственную загрузку (мягко, в корзину) #### 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 NOT заменять оригинал превью, пока для этого элемента есть незавершённая загрузка (в очереди или выполняющаяся), чтобы загрузка не отправила превью вместо оригинала. #### Scenario: Загрузка в очереди - **WHEN** для элемента есть задание загрузки в очереди или выполняющееся - **THEN** оригинал на устройстве не заменяется превью #### Scenario: Загрузка подтверждена - **WHEN** загрузка элемента подтверждена сервером - **THEN** приложение выполняет обычную разгрузку в режиме переноса ### Requirement: Провенанс разгруженных превью Разгруженные превью, постеры видео и скачанные с сервера элементы SHALL содержать метаданные провенанса, включая признак происхождения (загружено клиентом или скачано с сервера), достаточные для восстановления связи с серверным альбомом и принадлежности загрузок после переустановки приложения. Метка SHALL также служить признаком того, что локальный файл является созданным приложением превью: элемент с такой меткой SHALL NOT считаться содержащим оригинал, даже если состояние локального индекса утверждает обратное. #### Scenario: Восстановление после переустановки - **WHEN** приложение переустановлено или его данные очищены, а альбом устройства содержит разгруженные превью - **THEN** приложение восстанавливает связь с серверным альбомом и принадлежность своих загрузок из метаданных превью #### Scenario: Метка скачанного элемента - **WHEN** метка провенанса восстанавливается у элемента, скачанного с сервера - **THEN** элемент восстанавливается как скачанный, а не как собственная загрузка клиента #### Scenario: Метка утрачена - **WHEN** метаданные провенанса недоступны (например, файл перезаписан сторонним редактором) - **THEN** элемент считается неопознанным, не удаляется и не загружается повторно #### Scenario: Метка против состояния индекса - **WHEN** файл несёт метку созданного приложением превью, а состояние индекса указывает оригинал - **THEN** элемент считается разгруженным, а не новым оригиналом ### Requirement: Роль локального файла не является содержимым Приложение SHALL отличать превью, созданное им самим при разгрузке (в том числе по метке провенанса), от новой версии оригинала: разгруженный элемент SHALL NOT загружаться повторно и SHALL NOT вызывать удаление серверной копии, а расхождение состояния в локальном индексе SHALL выправляться. Это SHALL относиться и к постеру, которым в режиме переноса заменяется видео. #### Scenario: Разгруженный элемент не создаёт новую версию - **WHEN** серверная копия элемента есть в целевом альбоме, а на устройстве лежит созданное приложением превью, в том числе если состояние локального индекса ошибочно указывает оригинал - **THEN** приложение не ставит загрузку и не удаляет серверную копию #### Scenario: Состояние выправляется - **WHEN** файл на устройстве является созданным приложением превью, а индекс считает его оригиналом - **THEN** приложение помечает элемент как разгруженный и последующие проходы не создают загрузок #### Scenario: Постер разгруженного видео - **WHEN** видео разгружено (заменено постером под новым элементом MediaStore), а его метка указывает на исходное видео - **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** связь создаётся заново, а восстановление происхождения снова действует по обычным правилам #### 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** приложение связывает элемент с серверным фото, не создавая второй файл