Добавлены разделы о ВПК, алгоритме, ИИ
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
#import "common.typ": pz-fig
|
||||
#import "common.typ": pz-fig, pz-table
|
||||
|
||||
= Программная реализация
|
||||
|
||||
@@ -40,11 +40,72 @@ fun enableEncryption(storage: IStorageInfo, password: String, encryptPath: Boole
|
||||
|
||||
`VaultsManager` агрегирует один `LocalVault` и удалённые vault; адаптеры реализуют доступ к файлам внутри каждого `IStorage`. Регистрация удалённых vault — через модуль `:vault-contracts`.
|
||||
|
||||
=== Проект модуля синхронизации
|
||||
=== Модуль синхронизации хранилищ
|
||||
|
||||
В Room добавлена сущность `DbStorageSyncGroup`; спроектирован `SyncWorker` и очередь UUID для фонового сравнения историй коммитов (рис. @fig-01–@fig-03). Реализация синхронизации находится в стадии развития.
|
||||
В Room хранится сущность `DbStorageSyncGroup` — набор UUID `Storage`, которые должны иметь согласованное состояние. Запуск синхронизации выполняется через `RunStorageSyncUseCase` / `WorkManager` и debounce при изменении файлов (рис. @fig-01–@fig-03). Движок `StorageSyncEngine` (модуль `:usecases`) реализует согласование журналов изменений; доработка касается в основном политики фонового расписания и UX отображения прогресса.
|
||||
|
||||
Модель синхронизации опирается на аналогию с распределённой системой контроля версий: для каждого `Storage` ведётся история коммитов; сервис по таймеру или через `WorkManager` сравнивает локальную и удалённую истории и приводит зашифрованное содержимое к согласованному состоянию. Ключи шифрования в этот обмен не включаются — провайдер видит только зашифрованные объекты. Модуль `:task-runtime` зарезервирован для фоновых задач длительного шифрования и синхронизации без блокировки UI.
|
||||
Для каждого `Storage` ведётся журнал изменений по относительным путям пользовательских файлов. Запись журнала (`StorageSyncJournalEntry`) содержит операцию (`UPSERT`, `TRASH`, `DELETE`) и ревизию (`sequence`, `actorId`, `createdAt`). Ключи шифрования в обмен не включаются — провайдер видит только зашифрованные объекты. Модуль `:task-runtime` обслуживает длительные задачи шифрования и синхронизации без блокировки UI.
|
||||
|
||||
=== Алгоритм согласования журналов синхронизации
|
||||
|
||||
Синхронизация одной группы выполняется в несколько этапов (рис. @fig-35).
|
||||
|
||||
#pz-fig("fig_35_sync_merge_algorithm.png", [Алгоритм согласования журналов (StorageSyncEngine)], "fig-35")
|
||||
|
||||
*Подготовка.* По UUID из `DbStorageSyncGroup` загружаются объекты `IStorage`. Если в группе меньше двух хранилищ или несовместимы параметры шифрования, синхронизация пропускается. На каждом accessor запрашивается блокировка sync (lease на удалённом диске — best-effort; внутри процесса группа сериализуется `Mutex`). Параллельно для каждого storage вызываются `flushPendingSyncJournal()` и `readSyncJournal()`; служебные пути отфильтровываются (`StorageSyncPaths.isSyncableUserPath`).
|
||||
|
||||
*Слияние журналов.* Объект `StorageSyncJournalMerge` объединяет журналы в отображение «путь → победитель». Для каждого пути остаётся запись с наибольшей ревизией. Сравнение реализовано функцией `compareEntries`: сначала `revision.sequence`, при равенстве — `actorId`, затем `createdAt`. Такой порядок обеспечивает детерминированный выбор одной записи при конкурентных изменениях на разных устройствах.
|
||||
|
||||
*Выбор источника для пути.* После слияния для каждой пары (путь, `winnerEntry`) вызывается `findSourceStorage`:
|
||||
|
||||
+ при `UPSERT` — первый `Storage`, у которого запись по этому пути *совпадает* с победителем (`compareEntries == 0`): оттуда читаются байты для копирования;
|
||||
+ при `DELETE` или `TRASH` — первый `Storage`, где путь ещё присутствует в журнале (или любой storage группы, если записей нет): оттуда инициируется удаление на целях.
|
||||
|
||||
Если для `UPSERT` источник не найден, путь пропускается (файл уже отсутствует на всех носителях).
|
||||
|
||||
*Распространение на цели.* Для каждого `target` в группе, отличного от источника, сравнивается ревизия записи на цели с `winnerEntry`. Если цель уже не слабее победителя (`compareEntries(target, winner) >= 0`), шаг пропускается. Иначе вызывается `applyEntry`: для `UPSERT` — потоковое копирование `openRead` → `openWrite`; для `DELETE` / `TRASH` — `delete` или `moveToTrash`. Все операции выполняются с `recordSyncJournal = false`, чтобы не порождать цикл повторной синхронизации. Ошибки отдельных путей учитываются счётчиком `applyFailures`, отмена кооперативная (проверка `syncGeneration`, снятие блокировок в `finally`).
|
||||
|
||||
Фрагмент сравнения ревизий и выбора источника:
|
||||
|
||||
```kotlin
|
||||
private fun compareEntries(a: StorageSyncJournalEntry, b: StorageSyncJournalEntry): Int {
|
||||
val seqCmp = a.revision.sequence.compareTo(b.revision.sequence)
|
||||
if (seqCmp != 0) return seqCmp
|
||||
val actorCmp = a.revision.actorId.compareTo(b.revision.actorId)
|
||||
if (actorCmp != 0) return actorCmp
|
||||
return a.revision.createdAt.compareTo(b.revision.createdAt)
|
||||
}
|
||||
|
||||
private fun findSourceStorage(..., winnerEntry: StorageSyncJournalEntry): IStorage? {
|
||||
if (winnerEntry.operation == DELETE || winnerEntry.operation == TRASH) {
|
||||
return storages.firstOrNull { entriesByStorage[it.uuid]?.get(path) != null }
|
||||
?: storages.firstOrNull()
|
||||
}
|
||||
return storages.firstOrNull { storage ->
|
||||
val entry = entriesByStorage[storage.uuid]?.get(path) ?: return@firstOrNull false
|
||||
compareEntries(entry, winnerEntry) == 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Ограничения модели.* Механизм не является полноценным CRDT: конфликты снимаются фиксированным порядком ревизий, а не автоматическим слиянием содержимого. Содержимое файла при расхождении версий без роста `sequence` на одном пути не анализируется побайтно. Шифротекст передаётся как есть; расшифровка на стороне провайдера не предполагается. Корректность алгоритма проверена unit-тестами `StorageSyncEngineTest` (гл. 5): слияние одной записи на путь, пропуск цели с актуальной ревизией, копирование и удаление, cooperative cancellation.
|
||||
|
||||
=== Использование средств ИИ при разработке
|
||||
|
||||
Разработка Wallenc велась в два этапа. На первом этапе исполнитель самостоятельно спроектировал доменную модель (иерархия vault → storage → файлы, единый `VaultsManager`), навигацию между экранами, визуальный стиль UI на Jetpack Compose, границы Gradle-модулей и каркас use case-слоя. Криптографический контур (`Encryptor`, привязка ключей к storage), журнал синхронизации и сценарии OAuth проектировались и проверялись вручную.
|
||||
|
||||
На втором этапе, после готовности архитектурного каркаса, наращивание функционала выполнялось с помощью среды Cursor (модели семейства Composer): адаптеры Yandex Disk и локального storage, движок `StorageSyncEngine`, экраны 2FA и текстовых секретов, unit-тесты. После каждой генерации код просматривался в diff, запускались модульные тесты (`./gradlew :usecases:test` и смежные модули), критичные сценарии проверялись на устройстве.
|
||||
|
||||
#pz-table(
|
||||
[Роли при разработке (фрагмент)],
|
||||
3,
|
||||
table.header([Этап], [Исполнитель], [Инструмент / метод]),
|
||||
[Домен, навигация, UI-концепция], [Исполнитель ВКР], [Ручное проектирование, Compose],
|
||||
[Адаптеры, sync, тесты, доработка UI], [Исполнитель ВКР + ревью], [Cursor, Gradle test],
|
||||
[Шифрование, OAuth, ревизии журнала], [Исполнитель ВКР], [Ручная ревизия, без автогенерации «вслепую»],
|
||||
) <tbl-ai-dev>
|
||||
|
||||
ИИ использовался как ускоритель шаблонного и повторяющегося кода, а не как замена проектных решений. Риски (неверные сигнатуры API, лишние зависимости, утечки в логи) снижались обязательной проверкой сборки, отсутствием секретов в репозитории и правилами `.gitignore` для локальных конфигураций.
|
||||
|
||||
== Разработка мобильного приложения на Kotlin (Android)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user