Все картинки в отчёте
@@ -74,6 +74,17 @@ Wallenc — мобильное приложение для Android: `VaultsManag
|
||||
#pz-fig("fig_09_remote_vaults.jpg", [Экран удалённых vault], "fig-09-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).
|
||||
|
||||
@@ -1,39 +1,71 @@
|
||||
# Реестр иллюстраций ПЗ Wallenc
|
||||
|
||||
Правило: файл в `Report/images/` только с именем из таблицы. Статус `ready` — можно вставлять в `.typ`.
|
||||
Правила:
|
||||
|
||||
| № | Имя файла | Статус | Где используется | Typst label |
|
||||
|---|-----------|--------|------------------|-------------|
|
||||
| 1 | fig_01_start_sync.png | ready | 3.3.3, 4.1.4 | fig-01 |
|
||||
| 2 | fig_02_vault_lifecycle.png | ready | 3.3.3, 4.1.4 | fig-02 |
|
||||
| 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 |
|
||||
- Файл в `Report/images/` — **только** имя из таблицы.
|
||||
- Статус `ready` — файл на месте и подключён в `.typ`.
|
||||
- Статус `placeholder` — файл ещё не подготовлен (сейчас все иллюстрации `ready`).
|
||||
- Диаграммы PlantUML: `cd Report && bash scripts/render_puml.sh`.
|
||||
|
||||
Диаграммы fig_01–04, fig_11, fig_14–23: исходники `Report/puml/fig_*.puml` (`@startuml` = имя PNG), растр — `Report/scripts/render_puml.sh` сразу в `Report/images/` (без копирования).
|
||||
## Соответствие «new images» → `images/`
|
||||
|
||||
Скриншоты UI (fig_05–fig_10, fig_12–13) и Gradle (fig_27–32) — заменить 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"
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 424 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 11 KiB |
BIN
Report/images/fig_33_storage_secrets_2fa.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
Report/images/fig_34_2fa_single_token.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
@@ -48,7 +48,7 @@
|
||||
|
||||
==== Работа с содержимым storage
|
||||
|
||||
Операции чтения и записи выполняются через единый интерфейс файлового доступа независимо от типа хранилища.
|
||||
Операции чтения и записи выполняются через единый интерфейс файлового доступа независимо от типа хранилища. Внутри открытого storage доступны текстовые секреты и генерация TOTP для 2FA (рис. @fig-33, @fig-34).
|
||||
|
||||
==== Удалённые хранилища и авторизация во внешних провайдерах
|
||||
|
||||
|
||||
@@ -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_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_09_remote_vaults.jpg", [Экран удалённых vault], "fig-09")
|
||||
#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")
|
||||
|
||||
== Требования к эргономике и доступности
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
[T-9], [Открытие/закрытие vault], [Ручной], [Нет], [Доступ только с ключом (рис. @fig-07)],
|
||||
[T-10], [OAuth Яндекс], [Ручной / IT], [Частично], [Токен в Room (рис. @fig-10)],
|
||||
[T-11], [Экран задач и уведомления], [Ручной], [Частично], [Прогресс и завершение (рис. 12–13)],
|
||||
[T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений],
|
||||
[T-12], [Compose: секреты и 2FA], [IT], [Да], [Отображение без падений (рис. @fig-33–@fig-34)],
|
||||
) <tbl-testplan>
|
||||
|
||||
=== Критерии начала и окончания
|
||||
@@ -113,7 +113,7 @@
|
||||
[:app], [YandexDiskLiveIntegrationTest], [Живой API (при наличии токена)], [3],
|
||||
) <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")
|
||||
|
||||
|
||||
15
Report/puml/fig_32_manual_test_checklist.puml
Normal 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
|
||||
@@ -6,11 +6,14 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_READY = {
|
||||
f"fig_{i:02d}_" for i in range(1, 5)
|
||||
} | {f"fig_11_"} | {f"fig_{i:02d}_" for i in range(14, 24)}
|
||||
REQUIRED_READY = (
|
||||
{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_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:
|
||||
|
||||
@@ -72,3 +72,5 @@
|
||||
#pz-fig("fig_11_room_schema.png", [Схема Room], "fig-11-app")
|
||||
#pz-fig("fig_12_tasks_screen.jpg", [Экран задач], "fig-12-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")
|
||||
|
||||
@@ -28,10 +28,6 @@ android {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments["yandex.oauth.token"] =
|
||||
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 {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,15 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
/**
|
||||
* Live-прогон Disk API. Пути только `app:/` — как в [YandexVault]; токен из Auth SDK
|
||||
* обычно имеет `cloud_api:disk.app_folder`, без полного доступа к `disk:/` (там 403).
|
||||
*/
|
||||
@RunWith(JUnit4::class)
|
||||
class YandexDiskLiveIntegrationTest {
|
||||
|
||||
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 probePath = "$testFolder/$probeFileName"
|
||||
private val probePayload = "wallenc-integration-probe".encodeToByteArray()
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.github.nullptroma.wallenc.app.sync
|
||||
|
||||
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.tasks.StorageSyncTriggerReason
|
||||
import com.github.nullptroma.wallenc.usecases.RunStorageSyncUseCase
|
||||
import com.github.nullptroma.wallenc.usecases.StorageSyncReadiness
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -13,13 +11,10 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -29,15 +24,11 @@ class StorageSyncBootstrap @Inject constructor(
|
||||
private val scheduler: StorageSyncScheduler,
|
||||
private val vaultsManager: IVaultsManager,
|
||||
private val syncRunner: RunStorageSyncUseCase,
|
||||
private val syncReadiness: StorageSyncReadiness,
|
||||
private val groupStore: IStorageSyncGroupStore,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val startupSyncScheduled = AtomicBoolean(false)
|
||||
|
||||
fun start() {
|
||||
scheduler.ensureScheduled()
|
||||
scheduleStartupSyncOnce()
|
||||
scope.launch {
|
||||
combine(
|
||||
vaultsManager.allStorages,
|
||||
@@ -86,31 +77,4 @@ class StorageSyncBootstrap @Inject constructor(
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -316,13 +317,15 @@ class StorageSyncEngine @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
result.exceptionOrNull()?.let { error ->
|
||||
System.err.println(
|
||||
"StorageSyncEngine: apply ${entry.operation} ${entry.path} " +
|
||||
"target=${target.uuid}: ${error.message}",
|
||||
)
|
||||
val error = result.exceptionOrNull() ?: return result.isSuccess
|
||||
if (error is CancellationException) {
|
||||
throw error
|
||||
}
|
||||
return result.isSuccess
|
||||
System.err.println(
|
||||
"StorageSyncEngine: apply ${entry.operation} ${entry.path} " +
|
||||
"target=${target.uuid}: ${error.message}",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
private fun compareEntries(a: StorageSyncJournalEntry, b: StorageSyncJournalEntry): Int {
|
||||
|
||||