chore(openspec): archive add-phone-album-sync and sync main specs
Sync the deltas into phpne...
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-13
|
||||||
@@ -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` для неотредактированной геолокации при локальной
|
||||||
|
генерации превью — можно уточнить позже.
|
||||||
@@ -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; синхронизация на уровне сервера (устройства/сессии на
|
||||||
|
бэкенде); многопользовательские конфликты владения; редактирование изображений.
|
||||||
@@ -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** приложение продолжает работать с серверными альбомами и не показывает альбомы устройства
|
||||||
+242
@@ -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** приложение подгружает оригинал с сервера для полноэкранного просмотра
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Spec Delta
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Контрольная сумма оригинала
|
||||||
|
|
||||||
|
Данные фото SHALL включать контрольную сумму содержимого оригинала, чтобы клиенты
|
||||||
|
могли сопоставлять локальные файлы с серверными фото и избегать повторной загрузки.
|
||||||
|
Контрольная сумма SHALL быть детерминированной для одного и того же содержимого.
|
||||||
|
|
||||||
|
#### Scenario: Выдача контрольной суммы
|
||||||
|
|
||||||
|
- **WHEN** клиент получает данные фото, у которого зарегистрирован объект-оригинал
|
||||||
|
- **THEN** ответ содержит контрольную сумму оригинала
|
||||||
|
|
||||||
|
#### Scenario: Одинаковое содержимое
|
||||||
|
|
||||||
|
- **WHEN** два фото ссылаются на объекты с одинаковым содержимым
|
||||||
|
- **THEN** их контрольные суммы совпадают
|
||||||
|
|
||||||
|
#### Scenario: Контрольная сумма доступна в списке
|
||||||
|
|
||||||
|
- **WHEN** клиент запрашивает список фото альбома
|
||||||
|
- **THEN** контрольные суммы входят в данные полученных фото
|
||||||
@@ -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` перед архивацией.
|
||||||
|
- Архивировать изменение после выполнения требований ревью проекта.
|
||||||
|
- Проверить архивированный результат (обновлённые основные спеки).
|
||||||
@@ -79,8 +79,12 @@ Android-клиент для self-hosted galo: настройка подключ
|
|||||||
|
|
||||||
### Requirement: Список альбомов
|
### Requirement: Список альбомов
|
||||||
|
|
||||||
Приложение SHALL показывать доступные пользователю альбомы с обложкой и счётчиками,
|
Приложение SHALL показывать доступные пользователю альбомы одним смешанным списком:
|
||||||
беря данные из API, а при отсутствии сети — из локального кэша.
|
серверные альбомы, получаемые из API, а при отсутствии сети — из локального кэша, и
|
||||||
|
альбомы устройства. Элементы SHALL различать источник. Связанная пара «альбом
|
||||||
|
устройства → серверный альбом» SHALL отображаться одной карточкой альбома устройства
|
||||||
|
с указателем связи и меткой режима; связанный серверный альбом SHALL NOT показываться
|
||||||
|
в списке отдельной записью.
|
||||||
|
|
||||||
#### Scenario: Онлайн
|
#### Scenario: Онлайн
|
||||||
|
|
||||||
@@ -92,6 +96,53 @@ Android-клиент для self-hosted galo: настройка подключ
|
|||||||
- **WHEN** сеть недоступна, но кэш существует
|
- **WHEN** сеть недоступна, но кэш существует
|
||||||
- **THEN** список альбомов отображается из кэша с признаком офлайн-режима
|
- **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: Постраничный просмотр альбома
|
### Requirement: Постраничный просмотр альбома
|
||||||
|
|
||||||
Приложение SHALL показывать сетку медиа альбома с постраничной загрузкой по курсору и
|
Приложение SHALL показывать сетку медиа альбома с постраничной загрузкой по курсору и
|
||||||
|
|||||||
@@ -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** приложение подгружает оригинал с сервера для полноэкранного просмотра
|
||||||
@@ -260,3 +260,24 @@ SHALL быть доступно и для фото без альбома (нап
|
|||||||
#### Scenario: Пустая корзина
|
#### Scenario: Пустая корзина
|
||||||
- **WHEN** пользователь очищает пустую корзину
|
- **WHEN** пользователь очищает пустую корзину
|
||||||
- **THEN** операция завершается успешно, удалив ноль элементов
|
- **THEN** операция завершается успешно, удалив ноль элементов
|
||||||
|
|
||||||
|
### Requirement: Контрольная сумма оригинала
|
||||||
|
|
||||||
|
Данные фото SHALL включать контрольную сумму содержимого оригинала, чтобы клиенты
|
||||||
|
могли сопоставлять локальные файлы с серверными фото и избегать повторной загрузки.
|
||||||
|
Контрольная сумма SHALL быть детерминированной для одного и того же содержимого.
|
||||||
|
|
||||||
|
#### Scenario: Выдача контрольной суммы
|
||||||
|
|
||||||
|
- **WHEN** клиент получает данные фото, у которого зарегистрирован объект-оригинал
|
||||||
|
- **THEN** ответ содержит контрольную сумму оригинала
|
||||||
|
|
||||||
|
#### Scenario: Одинаковое содержимое
|
||||||
|
|
||||||
|
- **WHEN** два фото ссылаются на объекты с одинаковым содержимым
|
||||||
|
- **THEN** их контрольные суммы совпадают
|
||||||
|
|
||||||
|
#### Scenario: Контрольная сумма доступна в списке
|
||||||
|
|
||||||
|
- **WHEN** клиент запрашивает список фото альбома
|
||||||
|
- **THEN** контрольные суммы входят в данные полученных фото
|
||||||
|
|||||||
Reference in New Issue
Block a user