# 機能設計書 23-クラウド同期

## 概要

本ドキュメントは、Actual Budget におけるクラウド同期機能の設計仕様を記述する。この機能は、複数デバイス間で予算データを同期し、どのデバイスからでも最新の状態でアクセスできることを目的とする。

### 本機能の処理概要

クラウド同期機能は、ユーザーの予算データをサーバーと同期し、PC、スマートフォン、タブレットなど複数のデバイスから同一の予算データにアクセスできるようにする。同期はProtocol Buffers形式のメッセージを使用し、差分同期によりネットワーク帯域を効率的に使用する。

**業務上の目的・背景**：現代のユーザーは複数のデバイスを使用することが一般的であり、家計簿データはどのデバイスからでもアクセス・編集できる必要がある。また、データの紛失を防ぐためのバックアップとしても機能する。ローカルファーストアーキテクチャを採用し、オフライン時でも操作を継続できる。

**機能の利用シーン**：
- 外出先でスマートフォンから支出を記録し、帰宅後PCで詳細分析する
- 複数の家族メンバーが同じ予算データを共有・編集する
- デバイスの買い替え時にデータを新しいデバイスに移行する
- バックアップとしてクラウドにデータを保存する

**主要な処理内容**：
1. ファイルアップロード：予算ファイルをサーバーにアップロード
2. ファイルダウンロード：サーバーから予算ファイルをダウンロード
3. 差分同期：CRDTベースのMerkle Tree差分同期
4. ファイル一覧取得：ユーザーの予算ファイル一覧を取得
5. 暗号化キー管理：E2E暗号化用のキー作成・取得

**関連システム・外部連携**：
- 同期サーバー（sync-server）：データの保存・配信を担当
- CRDT（Conflict-free Replicated Data Type）：競合解決なしのデータ型
- Protocol Buffers：メッセージシリアライズ形式

**権限による制御**：
- ファイルの所有者とアクセス権を持つユーザーのみがファイルにアクセス可能
- ファイル削除は所有者またはサーバー管理者のみ

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | 予算ファイル選択画面 | 主画面 | 既存予算ファイルの一覧表示・選択処理 |
| 6 | サーバー設定画面 | 補助画面 | サーバーURL設定・接続設定の変更処理 |

## 機能種別

データ連携 / CRUD操作

## 入力仕様

### 入力パラメータ

#### sync（差分同期）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| fileId | string | Yes | 予算ファイルID | UUID形式 |
| groupId | string | Yes | 同期グループID | UUID形式 |
| keyId | string | No | 暗号化キーID | 暗号化有効時必須 |
| since | string | Yes | 同期開始タイムスタンプ | Merkle Treeキー形式 |
| messages | Message[] | No | 送信する変更メッセージ | Protocol Buffers形式 |

#### upload-user-file

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| x-actual-name | header | Yes | ファイル名 | URLエンコード |
| x-actual-file-id | header | Yes | ファイルID | UUID形式 |
| x-actual-group-id | header | No | グループID | 新規時は自動生成 |
| x-actual-encrypt-meta | header | No | 暗号化メタ情報 | JSON形式 |
| x-actual-format | header | No | 同期フォーマットバージョン | - |
| body | binary | Yes | ファイルデータ | サイズ制限あり |

### 入力データソース

- クライアントのローカルデータベース
- サーバーの同期グループファイル
- ユーザーセッション情報

## 出力仕様

### 出力データ

#### sync（差分同期）レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| merkle | string | 更新後のMerkle Tree（JSON文字列） |
| messages | Message[] | 受信した変更メッセージ |

#### list-user-files レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| fileId | string | ファイルID |
| groupId | string | グループID |
| name | string | ファイル名 |
| deleted | number | 削除フラグ（0/1） |
| encryptKeyId | string | 暗号化キーID |
| owner | string | 所有者ID |
| usersWithAccess | object[] | アクセス権を持つユーザー一覧 |

### 出力先

- クライアント：同期されたデータの反映
- サーバー：予算ファイル、同期グループファイルの更新

## 処理フロー

### 処理シーケンス

```
1. 同期開始
   └─ クライアントがローカルの変更をメッセージとしてパッケージ化
   └─ Merkle Treeのsinceタイムスタンプを取得
   └─ Protocol Buffers形式でリクエストを送信

2. サーバー処理
   └─ セッション認証の検証
   └─ ファイルの存在確認
   └─ グループID、暗号化キーIDの検証
   └─ simpleSync.sync()で差分同期を実行
   └─ 更新されたMerkle Treeと新規メッセージを返却

3. クライアント処理
   └─ 受信したメッセージをローカルに適用
   └─ Merkle Treeを更新
   └─ UIに変更を反映
```

### フローチャート

```mermaid
flowchart TD
    A[同期リクエスト] --> B{セッション有効?}
    B -->|No| C[401 Unauthorized]
    B -->|Yes| D{ファイル存在?}
    D -->|No| E[400 file-not-found]
    D -->|Yes| F{since指定?}
    F -->|No| G[422 since-required]
    F -->|Yes| H{groupId/keyId検証}
    H -->|失敗| I[400 エラーレスポンス]
    H -->|成功| J[simpleSync.sync実行]
    J --> K[Merkle Tree更新]
    K --> L[新規メッセージ取得]
    L --> M[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-23-1 | sinceパラメータ必須 | 差分同期にはsinceパラメータが必須 | sync API呼び出し時 |
| BR-23-2 | グループID整合性 | 既存ファイルはgroupIdが一致する必要がある | アップロード時 |
| BR-23-3 | 暗号化キー整合性 | 暗号化ファイルはkeyIdが一致する必要がある | sync時 |
| BR-23-4 | ファイルサイズ制限 | アップロードファイルはサイズ制限あり | upload時 |
| BR-23-5 | 削除権限 | ファイル削除は所有者または管理者のみ | delete時 |

### 計算ロジック

**Merkle Tree差分検出**:
- クライアントとサーバーのMerkle Treeを比較
- ハッシュが異なるノードを特定
- 差分が発生したタイムスタンプを算出
- そのタイムスタンプ以降のメッセージのみを同期

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ファイル一覧取得 | files | SELECT | ユーザーのファイル一覧取得 |
| ファイル情報取得 | files | SELECT | 特定ファイルの詳細取得 |
| ファイル作成 | files | INSERT | 新規ファイルの登録 |
| ファイル更新 | files | UPDATE | ファイル情報の更新 |
| ファイル削除 | files | UPDATE | 論理削除（deleted=1） |
| 同期データ保存 | 同期グループファイル | ファイルI/O | メッセージの永続化 |

### テーブル別操作詳細

#### files（サーバー側accountDb）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | ファイルID | UUID |
| INSERT | group_id | グループID | 同期グループ識別子 |
| INSERT | sync_version | フォーマットバージョン | - |
| INSERT | name | ファイル名 | - |
| INSERT | encrypt_meta | 暗号化メタ情報 | JSON形式 |
| INSERT | owner | 所有者ID | ユーザーID |
| UPDATE | deleted | 1 | 削除時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| file-not-found | クライアントエラー | 指定ファイルが存在しない | 正しいファイルIDを指定 |
| since-required | バリデーションエラー | sinceパラメータ未指定 | sinceパラメータを指定 |
| key-mismatch | 整合性エラー | 暗号化キーIDが不一致 | 正しいキーでアクセス |
| group-mismatch | 整合性エラー | グループIDが不一致 | 同期リセットを実行 |
| forbidden | 権限エラー | ファイルへのアクセス権がない | 所有者に権限付与を依頼 |

### リトライ仕様

- ネットワークエラー時はクライアント側で自動リトライ
- 認証エラー時は再ログインが必要

## トランザクション仕様

- サーバー側の同期処理はファイルレベルでロック
- 複数クライアントからの同時同期はCRDTにより自動マージ

## パフォーマンス要件

- 同期レスポンス時間：通常1秒以内（ネットワーク状況による）
- ファイルアップロード：最大20MB（設定により変更可能）
- 暗号化ファイルアップロード：最大50MB

## セキュリティ考慮事項

- 全通信はHTTPS（SSL/TLS）で暗号化
- セッショントークンによる認証
- E2E暗号化オプションによるサーバーでの読み取り不可
- ファイルアクセス権の厳格な管理

## 備考

- 同期サーバーはセルフホスト可能
- 同期フォーマットは将来のバージョンアップに対応
- オフライン時の変更はローカルに蓄積され、オンライン復帰時に同期

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

同期に使用されるデータ構造（Merkle Tree、タイムスタンプ）を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | timestamp.ts | `packages/crdt/src/crdt/timestamp.ts` | HULCタイムスタンプの構造、send/recv関数 |
| 1-2 | merkle.ts | `packages/crdt/src/crdt/merkle.ts` | Merkle Treeの構造、diff関数、insert関数 |

**読解のコツ**:
- `Timestamp`クラス（94-336行目）でタイムスタンプの形式（ISO日時-カウンタ-ノードID）を理解
- `Timestamp.send()`（195-230行目）でクライアント側のタイムスタンプ生成
- `Timestamp.recv()`（235-292行目）でサーバーからのタイムスタンプ受信処理
- `diff`関数（merkle.ts 78-139行目）でMerkle Tree間の差分検出アルゴリズムを確認

#### Step 2: サーバー側APIを理解する

同期サーバーのAPIエンドポイントを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | app-sync.ts | `packages/sync-server/src/app-sync.ts` | 全同期APIエンドポイント |

**主要処理フロー**:
1. **71-127行目**: `/sync` - 差分同期のメインエンドポイント
   - Protocol Buffers形式のリクエスト解析
   - ファイル存在確認、暗号化キー検証
   - `simpleSync.sync()`呼び出し
2. **202-294行目**: `/upload-user-file` - ファイルアップロード
3. **296-320行目**: `/download-user-file` - ファイルダウンロード
4. **335-353行目**: `/list-user-files` - ファイル一覧取得
5. **394-430行目**: `/delete-user-file` - ファイル削除

#### Step 3: 同期ロジックを理解する

CRDT同期の実装を読み解く。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | sync-simple.ts | `packages/sync-server/src/sync-simple.ts` | シンプル同期の実装（推測） |

**読解のコツ**:
- simpleSync.sync()関数で差分同期の実際の処理を確認
- メッセージの永続化と読み出しロジック

#### Step 4: クライアント側同期を理解する

クライアント側の同期呼び出しを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | methods.ts | `packages/api/methods.ts` | sync() API関数 |
| 4-2 | sync.ts | `packages/loot-core/src/server/sync/` | クライアント側同期処理 |

**主要処理フロー**:
- **57-59行目（methods.ts）**: `sync()` - 同期APIの呼び出し

### プログラム呼び出し階層図

```
Client                                    Server
  │                                         │
  ├─ sync() ──────────────────────────────▶ POST /sync
  │     │                                   │
  │     ├─ Protocol Buffers encode          ├─ validateSessionMiddleware
  │     │                                   │
  │     ├─ local changes to messages        ├─ SyncRequest deserialize
  │     │                                   │
  │     └─ Merkle Tree since               ├─ verifyFileExists()
  │                                         │
  │                                         ├─ validateSyncedFile()
  │                                         │
  │                                         ├─ simpleSync.sync()
  │                                         │     ├─ merge messages
  │                                         │     ├─ update Merkle Tree
  │                                         │     └─ get new messages
  │                                         │
  │ ◀────────────────────────────────────── ├─ SyncResponse serialize
  │                                         │
  ├─ apply received messages                │
  │                                         │
  └─ update local Merkle Tree               │
```

### データフロー図

```
[クライアント]                [サーバー]

ローカル変更         POST /sync
    │                   │
    ▼                   ▼
┌─────────────┐   ┌─────────────┐
│ Messages   │──▶│ sync API    │
│ Merkle Tree│   │             │
└─────────────┘   └─────────────┘
                        │
                        ▼
                  ┌─────────────┐
                  │ simpleSync  │
                  │   .sync()   │
                  └─────────────┘
                        │
           ┌────────────┼────────────┐
           ▼            ▼            ▼
     ┌──────────┐ ┌──────────┐ ┌──────────┐
     │ Merkle  │ │ Messages │ │ Group   │
     │ diff    │ │ merge    │ │ file    │
     └──────────┘ └──────────┘ └──────────┘
                        │
                        ▼
                  ┌─────────────┐
                  │ Response    │──▶ クライアント反映
                  │ (new msgs)  │
                  └─────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| app-sync.ts | `packages/sync-server/src/app-sync.ts` | ソース | 同期APIエンドポイント |
| sync-simple.ts | `packages/sync-server/src/sync-simple.ts` | ソース | シンプル同期ロジック |
| timestamp.ts | `packages/crdt/src/crdt/timestamp.ts` | ソース | HULCタイムスタンプ |
| merkle.ts | `packages/crdt/src/crdt/merkle.ts` | ソース | Merkle Tree操作 |
| sync_pb.ts | `packages/crdt/src/proto/sync_pb.ts` | ソース | Protocol Buffers定義 |
| files-service.ts | `packages/sync-server/src/app-sync/services/files-service.ts` | ソース | ファイル管理サービス |
| methods.ts | `packages/api/methods.ts` | ソース | クライアントAPI |
| validate-user.ts | `packages/sync-server/src/util/validate-user.ts` | ソース | ユーザー認証検証 |
