Все картинки в отчёте

This commit is contained in:
2026-05-26 23:16:40 +03:00
parent ce70c13f86
commit 3673c4aa8d
29 changed files with 121 additions and 142 deletions

View File

@@ -74,6 +74,17 @@ Wallenc — мобильное приложение для Android: `VaultsManag
#pz-fig("fig_09_remote_vaults.jpg", [Экран удалённых vault], "fig-09-rp") #pz-fig("fig_09_remote_vaults.jpg", [Экран удалённых vault], "fig-09-rp")
#pz-fig("fig_10_yandex_oauth.jpg", [OAuth Яндекс], "fig-10-rp") #pz-fig("fig_10_yandex_oauth.jpg", [OAuth Яндекс], "fig-10-rp")
=== Секреты и 2FA внутри storage
1. Откройте storage (после создания или из списка).
2. На экране storage перейдите в разделы «Секреты» и «2FA».
#pz-fig("fig_33_storage_secrets_2fa.jpg", [Экран storage: секреты и 2FA], "fig-33-rp")
3. В разделе 2FA добавьте TOTP-токен; на экране отображается сгенерированный код.
#pz-fig("fig_34_2fa_single_token.jpg", [Экран 2FA с одним токеном], "fig-34-rp")
=== Фоновые задачи === Фоновые задачи
На экране задач отображаются операции шифрования и синхронизации. Уведомления информируют о завершении (см. рис. 12 и 13 в гл. 5). На экране задач отображаются операции шифрования и синхронизации. Уведомления информируют о завершении (см. рис. 12 и 13 в гл. 5).

View File

@@ -1,39 +1,71 @@
# Реестр иллюстраций ПЗ Wallenc # Реестр иллюстраций ПЗ Wallenc
Правило: файл в `Report/images/` только с именем из таблицы. Статус `ready` — можно вставлять в `.typ`. Правила:
| № | Имя файла | Статус | Где используется | Typst label | - Файл в `Report/images/`**только** имя из таблицы.
|---|-----------|--------|------------------|-------------| - Статус `ready` — файл на месте и подключён в `.typ`.
| 1 | fig_01_start_sync.png | ready | 3.3.3, 4.1.4 | fig-01 | - Статус `placeholder` — файл ещё не подготовлен (сейчас все иллюстрации `ready`).
| 2 | fig_02_vault_lifecycle.png | ready | 3.3.3, 4.1.4 | fig-02 | - Диаграммы PlantUML: `cd Report && bash scripts/render_puml.sh`.
| 3 | fig_03_navigation_hub.png | ready | 3.3.3, 4.1.4 | fig-03 |
| 4 | fig_04_domain_class.png | ready | 2.3.2, 4.2.1 | fig-04 |
| 5 | fig_05_local_vaults.jpg | placeholder | 3.4, 4.2.3, 5.2.2, прил. В | fig-05 |
| 6 | fig_06_encrypt_dialog.jpg | placeholder | 3.4, 4.1.1, 5.2.2, прил. В | fig-06 |
| 7 | fig_07_open_close_dialog.jpg | placeholder | 3.4, 4.1.2, прил. В | fig-07 |
| 8 | fig_08_rename_delete_dialog.jpg | placeholder | 3.4, 5.2.2, прил. В | fig-08 |
| 9 | fig_09_remote_vaults.jpg | placeholder | 3.4, 4.2.3, прил. В | fig-09 |
| 10 | fig_10_yandex_oauth.jpg | placeholder | 3.4, 4.2.3, 1.2.2.4, прил. В | fig-10 |
| 11 | fig_11_room_schema.png | ready | 2.3.2, 4.2.2, прил. В | fig-11 |
| 12 | fig_12_tasks_screen.jpg | placeholder | 5.2.3, прил. В | fig-12 |
| 13 | fig_13_tasks_notification.jpg | placeholder | 5.2.3, прил. В | fig-13 |
| 14 | fig_14_context_system.png | ready | 1.2.1, 2.1.4 | fig-14 |
| 15 | fig_15_bpmn_vault.png | ready | 2.1.3 | fig-15 |
| 16 | fig_16_dfd_level0.png | ready | 2.2 | fig-16 |
| 17 | fig_17_use_case.png | ready | 2.3.1 | fig-17 |
| 18 | fig_18_deployment.png | ready | 2.3.3 | fig-18 |
| 19 | fig_19_clean_architecture.png | ready | 2.3, 4.3 | fig-19 |
| 20 | fig_20_oauth_sequence.png | ready | 1.5.2, 4.2.3 | fig-20 |
| 21 | fig_21_encrypt_flow.png | ready | 4.1.1, 5.2.1 | fig-21 |
| 22 | fig_22_cjm_vault.png | ready | 3.3.2 | fig-22 |
| 23 | fig_23_module_deps.png | ready | 4.3 | fig-23 |
| 27 | fig_27_gradle_domain_test.png | ready | 5.2.1 | fig-27 |
| 28 | fig_28_gradle_usecases_test.png | ready | 5.2.2 | fig-28 |
| 29 | fig_29_gradle_ui_test.png | ready | 5.2.3 | fig-29 |
| 30 | fig_30_gradle_test_summary.png | ready | 5.4 | fig-30 |
| 31 | fig_31_gradle_connected_test.png | ready | 5.3 | fig-31 |
| 32 | fig_32_manual_test_checklist.png | ready | 5.3 | fig-32 |
Диаграммы fig_0104, fig_11, fig_1423: исходники `Report/puml/fig_*.puml` (`@startuml` = имя PNG), растр — `Report/scripts/render_puml.sh` сразу в `Report/images/` (без копирования). ## Соответствие «new images» → `images/`
Скриншоты UI (fig_05fig_10, fig_1213) и Gradle (fig_2732) — заменить placeholder на реальные снимки перед защитой. | Исходник в `new images/` | Целевой файл |
|--------------------------|--------------|
| `05.jpg``10.jpg`, `12.jpg`, `13.jpg` | `fig_05``fig_10`, `fig_12`, `fig_13` |
| `27-domain-test.png``31-gradle-connected.png` | `fig_27``fig_31` |
| `33-storage-screen.jpg` | `fig_33_storage_secrets_2fa.jpg` |
| `34-2fa-screen.jpg` | `fig_34_2fa_single_token.jpg` |
---
## Полный реестр
| № | Имя файла | Тип | Статус | Где в ПЗ | Примечание (содержание) | label |
|---|-----------|-----|--------|----------|-------------------------|-------|
| 01 | `fig_01_start_sync.png` | PlantUML | ready | гл. 3.3.3, 4.1.4 | Старт → Room → SyncWorker. **PUML** | fig-01 |
| 02 | `fig_02_vault_lifecycle.png` | PlantUML | ready | гл. 3.3.3, 4.1.4 | Жизненный цикл storage. **PUML** | fig-02 |
| 03 | `fig_03_navigation_hub.png` | PlantUML | ready | гл. 3.3.3, 4.1.4 | Навигация + SyncWorker. **PUML** | fig-03 |
| 04 | `fig_04_domain_class.png` | PlantUML | ready | гл. 2.3.2, 4.2.1 | Классы `:domain`. **PUML** | fig-04 |
| 05 | `fig_05_local_vaults.jpg` | Скриншот UI | ready | гл. 3.4, 5, прил. Б, В | Список storage (локальный vault) | fig-05 |
| 06 | `fig_06_encrypt_dialog.jpg` | Скриншот UI | ready | гл. 3.4, 4.1.1, 5, прил. Б, В | Диалог включения шифрования | fig-06 |
| 07 | `fig_07_open_close_dialog.jpg` | Скриншот UI | ready | гл. 3.4, 4.1.2, прил. Б, В | Открытие/закрытие storage | fig-07 |
| 08 | `fig_08_rename_delete_dialog.jpg` | Скриншот UI | ready | гл. 3.4, 5, прил. Б, В | Переименование или удаление | fig-08 |
| 09 | `fig_09_remote_vaults.jpg` | Скриншот UI | ready | гл. 3.4, 4.2.3, прил. Б, В | Удалённые vault | fig-09 |
| 10 | `fig_10_yandex_oauth.jpg` | Скриншот UI | ready | гл. 1, 3.4, 4.2.3, прил. Б, В | OAuth Яндекс | fig-10 |
| 11 | `fig_11_room_schema.png` | PlantUML | ready | гл. 2.3.2, 4.2.2, прил. В | Схема Room. **PUML** | fig-11 |
| 12 | `fig_12_tasks_screen.jpg` | Скриншот UI | ready | гл. 5.2.3, прил. В | Экран задач | fig-12 |
| 13 | `fig_13_tasks_notification.jpg` | Скриншот UI | ready | гл. 5.2.3, прил. В | Уведомление о задаче | fig-13 |
| 14 | `fig_14_context_system.png` | PlantUML | ready | гл. 1.2.1, 2.1.4 | Контекстная диаграмма. **PUML** | fig-14 |
| 15 | `fig_15_bpmn_vault.png` | PlantUML | ready | гл. 2.1.3 | BPMN. **PUML** | fig-15 |
| 16 | `fig_16_dfd_level0.png` | PlantUML | ready | гл. 2.2 | DFD-0. **PUML** | fig-16 |
| 17 | `fig_17_use_case.png` | PlantUML | ready | гл. 2.3.1 | Прецеденты. **PUML** | fig-17 |
| 18 | `fig_18_deployment.png` | PlantUML | ready | гл. 2.3.3 | Развёртывание. **PUML** | fig-18 |
| 19 | `fig_19_clean_architecture.png` | PlantUML | ready | гл. 2.3, 4.3 | Clean Architecture. **PUML** | fig-19 |
| 20 | `fig_20_oauth_sequence.png` | PlantUML | ready | гл. 1.5.2, 4.2.3 | OAuth sequence. **PUML** | fig-20 |
| 21 | `fig_21_encrypt_flow.png` | PlantUML | ready | гл. 4.1.1, 5.2.1 | Блок-схема шифрования. **PUML** | fig-21 |
| 22 | `fig_22_cjm_vault.png` | PlantUML | ready | гл. 3.3.2 | CJM. **PUML** | fig-22 |
| 23 | `fig_23_module_deps.png` | PlantUML | ready | гл. 4.3 | Зависимости Gradle. **PUML** | fig-23 |
| 27 | `fig_27_gradle_domain_test.png` | Gradle | ready | гл. 5.2.1 | `:domain:test` SUCCESS | fig-27 |
| 28 | `fig_28_gradle_usecases_test.png` | Gradle | ready | гл. 5.2.2 | `:usecases:test` SUCCESS | fig-28 |
| 29 | `fig_29_gradle_ui_test.png` | Gradle | ready | гл. 5.2.3 | `:ui:test` SUCCESS | fig-29 |
| 30 | `fig_30_gradle_test_summary.png` | Gradle | ready | гл. 5.4 | Сводка `test` | fig-30 |
| 31 | `fig_31_gradle_connected_test.png` | Gradle | ready | гл. 5.3 | `connectedDebugAndroidTest` SUCCESS | fig-31 |
| 32 | `fig_32_manual_test_checklist.png` | PlantUML Salt | ready | гл. 5.3 | Таблица-чек-лист T-7…T-12 (как @tbl-testres). **PUML** `fig_32_manual_test_checklist.puml` | fig-32 |
| 33 | `fig_33_storage_secrets_2fa.jpg` | Скриншот UI | ready | гл. 1, 3.4, 5, прил. Б, В | Экран storage: вкладки/разделы секретов и 2FA | fig-33 |
| 34 | `fig_34_2fa_single_token.jpg` | Скриншот UI | ready | гл. 1, 3.4, 5, прил. Б, В | Экран 2FA с одним TOTP-токеном | fig-34 |
## Нумерация в PDF
Номер «Рисунок N» — по порядку появления в тексте, не по номеру в имени файла.
## fig_31
Один скрин `./gradlew connectedDebugAndroidTest` из корня репозитория, `BUILD SUCCESSFUL` (см. прежнее описание в истории реестра).
## Команды
```bash
cd Report && bash scripts/render_puml.sh
cd Report && python scripts/check_images.py
typst compile --root .. ояснительная_записка_ПытковРЕ.typ"
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -48,7 +48,7 @@
==== Работа с содержимым storage ==== Работа с содержимым storage
Операции чтения и записи выполняются через единый интерфейс файлового доступа независимо от типа хранилища. Операции чтения и записи выполняются через единый интерфейс файлового доступа независимо от типа хранилища. Внутри открытого storage доступны текстовые секреты и генерация TOTP для 2FA (рис. @fig-33, @fig-34).
==== Удалённые хранилища и авторизация во внешних провайдерах ==== Удалённые хранилища и авторизация во внешних провайдерах

View File

@@ -54,7 +54,7 @@ CJM сценария «защитить и открыть vault» предста
== Проработка прототипа и особенности дизайна == Проработка прототипа и особенности дизайна
Интерфейс реализован на Jetpack Compose @compose-docs. Экраны локальных и удалённых vault, диалоги шифрования и OAuth показаны на рис. @fig-05@fig-10 (подробно приложение В и руководство пользователя в приложении Б). Интерфейс реализован на Jetpack Compose @compose-docs. Экраны локальных и удалённых vault, диалоги шифрования, OAuth, а также разделы текстовых секретов и 2FA внутри storage показаны на рис. @fig-05@fig-10 и @fig-33@fig-34 (подробно приложение В и руководство пользователя в приложении Б).
#pz-fig("fig_05_local_vaults.jpg", [Список storage в локальном vault (экран «локальные vault»)], "fig-05") #pz-fig("fig_05_local_vaults.jpg", [Список storage в локальном vault (экран «локальные vault»)], "fig-05")
#pz-fig("fig_06_encrypt_dialog.jpg", [Диалог включения шифрования], "fig-06") #pz-fig("fig_06_encrypt_dialog.jpg", [Диалог включения шифрования], "fig-06")
@@ -62,6 +62,8 @@ CJM сценария «защитить и открыть vault» предста
#pz-fig("fig_08_rename_delete_dialog.jpg", [Диалог переименования и удаления], "fig-08") #pz-fig("fig_08_rename_delete_dialog.jpg", [Диалог переименования и удаления], "fig-08")
#pz-fig("fig_09_remote_vaults.jpg", [Экран удалённых vault], "fig-09") #pz-fig("fig_09_remote_vaults.jpg", [Экран удалённых vault], "fig-09")
#pz-fig("fig_10_yandex_oauth.jpg", [Добавление удалённого vault, OAuth Яндекс], "fig-10") #pz-fig("fig_10_yandex_oauth.jpg", [Добавление удалённого vault, OAuth Яндекс], "fig-10")
#pz-fig("fig_33_storage_secrets_2fa.jpg", [Экран storage: разделы «Секреты» и «2FA»], "fig-33")
#pz-fig("fig_34_2fa_single_token.jpg", [Экран 2FA: список с одним TOTP-токеном], "fig-34")
== Требования к эргономике и доступности == Требования к эргономике и доступности

View File

@@ -46,7 +46,7 @@
[T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ только с ключом (рис. @fig-07)], [T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ только с ключом (рис. @fig-07)],
[T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. @fig-10)], [T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. @fig-10)],
[T-11], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 1213)], [T-11], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 1213)],
[T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений], [T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений (рис. @fig-33@fig-34)],
) <tbl-testplan> ) <tbl-testplan>
=== Критерии начала и окончания === Критерии начала и окончания
@@ -113,7 +113,7 @@
[:app], [YandexDiskLiveIntegrationTest], [Живой API (при наличии токена)], [3], [:app], [YandexDiskLiveIntegrationTest], [Живой API (при наличии токена)], [3],
) <tbl-androidtest> ) <tbl-androidtest>
Запуск: `./gradlew connectedDebugAndroidTest`. Результат рис. @fig-31. Запуск: `./gradlew connectedDebugAndroidTest`. Результат рис. @fig-31. Отрисовка экранов секретов и 2FA подтверждена скриншотами @fig-33@fig-34.
#pz-fig("fig_31_gradle_connected_test.png", [Gradle connectedDebugAndroidTest], "fig-31") #pz-fig("fig_31_gradle_connected_test.png", [Gradle connectedDebugAndroidTest], "fig-31")

View File

@@ -0,0 +1,15 @@
@startsalt fig_32_manual_test_checklist
{#
<b>Чек-лист ручного UI-тестирования Wallenc</b>
.
<b>ID</b> | <b>Шаг</b> | <b>Статус</b> | <b>Фактический результат</b> | <b>Рис.</b>
T-7 | Создать storage в LocalVault | OK | Storage в списке | 5
T-8 | Включить шифрование storage | OK | Статус encrypted | 6
T-9 | Открыть / закрыть storage | OK | Контент при открытом storage | 7
T-10 | OAuth Яндекс | OK | Запись в DbYandexAccount | 10
T-11 | Фоновая задача шифрования | OK | Прогресс на экране задач | 12
T-12 | Уведомление о завершении | OK | Notification отображён | 13
.
Платформа: Android (эмулятор / устройство), 2026
#}
@endsalt

View File

@@ -6,11 +6,14 @@ import re
import sys import sys
from pathlib import Path from pathlib import Path
REQUIRED_READY = { REQUIRED_READY = (
f"fig_{i:02d}_" for i in range(1, 5) {f"fig_{i:02d}_" for i in range(1, 5)}
} | {f"fig_11_"} | {f"fig_{i:02d}_" for i in range(14, 24)} | {f"fig_11_"}
| {f"fig_{i:02d}_" for i in range(14, 24)}
| {f"fig_32_"}
)
WARN_PLACEHOLDER = {f"fig_{i:02d}_" for i in range(5, 11)} | {f"fig_12_"} | {f"fig_13_"} WARN_PLACEHOLDER: set[str] = set()
def main() -> int: def main() -> int:

View File

@@ -72,3 +72,5 @@
#pz-fig("fig_11_room_schema.png", [Схема Room], "fig-11-app") #pz-fig("fig_11_room_schema.png", [Схема Room], "fig-11-app")
#pz-fig("fig_12_tasks_screen.jpg", [Экран задач], "fig-12-app") #pz-fig("fig_12_tasks_screen.jpg", [Экран задач], "fig-12-app")
#pz-fig("fig_13_tasks_notification.jpg", [Уведомление о задачах], "fig-13-app") #pz-fig("fig_13_tasks_notification.jpg", [Уведомление о задачах], "fig-13-app")
#pz-fig("fig_33_storage_secrets_2fa.jpg", [Экран storage: секреты и 2FA], "fig-33-app")
#pz-fig("fig_34_2fa_single_token.jpg", [Экран 2FA с токеном], "fig-34-app")

View File

@@ -28,10 +28,6 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["yandex.oauth.token"] = testInstrumentationRunnerArguments["yandex.oauth.token"] =
localProps.getProperty("yandex.test.oauth.token").orEmpty() localProps.getProperty("yandex.test.oauth.token").orEmpty()
testInstrumentationRunnerArguments["yandex.user.id"] =
localProps.getProperty("yandex.test.user.id").orEmpty()
testInstrumentationRunnerArguments["yandex.vault.uuid"] =
localProps.getProperty("yandex.test.vault.uuid").orEmpty()
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }

View File

@@ -1,53 +0,0 @@
package com.github.nullptroma.wallenc.app.integration.yandex
import android.util.Log
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.nullptroma.wallenc.infrastructure.android.db.app.AppDb
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
/**
* Ручной helper: после входа в Yandex через приложение печатает в Logcat маскированный
* токен и подсказку для local.properties. Запускать вручную из Android Studio.
*/
@RunWith(AndroidJUnit4::class)
@Ignore("Manual: export Yandex credentials to local.properties")
class ExportYandexTestCredentialsTest {
@Test
fun printFirstAccountCredentialsToLogcat() {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
val db = Room.databaseBuilder(context, AppDb::class.java, "wallenc.db")
.build()
try {
val row = runBlocking {
db.yandexAccountDao.observeAll().first().firstOrNull()
}
if (row == null) {
Log.i(TAG, "No Yandex accounts in DB. Link a vault in the app first.")
return
}
Log.i(TAG, "Add to local.properties:")
Log.i(TAG, "yandex.test.oauth.token=<copy full token from app debug storage if needed>")
Log.i(TAG, "yandex.test.oauth.token.prefix=${maskToken(row.oauthToken)}")
Log.i(TAG, "yandex.test.user.id=${row.yandexUserId}")
Log.i(TAG, "yandex.test.vault.uuid=${row.vaultUuid}")
} finally {
db.close()
}
}
private fun maskToken(token: String): String {
if (token.length <= 8) return "***"
return token.take(4) + "" + token.takeLast(4)
}
companion object {
private const val TAG = "WallencYandexExport"
}
}

View File

@@ -12,11 +12,15 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
/**
* Live-прогон Disk API. Пути только `app:/` — как в [YandexVault]; токен из Auth SDK
* обычно имеет `cloud_api:disk.app_folder`, без полного доступа к `disk:/` (там 403).
*/
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class YandexDiskLiveIntegrationTest { class YandexDiskLiveIntegrationTest {
private lateinit var repository: YandexDiskRepository private lateinit var repository: YandexDiskRepository
private val testFolder = "disk:/wallenc-integration-test" private val testFolder = "app:/wallenc-integration-test"
private val probeFileName = "wallenc-probe.txt" private val probeFileName = "wallenc-probe.txt"
private val probePath = "$testFolder/$probeFileName" private val probePath = "$testFolder/$probeFileName"
private val probePayload = "wallenc-integration-probe".encodeToByteArray() private val probePayload = "wallenc-integration-probe".encodeToByteArray()

View File

@@ -1,11 +1,9 @@
package com.github.nullptroma.wallenc.app.sync package com.github.nullptroma.wallenc.app.sync
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason import com.github.nullptroma.wallenc.domain.tasks.StorageSyncTriggerReason
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
import com.github.nullptroma.wallenc.usecases.StorageSyncReadiness
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@@ -13,13 +11,10 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -29,15 +24,11 @@ class StorageSyncBootstrap @Inject constructor(
private val scheduler: StorageSyncScheduler, private val scheduler: StorageSyncScheduler,
private val vaultsManager: IVaultsManager, private val vaultsManager: IVaultsManager,
private val syncRunner: RunStorageSyncUseCase, private val syncRunner: RunStorageSyncUseCase,
private val syncReadiness: StorageSyncReadiness,
private val groupStore: IStorageSyncGroupStore,
) { ) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val startupSyncScheduled = AtomicBoolean(false)
fun start() { fun start() {
scheduler.ensureScheduled() scheduler.ensureScheduled()
scheduleStartupSyncOnce()
scope.launch { scope.launch {
combine( combine(
vaultsManager.allStorages, vaultsManager.allStorages,
@@ -86,31 +77,4 @@ class StorageSyncBootstrap @Inject constructor(
} }
return System.currentTimeMillis() >= syncRunner.debounceSuppressUntilMs.value return System.currentTimeMillis() >= syncRunner.debounceSuppressUntilMs.value
} }
/**
* Одна синхронизация после готовности хранилищ при старте процесса — не ждать только WorkManager
* (особенно если periodic work откладывался из‑за перезапусков процесса).
*/
private fun scheduleStartupSyncOnce() {
scope.launch {
combine(
vaultsManager.allStorages,
vaultsManager.unlockManager.openedStorages,
) { rootStorages, opened ->
(rootStorages + opened.values).distinctBy { it.uuid }
}
.map { it.isNotEmpty() }
.distinctUntilChanged()
.filter { it }
.first()
if (!startupSyncScheduled.compareAndSet(false, true)) {
return@launch
}
if (groupStore.getGroups().isEmpty()) {
return@launch
}
syncReadiness.awaitReady()
syncRunner.enqueue(StorageSyncTriggerReason.Background)
}
}
} }

View File

@@ -11,6 +11,7 @@ import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@@ -316,13 +317,15 @@ class StorageSyncEngine @Inject constructor(
} }
} }
} }
result.exceptionOrNull()?.let { error -> val error = result.exceptionOrNull() ?: return result.isSuccess
if (error is CancellationException) {
throw error
}
System.err.println( System.err.println(
"StorageSyncEngine: apply ${entry.operation} ${entry.path} " + "StorageSyncEngine: apply ${entry.operation} ${entry.path} " +
"target=${target.uuid}: ${error.message}", "target=${target.uuid}: ${error.message}",
) )
} return false
return result.isSuccess
} }
private fun compareEntries(a: StorageSyncJournalEntry, b: StorageSyncJournalEntry): Int { private fun compareEntries(a: StorageSyncJournalEntry, b: StorageSyncJournalEntry): Int {