diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/.openspec.yaml b/openspec/changes/archive/2026-10-13-add-phone-album-sync/.openspec.yaml new file mode 100644 index 0000000..5fcc9ec --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-13 diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/design.md b/openspec/changes/archive/2026-10-13-add-phone-album-sync/design.md new file mode 100644 index 0000000..feab641 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/design.md @@ -0,0 +1,206 @@ +# Design + +## Context + +Мотивацию см. в `proposal.md`. Здесь — текущее состояние и ограничения, задающие подход. + +- Android-клиент (`android/`) построен вокруг серверных альбомов: `AlbumsScreen` → + `AlbumScreen` (`GET /api/albums/:id/photos`), SSE, viewer. Слой данных + (`com.galo.android.data.*`) уже содержит `UploadRepository`/`UploadWorker` + (по файлу на multipart POST, повторы, «только Wi-Fi», переживает перезапуск), + Room-кэш альбомов/фото, DataStore-настройки, `HeicConverter`. Понятий + «альбом устройства», «синхронизация», «устройство» нет. +- Сервер (`backend/`) хранит `objects.checksum` (sha256 оригинала), но **не отдаёт + его в API**. Фото неизменяемы по контенту: есть `PATCH /photos/:id` + (переименование), `POST /photo-batch/move|copy`, удаление/восстановление. Есть + `POST /api/albums` (создание). Загрузка — один файл на запрос, fail-fast, + ограничение размера тела. +- Платформа Android накладывает рамки: изменение/удаление медиа, созданного не + приложением, требует согласия пользователя (`MediaStore.createWriteRequest` / + `createDeleteRequest` / `RecoverableSecurityException`); фоновая работа — + WorkManager (периодические задачи не чаще ~15 мин); широкий доступ к медиа — + `READ_MEDIA_IMAGES`/`READ_MEDIA_VIDEO`, который ограничивается политикой Google + Play. Текущее приложение живёт на scoped Photo Picker без широких прав. +- `AndroidManifest.xml` задаёт `allowBackup="false"`, поэтому авто-бэкап данных + приложения недоступен. + +## Goals / Non-Goals + +**Goals:** + +- Единый конвейер для трёх режимов (A/B/C) поверх существующей инфраструктуры + загрузки, с идемпотентностью и корректным возобновлением после сбоя/перезапуска. +- Надёжная идентичность «локальный ассет ↔ серверное фото», устойчивая к + переустановке в режиме C. +- Безопасная политика удалений при общей цели (n:1) и общем серверном альбоме. + +**Non-Goals:** + +- Серверная модель устройств/сессий синхронизации; изменения схемы БД. +- Транскодирование видео на устройстве, редактирование изображений. +- Разрешение конфликтов владения между пользователями; версионирование контента. +- Работа с облачными «плейсхолдерами» (Google Photos) как с локальными ассетами + сверх того, что отдаёт MediaStore. + +## Decisions + +### 1. Единица синхронизации и цель: альбом устройства → существующий серверный альбом + +Единица — bucket MediaStore (телефонный альбом). Пользователь выбирает целевой +**существующий** серверный альбом; допускается n:1 (несколько телефонных в один +серверный). Связь хранится в локальном индексе и дублируется в EXIF-метке +разгруженных превью. + +*Альтернатива:* серверный альбом-зеркало 1:1 (автосоздание). Отклонено: лишает +пользователя контроля над тем, куда попадают фото, и хуже переиспользует уже +наполненные альбомы. *Альтернатива:* смешанный список без явной связи. +Отклонено: теряется понимание, куда льётся альбом. + +### 2. Идентичность и дедуп: content-hash ↔ checksum объекта + +Клиент хэширует **байты, которые реально уходят** (после HEIC-конвертации), и +сопоставляет с `checksum` объекта на сервере. Сервер начинает отдавать `checksum` +в данных фото (аддитивная правка, без миграции). + +*Альтернатива:* локальная таблица `asset ↔ photo_id` — не переживает +переустановку. *Альтернатива:* полноценный серверный провенанс (device+asset) — +надёжнее, но заметно расширяет объём backend/API. Отклонено в пользу минимальной +правки; провенанс после переустановки закрывается EXIF-меткой (решение 5). + +### 3. Политика изменений и удалений + +- Телефон → сервер: добавление, удаление, переименование, перенос, правка контента + (как «новый upload + удалить старый, если он наш»). +- Удаление на сервере допускается **только для «наших» загрузок**: фото, которое + залил этот клиент, и чей hash исчез из всех локальных альбомов, льющихся в этот + серверный. Чужие фото (веб, другие устройства, другие участники) не трогаются. +- Сервер → телефон: отражаем метаданные и новые элементы; **оригиналы на телефоне + не трогаем** (удаление на сервере убирает только локальное представление). +- Отвязка и выключение синхронизации **никогда не удаляют** фото с сервера. + +*Альтернатива:* полное зеркало — разрушительно при общей цели. *Альтернатива:* +только добавление — не покрывает «синхронизировать изменения». + +### 4. Три режима как состояния одного конвейера + +- **A** — индекс + генерация превью на телефоне; сервера не касается. +- **B** — A + загрузка копий; оригинал остаётся. +- **C** — B + разгрузка: после успешной загрузки оригинал на телефоне заменяется + превью. + +Разгрузка в C реализуется записью превью **обратно в MediaStore** тем же `_ID`: +`openFileDescriptor(uri, "rwt")` и запись JPEG с сохранением `DATE_TAKEN`. Видео +заменяется JPEG-постером: это смена типа, поэтому выполняется как «удалить +видео-запись + вставить постер-картинку в ту же папку». Разгрузка выполняется +**автоматически** после успешной загрузки, без отдельного действия на экране; чтобы +замена не сопровождалась системным диалогом согласия, при включении режима C +приложение запрашивает широкий доступ к файлам (`MANAGE_EXTERNAL_STORAGE`, «доступ +ко всем файлам»). Загрузка и дедуп идут тихо. + +*Альтернатива:* удалять оригинал без замены — теряется присутствие в системной +галерее. *Альтернатива:* транскодировать видео — дорого/нестабильно. Отклонено: +постер в v1. + +### 5. Провенанс, переживающий переустановку: EXIF-метка на разгруженных превью + +Файлы, которые мы пишем сами (превью в C и постеры видео), содержат EXIF-метку +(например, `UserComment`/XMP) с `server_album`, `server_photo`, `original_hash`, +`device`, `mode`. После переустановки обход галереи читает метку и восстанавливает +и связь альбомов, и провенанс наших загрузок. В A/B оригиналы не помечаются — там +дедуп идёт через content-hash. + +*Альтернатива:* расширить серверное поле-источник — расширяет объём API. +*Альтернатива:* Android Auto Backup метаданных — не гарантирован, ломается при +переносе на другое устройство. Метка выбрана: провенанс путешествует с файлом. + +### 6. Движок синхронизации и триггеры + +- Локальный индекс содержимого каждого синхронизируемого альбома — в Room + (снапшот + провенанс). Внешние удаления не имеют «надгробий», поэтому diff — + единственный способ их заметить. +- Обнаружение изменений: `MediaStore.getGeneration()` / `ContentObserver` в + foreground; периодический WorkManager в фоне с ограничениями «только Wi-Fi» / + «только на зарядке» (переиспользуя `SettingsRepository`). +- Очередь и выполнение — на `UploadRepository`/`UploadWorker`: по файлу, повторы с + backoff, продолжение после перезапуска. +- Серверные изменения — SSE, пока приложение открыто, плюс догон `?since=`. +- Объём изменений v1 — расширенный (add/delete/rename/move/edit). + +*Альтернатива:* только ручной запуск — противоречит «отслеживать изменения». +*Альтернатива:* авто лишь в foreground — не догоняет фон. + +### 7. Интерфейс + +Один смешанный список альбомов (устройство + сервер). Связанная пара показывается +**одной карточкой** альбома устройства с указателем связи (`→ <серверный>`) и меткой +режима; связанный серверный альбом из списка скрывается, чтобы синхронизируемый +альбом читался как один объект. Экран альбома устройства даёт настройки +синхронизации по свайпу вправо — **только выбор режима**. Разгруженные элементы в +сетке помечены значком облака; статусы элементов: локально / загружено / только +превью (разгружено). + +### 8. Параметры превью + +Крупное превью — длинная сторона ~1600 px, JPEG q80; мелкое — для сетки. Превью +генерируются на устройстве и лежат в приватном кэше приложения; в C крупное превью +дополнительно пишется в галерею. Дата съёмки (EXIF `DateTimeOriginal`/`DATE_TAKEN`) +сохраняется при замене. + +### 9. Права + +`READ_MEDIA_IMAGES` и `READ_MEDIA_VIDEO` (плюс legacy `READ_EXTERNAL_STORAGE` для +API до 33). Запись в MediaStore — через системные запросы согласия. +`ACCESS_MEDIA_LOCATION` — не требуется для метки (геолокации в ней нет). + +### 10. Предпосылка: `.gitignore` + +Правило `data/` в `.gitignore` игнорирует `android/app/src/main/java/com/galo/android/data/` +(весь слой данных, отсутствует и в git-истории). Устранить **до** реализации, иначе +новые файлы синхронизации не будут отслеживаться git. Отдельная правка, не часть +поведения синхронизации. + +## Risks / Trade-offs + +- [Замена файлов в MediaStore требует особого доступа на Android 11+] → при + включении режима C запрашивается широкий доступ к файлам, чтобы разгрузка шла + автоматически и без системного диалога; без доступа разгрузка пропускается. +- [EXIF-метка теряется, если сторонний редактор перезапишет превью без EXIF] → + деградация безопасная: элемент становится «неопознанным» (без дублей и без + удаления). +- [Общая цель (n:1) и наполненный серверный альбом] → удаляем только «наши» + загрузки; чужие источники не затрагиваются. +- [Content-hash не переживает переустановку в C сам по себе] → закрывается + EXIF-меткой; в B переустановка безопасна (оригиналы на месте, дедуп сработает + после повторного выбора цели). +- [Высокий расход на хэширование видео] → `sha256` считается лениво, только для + сравнения/загрузки; дешёвая первичная детекция изменения — по `DATE_MODIFIED`/`SIZE`. +- [Замена видео = delete+insert: место освобождается с задержкой (корзина ОС на 30 + дней), коротко нужно место под постер] → сбойный элемент пропускается, проход не + падает; ограничение известно и принято. +- [Широкие `READ_MEDIA_*` и политика Google Play] → риск только при публикации в + Play; для self-hosted/сайдлоада не блокирует. Отмечено как внешнее ограничение. +- [Разгрузка C требует свободного места под возврат оригиналов при откате] → + предлагать возврат с проверкой свободного места. +- [Новые файлы синхронизации не попадут в git из-за `.gitignore` `data/`] → + устранить правило до начала реализации (см. Decision 10). + +## Migration Plan + +1. Предпосылка: поправить `.gitignore` (убрать `data/`), убедиться, что слой данных + попал в git. +2. Сервер: отдать `checksum` в моделях/запросах/ответах фото. Аддитивно, без + миграции БД; откат — убрать поле. +3. Клиент, этапами: (a) источник MediaStore + смешанный список + режим A; + (b) загрузка/дедуп + режим B; (c) разгрузка + EXIF-метка + режим C; (d) возврат + оригиналов и переходы режимов. +4. Room-миграция под индекс синхронизации и провенанс. +5. Откат: перевод альбомов в режим A не разрушителен (серверное остаётся); + серверное изменение аддитивно. + +## Open Questions + +- Конкретное поле для метки (`UserComment` vs XMP) и формат значения — решается при + реализации, не влияет на поведение. +- Частота периодической фоновой синхронизации и правила пробуждения — тюнинг. +- Нужен ли `ACCESS_MEDIA_LOCATION` для неотредактированной геолокации при локальной + генерации превью — можно уточнить позже. diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/proposal.md b/openspec/changes/archive/2026-10-13-add-phone-album-sync/proposal.md new file mode 100644 index 0000000..31120b8 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/proposal.md @@ -0,0 +1,78 @@ +# Proposal + +## Why + +Android-клиент работает только с серверными альбомами и ручной загрузкой через +системный Photo Picker: он не видит альбомы самого телефона, не следит за их +изменениями и не освобождает место. Основной мобильный сценарий — «снимаю на +телефон и не думаю о переносе» — не покрыт. Нужна синхронизация альбомов +устройства с серверным альбомом в трёх режимах, с отслеживанием изменений. + +## What Changes + +- Смешанный список альбомов: альбомы устройства (MediaStore bucket'ы) и серверные + альбомы в одном списке; связанная пара «телефонный → серверный» показывается + одной карточкой устройства с указателем связи и меткой режима, а связанный + серверный альбом из списка скрывается. +- Экран альбома устройства с настройками по свайпу вправо: только выбор режима + синхронизации; целевой серверный альбом выбирается (или создаётся) при включении, + допускается n:1. +- Три режима синхронизации альбома: + - **A — выключена**: оригиналы на телефоне, превью генерируются и хранятся на + телефоне; сервера не касается. + - **B — копии оригиналов**: оригиналы остаются на телефоне, копии загружаются на + сервер. + - **C — перенос оригиналов**: оригиналы уходят на сервер, на телефоне вместо них + остаются превью; замена файла в галерее выполняется автоматически (широкий + доступ к файлам запрашивается при включении режима, без системного диалога); + видео заменяется постером. +- Отслеживание и синхронизация изменений (добавление, удаление, переименование, + перенос между альбомами, правка контента как «новый + удалить старый») + автоматически в фоне (`MediaStore.getGeneration`/`ContentObserver` + + периодический WorkManager) и при открытом приложении (SSE + `?since=`). +- Идентичность и дедупликация по content-hash (sha256) относительно контрольной + суммы объекта на сервере; удаление на сервере — только для «наших» загрузок. +- Провенанс, переживающий переустановку: в EXIF разгруженных в C превью пишется + метка (серверный альбом/фото, hash оригинала), по которой связь и провенанс + восстанавливаются. +- Возврат оригиналов из C («вернуть оригиналы»); отвязка и выключение + синхронизации никогда не удаляют фотографии с сервера. +- Сервер начинает отдавать контрольную сумму оригинала в данных фото (малая + правка API, без миграции БД — `checksum` уже хранится). +- **BREAKING**: нет (добавляемое поле ответа; существующие клиенты не ломаются). + +## Capabilities + +### New Capabilities + +- `phone-album-sync`: синхронизация альбома устройства с существующим серверным + альбомом — режимы A/B/C, отслеживание изменений, идентичность и дедуп по + content-hash, политика удалений, разгрузка места и провенанс. + +### Modified Capabilities + +- `android-client`: смешанный список альбомов устройства и сервера, экран и + настройки синхронизации телефонного альбома, широкий доступ к медиа + (`READ_MEDIA_*`) и фоновая работа синхронизации. +- `photos`: данные фото включают контрольную сумму оригинала, чтобы клиент мог + сопоставлять локальный ассет с серверным фото и не загружать дубли. + +## Impact + +- `android/`: новый источник MediaStore (bucket'ы и ассеты), движок синхронизации + и очередь на существующем `UploadRepository`/`UploadWorker`, локальный индекс и + провенанс в Room, кэш превью, EXIF-метка, чтение/запись превью (широкий доступ + к файлам `MANAGE_EXTERNAL_STORAGE`), правки навигации (`GaloNavHost`), манифеста + (права) и `AppContainer`. +- `backend/`: отдать `checksum` в моделях/запросах/ответах фото. Миграции БД не + требуются. +- Зависимости клиента: MediaStore, ExifInterface, MediaMetadataRetriever, + WorkManager, Room, Coil — большинство уже подключено. +- Платформенные ограничения: согласие ОС на изменение/удаление чужого медиа; + фоновая работа ограничена WorkManager; Google Play ограничивает `READ_MEDIA_*`. +- **Предпосылка (побочно, вне объёма этой правки)**: `.gitignore` содержит правило + `data/`, из-за которого весь Kotlin-слой `android/.../com/galo/android/data/` + не отслеживается git и отсутствует в git-истории. До реализации это нужно + устранить, иначе новые файлы синхронизации тоже не попадут в коммит. +- Вне объёма: iOS; синхронизация на уровне сервера (устройства/сессии на + бэкенде); многопользовательские конфликты владения; редактирование изображений. diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/android-client/spec.md b/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/android-client/spec.md new file mode 100644 index 0000000..8d621f0 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/android-client/spec.md @@ -0,0 +1,71 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Список альбомов + +Приложение SHALL показывать доступные пользователю альбомы одним смешанным списком: +серверные альбомы, получаемые из API, а при отсутствии сети — из локального кэша, и +альбомы устройства. Элементы SHALL различать источник. Связанная пара «альбом +устройства → серверный альбом» SHALL отображаться одной карточкой альбома устройства +с указателем связи и меткой режима; связанный серверный альбом SHALL NOT показываться +в списке отдельной записью. + +#### Scenario: Онлайн + +- **WHEN** авторизованный пользователь открывает приложение +- **THEN** отображается список альбомов с обложками или плейсхолдером + +#### Scenario: Офлайн из кэша + +- **WHEN** сеть недоступна, но кэш существует +- **THEN** список альбомов отображается из кэша с признаком офлайн-режима + +#### Scenario: Альбомы устройства + +- **WHEN** у приложения есть доступ к медиа устройства +- **THEN** в списке появляются альбомы устройства наряду с серверными и помечены источником + +#### Scenario: Связанная пара + +- **WHEN** альбом устройства синхронизируется с серверным альбомом +- **THEN** в списке показывается одна карточка альбома устройства с указателем связи и меткой режима, а связанный серверный альбом отдельной записью не показывается + +## ADDED Requirements + +### Requirement: Экран альбома устройства и настройки синхронизации + +Приложение SHALL открывать альбом устройства с сеткой медиа и SHALL предоставлять +настройки синхронизации этого альбома по свайпу вправо. Настройки SHALL содержать +только выбор режима синхронизации. + +#### Scenario: Открытие альбома устройства + +- **WHEN** пользователь выбирает альбом устройства в списке +- **THEN** открывается сетка медиа этого альбома + +#### Scenario: Настройки по свайпу + +- **WHEN** пользователь свайпает вправо в открытом альбоме устройства +- **THEN** открываются настройки синхронизации с выбором режима и только им + +#### Scenario: Метка перенесённого элемента + +- **WHEN** оригинал элемента разгружен на сервер в режиме переноса +- **THEN** в сетке на элементе показывается значок облака + +### Requirement: Доступ к альбомам устройства + +Приложение SHALL запрашивать доступ к медиа устройства, нужный для перечисления +альбомов и чтения файлов, и SHALL работать без него, не показывая альбомы +устройства. + +#### Scenario: Доступ предоставлен + +- **WHEN** пользователь предоставляет доступ к медиа устройства +- **THEN** приложение перечисляет альбомы устройства и их содержимое + +#### Scenario: Доступ не предоставлен + +- **WHEN** доступ к медиа устройства не предоставлен +- **THEN** приложение продолжает работать с серверными альбомами и не показывает альбомы устройства diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/phone-album-sync/spec.md b/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/phone-album-sync/spec.md new file mode 100644 index 0000000..3d5d331 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/phone-album-sync/spec.md @@ -0,0 +1,242 @@ +# Spec Delta + +## Purpose + +Синхронизация альбома устройства с существующим серверным альбомом: режимы A/B/C, +отслеживание изменений, идентичность и дедупликация, политика удалений, разгрузка +места и провенанс разгруженных превью. + +## ADDED 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 сохранять остальные фото целевого альбома, +в том числе загруженные из других источников. + +#### Scenario: Собственная загрузка + +- **WHEN** удалено содержимое, которое загрузил этот клиент, и его контрольная сумма отсутствует во всех локальных альбомах, льющихся в этот серверный +- **THEN** приложение удаляет соответствующее серверное фото + +#### Scenario: Чужие фото + +- **WHEN** в целевом серверном альбоме есть фото, загруженные из других источников +- **THEN** приложение не удаляет их + +### Requirement: Отражение серверных изменений на устройстве + +Приложение SHALL отражать изменения целевого серверного альбома — новые фото, +переименования, теги, подписи и удаления — в локальном представлении, SHALL NOT +изменять или удалять оригиналы на устройстве. + +#### Scenario: Новое с другого устройства + +- **WHEN** другое устройство добавляет фото в целевой серверный альбом +- **THEN** приложение показывает его в серверном представлении альбома + +#### Scenario: Удалено на сервере + +- **WHEN** серверное фото удалено на сервере +- **THEN** приложение убирает его из локального представления, не трогая оригинал на устройстве + +### Requirement: Разгрузка оригиналов в режиме переноса + +В режиме C после успешной загрузки приложение SHALL автоматически заменять оригинал +на устройстве превью, без отдельного действия на экране; видео SHALL заменяться +постером-кадром. При включении режима C приложение SHALL запрашивать широкий доступ +к файлам, чтобы замена выполнялась без системного диалога согласия. + +#### Scenario: Разгрузка фото + +- **WHEN** оригинал фото успешно загружен +- **THEN** файл в галерее автоматически заменяется превью с сохранением даты съёмки и места в альбоме + +#### Scenario: Разгрузка видео + +- **WHEN** оригинал видео успешно загружен +- **THEN** видео в галерее автоматически заменяется статичным постером-кадром, а элемент в приложении остаётся видео с оригиналом на сервере + +#### Scenario: Доступ к файлам для разгрузки + +- **WHEN** пользователь включает режим переноса +- **THEN** приложение запрашивает широкий доступ к файлам, и разгрузка выполняется без системного диалога согласия + +### Requirement: Провенанс разгруженных превью + +Разгруженные превью и постеры видео SHALL содержать метаданные провенанса, +достаточные для восстановления связи с серверным альбомом и принадлежности +загрузок после переустановки приложения. + +#### Scenario: Восстановление после переустановки + +- **WHEN** приложение переустановлено или его данные очищены, а альбом устройства содержит разгруженные превью +- **THEN** приложение восстанавливает связь с серверным альбомом и принадлежность своих загрузок из метаданных превью + +#### Scenario: Метка утрачена + +- **WHEN** метаданные провенанса недоступны (например, файл перезаписан сторонним редактором) +- **THEN** элемент считается неопознанным, не удаляется и не загружается повторно + +### Requirement: Возврат оригиналов и отвязка + +При выходе из режима C приложение SHALL предложить вернуть оригиналы на устройство +и SHALL предупредить, если пользователь отказывается. Отвязка альбома и выключение +синхронизации SHALL NOT удалять фото с сервера. + +#### Scenario: Возврат оригиналов + +- **WHEN** пользователь выходит из режима C и соглашается вернуть оригиналы +- **THEN** приложение возвращает оригиналы на устройство вместо превью + +#### Scenario: Отказ от возврата + +- **WHEN** пользователь выходит из режима C и отказывается возвращать оригиналы +- **THEN** приложение предупреждает, что на устройстве останутся только превью + +#### Scenario: Отвязка + +- **WHEN** пользователь отвязывает альбом устройства от серверного +- **THEN** фото на сервере сохраняются + +#### Scenario: Выключение синхронизации + +- **WHEN** пользователь выключает синхронизацию альбома устройства +- **THEN** связь с серверным альбомом снимается, а фото на сервере сохраняются + +### Requirement: Автоматическая фоновая синхронизация + +Синхронизация SHALL выполняться автоматически: при открытом приложении — по +событиям изменения, в фоне — периодически, с учётом ограничений сети, выбранных +пользователем. В режиме C разгрузка оригиналов SHALL выполняться автоматически +после успешной загрузки. + +#### Scenario: Фоновая синхронизация + +- **WHEN** приложение не открыто, а в альбоме устройства появились изменения +- **THEN** синхронизация выполняется в фоне при ближайшей возможности + +#### Scenario: Ограничение сети + +- **WHEN** включено ограничение «только Wi-Fi», а доступна только сотовая сеть +- **THEN** синхронизация не начинается до появления Wi-Fi + +#### Scenario: Автоматическая разгрузка + +- **WHEN** оригиналы загружены в режиме C +- **THEN** замена оригиналов превью выполняется автоматически, без отдельного подтверждения + +### Requirement: Локальные превью + +Приложение SHALL генерировать превью на устройстве и хранить их локально; при +просмотре элемента без локального оригинала приложение SHALL подгружать оригинал с +сервера. + +#### Scenario: Сетка из превью + +- **WHEN** открыт альбом устройства +- **THEN** сетка показывает локальные превью + +#### Scenario: Просмотр без локального оригинала + +- **WHEN** пользователь открывает элемент, чей оригинал разгружен на сервер +- **THEN** приложение подгружает оригинал с сервера для полноэкранного просмотра diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/photos/spec.md b/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/photos/spec.md new file mode 100644 index 0000000..8c1cdda --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/specs/photos/spec.md @@ -0,0 +1,24 @@ +# Spec Delta + +## ADDED Requirements + +### Requirement: Контрольная сумма оригинала + +Данные фото SHALL включать контрольную сумму содержимого оригинала, чтобы клиенты +могли сопоставлять локальные файлы с серверными фото и избегать повторной загрузки. +Контрольная сумма SHALL быть детерминированной для одного и того же содержимого. + +#### Scenario: Выдача контрольной суммы + +- **WHEN** клиент получает данные фото, у которого зарегистрирован объект-оригинал +- **THEN** ответ содержит контрольную сумму оригинала + +#### Scenario: Одинаковое содержимое + +- **WHEN** два фото ссылаются на объекты с одинаковым содержимым +- **THEN** их контрольные суммы совпадают + +#### Scenario: Контрольная сумма доступна в списке + +- **WHEN** клиент запрашивает список фото альбома +- **THEN** контрольные суммы входят в данные полученных фото diff --git a/openspec/changes/archive/2026-10-13-add-phone-album-sync/tasks.md b/openspec/changes/archive/2026-10-13-add-phone-album-sync/tasks.md new file mode 100644 index 0000000..3f525fa --- /dev/null +++ b/openspec/changes/archive/2026-10-13-add-phone-album-sync/tasks.md @@ -0,0 +1,62 @@ +# Tasks + +## 1. Предпосылки + +- [x] 1.1 Убрать правило `data/` из `.gitignore` и убедиться, что `git status` показывает файлы `android/app/src/main/java/com/galo/android/data/**` (проверка: `git status --porcelain` и `git check-ignore` больше не игнорируют их) +- [x] 1.2 Проверить, что проект Android собирается и тесты проходят после включения слоя данных в git (проверка: `cd android && ./gradlew test lintDebug`) + +## 2. Сервер: контрольная сумма оригинала + +- [x] 2.1 Добавить контрольную сумму оригинала в данные фото (модели, запросы, ответы) на существующем `objects.checksum`, без миграций БД (проверка: юнит-тест на сопоставление и на одинаковое содержимое) +- [x] 2.2 Убедиться, что контрольная сумма присутствует и в одиночной выдаче фото, и в листинге альбома (проверка: интеграционный тест ответов `GET /api/photos/:id` и `GET /api/albums/:id/photos`) + +## 3. Источник MediaStore, индекс и просмотр (режим A) + +- [x] 3.1 Добавить права `READ_MEDIA_IMAGES`/`READ_MEDIA_VIDEO` (и legacy `READ_EXTERNAL_STORAGE`) и запрос доступа; проверить поведение при предоставленном и непредоставленном доступе +- [x] 3.2 Реализовать источник альбомов устройства (bucket'ы) и их содержимого через MediaStore (проверка: тест перечисления на подготовленном наборе) +- [x] 3.3 Добавить в Room индекс содержимого синхронизируемого альбома, провенанс загрузок и связь с серверным альбомом; проверить миграцию схемы +- [x] 3.4 Реализовать генерацию превью на устройстве (фото и постер видео, длинная сторона ~1600, q80) в приватный кэш (проверка: тест размеров и наличия файлов кэша) +- [x] 3.5 Сделать смешанный список альбомов (серверные + устройства) с пометкой источника и указателем связи (проверка: UI-тест списка и связанной пары) +- [x] 3.6 Сделать экран альбома устройства с сеткой и настройками синхронизации по свайпу вправо (только выбор режима; целевой альбом выбирается при включении) (проверка: UI-тест открытия и панели) +- [x] 3.7 Реализовать режим A: просмотр альбома локально без запросов к серверу (проверка: тест, что при режиме A исходящих запросов по альбому нет) +- [x] 3.8 Описать в `android/README.md` доступ к альбомам устройства, права и режим A (проверка: описанные команды/шаги соответствуют поведению) + +## 4. Идентичность и синхронизация изменений (режим B) + +- [x] 4.1 Реализовать content-hash уходящего содержимого и сопоставление с контрольной суммой сервера, чтобы не загружать уже имеющееся (проверка: тест повторной синхронизации без новых загрузок) +- [x] 4.2 Реализовать движок обнаружения изменений на устройстве (`getGeneration`/`ContentObserver`) — добавление, удаление вне приложения, переименование, перенос, изменение содержимого (проверка: тесты на каждый вид изменения) +- [x] 4.3 Подключить синхронизацию изменений к существующей очереди `UploadRepository`/`UploadWorker` (проверка: интеграционный тест загрузки нового содержимого в целевой альбом) +- [x] 4.4 Реализовать политику удалений «только собственные загрузки» (проверка: тесты на удаление своей загрузки и на сохранение чужих фото) +- [x] 4.5 Реализовать синхронизацию переименования, переноса и правки контента (как новая загрузка + удаление устаревшей) (проверка: тесты на `PATCH`/move и на замену контента) +- [x] 4.6 Реализовать отражение серверных изменений (SSE + `?since=`) в локальном представлении без изменения оригиналов на устройстве (проверка: тесты нового фото с другого устройства и удаления на сервере) +- [x] 4.7 Включить автоматическую синхронизацию: observer в foreground и периодический WorkManager с ограничением сети из настроек (проверка: тест, что при «только Wi-Fi» синхронизация не стартует на сотовой сети) +- [x] 4.8 Описать в `android/README.md` режим B, дедуп и правила фоновой синхронизации (проверка: описание соответствует реализованному поведению) + +## 5. Разгрузка места (режим C) + +- [x] 5.1 Реализовать разгрузку фото: автоматическая перезапись файла превью с сохранением даты съёмки и места в альбоме (широкий доступ к файлам запрашивается при включении режима) (проверка: тест/ручная проверка сохранения `_ID`, даты и папки) +- [x] 5.2 Реализовать разгрузку видео как постер: удаление видео-записи и вставка постера-картинки в ту же папку (проверка: тест типа и расположения записи) +- [x] 5.3 Реализовать автоматическую разгрузку после успешной загрузки, без отдельного действия в настройках альбома (проверка: тест, что исходный ассет после загрузки становится превью) +- [x] 5.4 Писать и читать EXIF-метку провенанса (`server_album`, `server_photo`, `original_hash`, `device`, `mode`) на разгруженных превью и постерах (проверка: тест записи и чтения метки) +- [x] 5.5 Реализовать восстановление связи и провенанса из метки после переустановки, с безопасным поведением при утрате метки (проверка: тест восстановления и тест «неопознанный элемент не удаляется и не перезагружается») +- [x] 5.6 Описать в `android/README.md` режим C, широкий доступ к файлам, постер видео и метку провенанса (проверка: описание соответствует поведению) + +## 6. Обратимость и переходы режимов + +- [x] 6.1 Реализовать возврат оригиналов из режима C на устройство с проверкой свободного места (проверка: тест возврата и предупреждения при нехватке места) +- [x] 6.2 Реализовать предупреждения при выходе из C и при отказе от возврата (проверка: UI-тест текста предупреждения) +- [x] 6.3 Реализовать отвязку альбома и выключение синхронизации без удаления фото с сервера (проверка: тест, что серверные фото остаются) +- [x] 6.4 Реализовать и проверить переходы A↔B↔C, включая безопасный повторный выбор цели после переустановки (проверка: тесты переходов, отсутствие дублей при возврате с оригиналом) +- [x] 6.5 Описать в `android/README.md` обратимость, отвязку и переходы режимов (проверка: описание соответствует поведению) + +## 7. Интеграция + +- [x] 7.1 Прогнать сквозной сценарий на устройстве против запущенного galo: A → B → C → возврат, включая разгрузку и переустановку (проверка: ручной сценарий выполняется без потери и без дублей) +- [x] 7.2 Убедиться, что весь набор проверок клиента проходит (проверка: `cd android && ./gradlew test lintDebug assembleDebug`) +- [x] 7.3 Убедиться, что серверные тесты проходят с новым полем (проверка: `go test ./...` в `backend`) + +## Workflow follow-up + +- Запустить `openspec validate add-phone-album-sync --strict` перед архивацией. +- Архивировать изменение после выполнения требований ревью проекта. +- Проверить архивированный результат (обновлённые основные спеки). diff --git a/openspec/specs/android-client/spec.md b/openspec/specs/android-client/spec.md index a77788d..30bd0c9 100644 --- a/openspec/specs/android-client/spec.md +++ b/openspec/specs/android-client/spec.md @@ -79,8 +79,12 @@ Android-клиент для self-hosted galo: настройка подключ ### Requirement: Список альбомов -Приложение SHALL показывать доступные пользователю альбомы с обложкой и счётчиками, -беря данные из API, а при отсутствии сети — из локального кэша. +Приложение SHALL показывать доступные пользователю альбомы одним смешанным списком: +серверные альбомы, получаемые из API, а при отсутствии сети — из локального кэша, и +альбомы устройства. Элементы SHALL различать источник. Связанная пара «альбом +устройства → серверный альбом» SHALL отображаться одной карточкой альбома устройства +с указателем связи и меткой режима; связанный серверный альбом SHALL NOT показываться +в списке отдельной записью. #### Scenario: Онлайн @@ -92,6 +96,53 @@ Android-клиент для self-hosted galo: настройка подключ - **WHEN** сеть недоступна, но кэш существует - **THEN** список альбомов отображается из кэша с признаком офлайн-режима +#### Scenario: Альбомы устройства + +- **WHEN** у приложения есть доступ к медиа устройства +- **THEN** в списке появляются альбомы устройства наряду с серверными и помечены источником + +#### Scenario: Связанная пара + +- **WHEN** альбом устройства синхронизируется с серверным альбомом +- **THEN** в списке показывается одна карточка альбома устройства с указателем связи и меткой режима, а связанный серверный альбом отдельной записью не показывается + +### Requirement: Экран альбома устройства и настройки синхронизации + +Приложение SHALL открывать альбом устройства с сеткой медиа и SHALL предоставлять +настройки синхронизации этого альбома по свайпу вправо. Настройки SHALL содержать +только выбор режима синхронизации. + +#### Scenario: Открытие альбома устройства + +- **WHEN** пользователь выбирает альбом устройства в списке +- **THEN** открывается сетка медиа этого альбома + +#### Scenario: Настройки по свайпу + +- **WHEN** пользователь свайпает вправо в открытом альбоме устройства +- **THEN** открываются настройки синхронизации с выбором режима и только им + +#### Scenario: Метка перенесённого элемента + +- **WHEN** оригинал элемента разгружен на сервер в режиме переноса +- **THEN** в сетке на элементе показывается значок облака + +### Requirement: Доступ к альбомам устройства + +Приложение SHALL запрашивать доступ к медиа устройства, нужный для перечисления +альбомов и чтения файлов, и SHALL работать без него, не показывая альбомы +устройства. + +#### Scenario: Доступ предоставлен + +- **WHEN** пользователь предоставляет доступ к медиа устройства +- **THEN** приложение перечисляет альбомы устройства и их содержимое + +#### Scenario: Доступ не предоставлен + +- **WHEN** доступ к медиа устройства не предоставлен +- **THEN** приложение продолжает работать с серверными альбомами и не показывает альбомы устройства + ### Requirement: Постраничный просмотр альбома Приложение SHALL показывать сетку медиа альбома с постраничной загрузкой по курсору и diff --git a/openspec/specs/phone-album-sync/spec.md b/openspec/specs/phone-album-sync/spec.md new file mode 100644 index 0000000..035d454 --- /dev/null +++ b/openspec/specs/phone-album-sync/spec.md @@ -0,0 +1,242 @@ +# 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 сохранять остальные фото целевого альбома, +в том числе загруженные из других источников. + +#### Scenario: Собственная загрузка + +- **WHEN** удалено содержимое, которое загрузил этот клиент, и его контрольная сумма отсутствует во всех локальных альбомах, льющихся в этот серверный +- **THEN** приложение удаляет соответствующее серверное фото + +#### Scenario: Чужие фото + +- **WHEN** в целевом серверном альбоме есть фото, загруженные из других источников +- **THEN** приложение не удаляет их + +### Requirement: Отражение серверных изменений на устройстве + +Приложение SHALL отражать изменения целевого серверного альбома — новые фото, +переименования, теги, подписи и удаления — в локальном представлении, SHALL NOT +изменять или удалять оригиналы на устройстве. + +#### Scenario: Новое с другого устройства + +- **WHEN** другое устройство добавляет фото в целевой серверный альбом +- **THEN** приложение показывает его в серверном представлении альбома + +#### Scenario: Удалено на сервере + +- **WHEN** серверное фото удалено на сервере +- **THEN** приложение убирает его из локального представления, не трогая оригинал на устройстве + +### Requirement: Разгрузка оригиналов в режиме переноса + +В режиме C после успешной загрузки приложение SHALL автоматически заменять оригинал +на устройстве превью, без отдельного действия на экране; видео SHALL заменяться +постером-кадром. При включении режима C приложение SHALL запрашивать широкий доступ +к файлам, чтобы замена выполнялась без системного диалога согласия. + +#### Scenario: Разгрузка фото + +- **WHEN** оригинал фото успешно загружен +- **THEN** файл в галерее автоматически заменяется превью с сохранением даты съёмки и места в альбоме + +#### Scenario: Разгрузка видео + +- **WHEN** оригинал видео успешно загружен +- **THEN** видео в галерее автоматически заменяется статичным постером-кадром, а элемент в приложении остаётся видео с оригиналом на сервере + +#### Scenario: Доступ к файлам для разгрузки + +- **WHEN** пользователь включает режим переноса +- **THEN** приложение запрашивает широкий доступ к файлам, и разгрузка выполняется без системного диалога согласия + +### Requirement: Провенанс разгруженных превью + +Разгруженные превью и постеры видео SHALL содержать метаданные провенанса, +достаточные для восстановления связи с серверным альбомом и принадлежности +загрузок после переустановки приложения. + +#### Scenario: Восстановление после переустановки + +- **WHEN** приложение переустановлено или его данные очищены, а альбом устройства содержит разгруженные превью +- **THEN** приложение восстанавливает связь с серверным альбомом и принадлежность своих загрузок из метаданных превью + +#### Scenario: Метка утрачена + +- **WHEN** метаданные провенанса недоступны (например, файл перезаписан сторонним редактором) +- **THEN** элемент считается неопознанным, не удаляется и не загружается повторно + +### Requirement: Возврат оригиналов и отвязка + +При выходе из режима C приложение SHALL предложить вернуть оригиналы на устройство +и SHALL предупредить, если пользователь отказывается. Отвязка альбома и выключение +синхронизации SHALL NOT удалять фото с сервера. + +#### Scenario: Возврат оригиналов + +- **WHEN** пользователь выходит из режима C и соглашается вернуть оригиналы +- **THEN** приложение возвращает оригиналы на устройство вместо превью + +#### Scenario: Отказ от возврата + +- **WHEN** пользователь выходит из режима C и отказывается возвращать оригиналы +- **THEN** приложение предупреждает, что на устройстве останутся только превью + +#### Scenario: Отвязка + +- **WHEN** пользователь отвязывает альбом устройства от серверного +- **THEN** фото на сервере сохраняются + +#### Scenario: Выключение синхронизации + +- **WHEN** пользователь выключает синхронизацию альбома устройства +- **THEN** связь с серверным альбомом снимается, а фото на сервере сохраняются + +### Requirement: Автоматическая фоновая синхронизация + +Синхронизация SHALL выполняться автоматически: при открытом приложении — по +событиям изменения, в фоне — периодически, с учётом ограничений сети, выбранных +пользователем. В режиме C разгрузка оригиналов SHALL выполняться автоматически +после успешной загрузки. + +#### Scenario: Фоновая синхронизация + +- **WHEN** приложение не открыто, а в альбоме устройства появились изменения +- **THEN** синхронизация выполняется в фоне при ближайшей возможности + +#### Scenario: Ограничение сети + +- **WHEN** включено ограничение «только Wi-Fi», а доступна только сотовая сеть +- **THEN** синхронизация не начинается до появления Wi-Fi + +#### Scenario: Автоматическая разгрузка + +- **WHEN** оригиналы загружены в режиме C +- **THEN** замена оригиналов превью выполняется автоматически, без отдельного подтверждения + +### Requirement: Локальные превью + +Приложение SHALL генерировать превью на устройстве и хранить их локально; при +просмотре элемента без локального оригинала приложение SHALL подгружать оригинал с +сервера. + +#### Scenario: Сетка из превью + +- **WHEN** открыт альбом устройства +- **THEN** сетка показывает локальные превью + +#### Scenario: Просмотр без локального оригинала + +- **WHEN** пользователь открывает элемент, чей оригинал разгружен на сервер +- **THEN** приложение подгружает оригинал с сервера для полноэкранного просмотра diff --git a/openspec/specs/photos/spec.md b/openspec/specs/photos/spec.md index 6170a3c..42a6bbd 100644 --- a/openspec/specs/photos/spec.md +++ b/openspec/specs/photos/spec.md @@ -260,3 +260,24 @@ SHALL быть доступно и для фото без альбома (нап #### Scenario: Пустая корзина - **WHEN** пользователь очищает пустую корзину - **THEN** операция завершается успешно, удалив ноль элементов + +### Requirement: Контрольная сумма оригинала + +Данные фото SHALL включать контрольную сумму содержимого оригинала, чтобы клиенты +могли сопоставлять локальные файлы с серверными фото и избегать повторной загрузки. +Контрольная сумма SHALL быть детерминированной для одного и того же содержимого. + +#### Scenario: Выдача контрольной суммы + +- **WHEN** клиент получает данные фото, у которого зарегистрирован объект-оригинал +- **THEN** ответ содержит контрольную сумму оригинала + +#### Scenario: Одинаковое содержимое + +- **WHEN** два фото ссылаются на объекты с одинаковым содержимым +- **THEN** их контрольные суммы совпадают + +#### Scenario: Контрольная сумма доступна в списке + +- **WHEN** клиент запрашивает список фото альбома +- **THEN** контрольные суммы входят в данные полученных фото