# 帳票設計書 3-Etherpadエクスポート

## 概要

本ドキュメントは、Etherpadのパッド（共同編集ドキュメント）をEtherpad独自のJSON形式でエクスポートする機能に関する設計書である。リビジョン履歴、チャットメッセージ、著者情報を含む完全なパッドデータをバックアップまたは移行用に取得するための仕様を定義する。

### 本帳票の処理概要

Etherpadエクスポート機能は、パッドに関連する全てのデータ（リビジョン履歴、チャットメッセージ、著者情報、プラグイン固有データ）をJSON形式で一括エクスポートし、他のEtherpadインスタンスへのインポートや完全バックアップとして利用可能なファイルを生成する処理を行う。

**業務上の目的・背景**：Etherpadを運用する上で、パッドデータの完全なバックアップや他サーバーへの移行が必要になる場合がある。HTML/TXTエクスポートではリビジョン履歴やチャット履歴が失われるが、Etherpad形式エクスポートではこれらを含む完全なデータを保持できる。災害復旧、サーバー移行、データアーカイブなどの用途で重要な機能である。

**帳票の利用シーン**：サーバー管理者がパッドデータの完全バックアップを取得したい場合、別のEtherpadインスタンスにパッドを移行したい場合、パッドの履歴を含めたアーカイブを作成したい場合に利用される。また、開発者がパッドデータの構造を分析する際のデバッグ用途でも使用される。

**主要な出力内容**：
1. パッドメタデータ（ID、現在のテキスト、属性プール）
2. 全リビジョン履歴（changeset、メタデータ）
3. 全チャットメッセージ（送信者、テキスト、タイムスタンプ）
4. 著者情報（著者ID、名前、カラー）
5. プラグイン固有のカスタムデータ

**帳票の出力タイミング**：ユーザーがパッドページにアクセスし、エクスポートメニューからEtherpad形式を選択した時点で即座に生成・ダウンロードされる。履歴を含むため、リビジョン指定エクスポートは不可。

**帳票の利用者**：パッドにアクセス権限を持つ全てのユーザーが利用可能。ただし、履歴・著者情報を含むため、実運用ではアクセス制御に注意が必要。

## 帳票種別

ドキュメントエクスポート（バックアップ/移行用JSON形式）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| - | パッド編集画面 | `/p/:pad/export/etherpad` | エクスポートメニューから「Etherpad」を選択 |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | JSON（Etherpad独自形式） |
| 用紙サイズ | 該当なし |
| 向き | 該当なし |
| ファイル名 | `{padId}.etherpad` または `{readOnlyId}.etherpad` |
| 出力方法 | ダウンロード（`Content-Disposition: attachment`） |
| 文字コード | UTF-8 |
| MIMEタイプ | application/json |

### JSON構造

```json
{
  "pad:{padId}": {
    "atext": { "text": "...", "attribs": "..." },
    "pool": { "numToAttrib": {...}, "nextNum": N },
    "head": N,
    "chatHead": N
  },
  "pad:{padId}:revs:0": { "changeset": "...", "meta": {...} },
  "pad:{padId}:revs:1": { ... },
  ...
  "pad:{padId}:chat:0": { "text": "...", "userId": "...", "time": N },
  ...
  "globalAuthor:{authorId}": { "name": "...", "colorId": "..." }
}
```

## 帳票レイアウト

### レイアウト概要

JSONオブジェクトとしてフラットなキー・値構造で出力される。

```
{
┌─────────────────────────────────────┐
│ "pad:{padId}": パッドメタ情報         │
│   - atext: 現在のテキスト+属性        │
│   - pool: 属性プール                 │
│   - head: 最新リビジョン番号          │
│   - chatHead: 最新チャット番号        │
├─────────────────────────────────────┤
│ "pad:{padId}:revs:N": リビジョンN     │
│   - changeset: 変更セット            │
│   - meta: メタデータ                 │
├─────────────────────────────────────┤
│ "pad:{padId}:chat:N": チャットN       │
│   - text: メッセージ本文             │
│   - userId: 送信者ID                │
│   - time: タイムスタンプ             │
├─────────────────────────────────────┤
│ "globalAuthor:{authorId}": 著者情報   │
│   - name: 著者名                    │
│   - colorId: カラーID               │
│   - padIDs: 関連パッドID             │
└─────────────────────────────────────┘
}
```

### パッドメタデータ（pad:{padId}）

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | atext.text | 現在のテキスト内容 | pad.atext.text | 文字列 |
| 2 | atext.attribs | 現在の属性情報 | pad.atext.attribs | 操作文字列 |
| 3 | pool | 属性プール | pad.pool | オブジェクト |
| 4 | head | 最新リビジョン番号 | pad.head | 数値 |
| 5 | chatHead | 最新チャットメッセージ番号 | pad.chatHead | 数値 |

### リビジョン情報（pad:{padId}:revs:{N}）

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | changeset | 変更セット | pad.getRevision(N) | 文字列 |
| 2 | meta | リビジョンメタデータ | pad.getRevision(N) | オブジェクト |

### チャットメッセージ（pad:{padId}:chat:{N}）

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | text | メッセージ本文 | pad.getChatMessage(N) | 文字列 |
| 2 | userId | 送信者ID | pad.getChatMessage(N) | 文字列 |
| 3 | time | 送信時刻 | pad.getChatMessage(N) | UNIXタイムスタンプ |

### 著者情報（globalAuthor:{authorId}）

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | name | 著者名 | authorManager.getAuthor | 文字列 |
| 2 | colorId | カラーID | authorManager.getAuthor | 文字列 |
| 3 | padIDs | 関連パッドID | 書き換え（readOnlyId優先） | 文字列 |

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| パッドID | エクスポート対象のパッド識別子 | Yes |
| アクセス権限 | パッドへの読み取り権限 | Yes |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| 1 | リビジョン番号 | 昇順（0からhead） |
| 2 | チャット番号 | 昇順（0からchatHead） |

### データ取得範囲

- リビジョン: 0 から pad.head まで全て
- チャット: 0 から pad.chatHead まで全て
- 著者: パッドに関連する全著者

## データベース参照仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 結合条件 |
|-----------|------|---------|
| pad:{padId} | パッドメタ情報 | キー直接参照 |
| pad:{padId}:revs:{N} | リビジョン履歴 | 0〜head全取得 |
| pad:{padId}:chat:{N} | チャットメッセージ | 0〜chatHead全取得 |
| globalAuthor:{authorId} | 著者情報 | pad.getAllAuthors() |
| {customPrefix}:{padId}:* | プラグインデータ | exportEtherpadAdditionalContentフック |

### テーブル別参照項目詳細

#### pad:{padId}

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| atext | 現在のテキスト+属性 | 常時 | - |
| pool | 属性プール | 常時 | - |
| head | 最新リビジョン番号 | 常時 | - |
| chatHead | 最新チャット番号 | 常時 | - |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| 出力キー（pad） | `pad:${readOnlyId \|\| padId}` | - | readOnlyId優先 |
| 出力キー（リビジョン） | `pad:${id}:revs:${i}` | - | i: 0〜head |
| 出力キー（チャット） | `pad:${id}:chat:${i}` | - | i: 0〜chatHead |
| バッチサイズ | 100 | - | Stream.batch(100) |

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[エクスポートリクエスト<br>/p/:pad/export/etherpad] --> B{ファイル形式チェック}
    B -->|etherpad| C[アクセス権限確認]
    B -->|サポート外| Z[404エラー]
    C -->|権限なし| Y[アクセス拒否]
    C -->|権限あり| D{readOnlyId?}
    D -->|Yes| E[readOnlyIdからpadId取得]
    D -->|No| F[padIdを使用]
    E --> G[パッド存在確認]
    F --> G
    G -->|存在しない| X[404エラー]
    G -->|存在する| H[getPadRaw実行]
    H --> I[パッドオブジェクト取得]
    I --> J[exportEtherpadAdditionalContent<br>フック実行]
    J --> K[著者情報収集]
    K --> L[リビジョン履歴収集<br>0〜head]
    L --> M[チャットメッセージ収集<br>0〜chatHead]
    M --> N[プラグインデータ収集]
    N --> O[バッチ処理で<br>Promise解決]
    O --> P[exportEtherpadフック実行]
    P --> Q[JSONオブジェクト構築]
    Q --> R[Content-Disposition設定]
    R --> S[HTTPレスポンス送信]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| パッド不存在 | 指定されたpadIdが存在しない | 404 Not Found | ログ出力後、404レスポンス |
| アクセス拒否 | パッドへの読み取り権限がない | アクセス権に応じた処理 | hasPadAccess関数で判定 |
| 著者情報なし | 著者がDB上に存在しない | - | undefinedを返す（JSON上ではキー省略） |
| アサーションエラー | プラグインプレフィックスに*が含まれる | AssertionError | 開発者向けエラー |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | 1パッド（リビジョン数+チャット数に依存） |
| 目標出力時間 | 数秒〜数十秒（履歴量に依存） |
| 同時出力数上限 | レートリミッター設定による（settings.importExportRateLimiting） |
| バッチ処理 | 100件ずつPromise解決 |
| バッファリング | 99件分のバッファ |

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

- **アクセス制御**: hasPadAccess()によるパッド単位のアクセス権限チェック
- **レートリミッター**: DoS攻撃防止のためエクスポートリクエスト数を制限
- **著者情報の露出**: 著者名・カラーが含まれるため、プライバシーに注意
- **履歴情報の露出**: 削除されたテキストもリビジョン履歴に残る可能性がある
- **readOnlyIdの使用**: 出力時にpadIdをreadOnlyIdに置き換え、元のpadId露出を防止

## 備考

- Etherpad形式は他のEtherpadインスタンスへのインポートに対応
- プラグインフック`exportEtherpadAdditionalContent`によりカスタムデータ追加可能
- プラグインフック`exportEtherpad`により最終データの加工が可能
- Stream.batch(100).buffer(99)によるバッチ処理で大量データに対応
- リビジョン指定エクスポートは不可（常に全履歴が出力される）

---

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

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

### 推奨読解順序

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

Etherpadエクスポートでは、パッドの全データ構造を理解する必要がある。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | PadType.ts | `src/node/types/PadType.ts` | PadType型定義、head、chatHead、getAllAuthors |
| 1-2 | Pad.ts | `src/node/db/Pad.ts` | getRevision、getChatMessage メソッド |

**読解のコツ**: Etherpadのデータはキー・値ストアで管理され、`pad:{id}:revs:{n}`形式でリビジョンが保存される。

#### Step 2: エントリーポイントを理解する

HTTPリクエストからエクスポート処理への流れを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | importexport.ts | `src/node/hooks/express/importexport.ts` | ルーティング定義 |
| 2-2 | ExportHandler.ts | `src/node/handler/ExportHandler.ts` | `type === 'etherpad'` 分岐 |

**主要処理フロー**:
1. **69-71行目**: etherpadの場合はexportEtherpad.getPadRawを呼び出し
2. **70行目**: padとreadOnlyIdを引数に渡す
3. **71行目**: res.send(pad) でJSONレスポンス

#### Step 3: エクスポートロジックを理解する

ExportEtherpad.tsがEtherpadエクスポートのコアロジック。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | ExportEtherpad.ts | `src/node/utils/ExportEtherpad.ts` | getPadRaw関数 |
| 3-2 | Stream.ts | `src/node/utils/Stream.ts` | バッチ処理ユーティリティ |

**主要処理フロー**:
- **24-64行目**: getPadRaw - 全データ収集ロジック
- **25行目**: dstPfx（出力キープレフィックス）の計算
- **26-29行目**: パッド取得とプラグインフック
- **30-42行目**: プラグインカスタムデータの収集
- **43-55行目**: ジェネレータ関数でデータ列挙
- **52行目**: リビジョン `0〜pad.head` の反復
- **53行目**: チャット `0〜pad.chatHead` の反復
- **57行目**: バッチ処理 `batch(100).buffer(99)`
- **58-62行目**: exportEtherpadフック実行

#### Step 4: 著者情報取得を理解する

著者情報はauthorManagerから取得される。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | AuthorManager.ts | `src/node/db/AuthorManager.ts` | getAuthor関数 |

**主要処理フロー**:
- **44-50行目**: 著者データ取得と加工
- **48行目**: padIDsをreadOnlyIdまたはpadIdに書き換え

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

```
HTTPリクエスト /p/:pad/export/etherpad
    │
    ├─ importexport.ts (expressCreateServer)
    │      └─ args.app.get('/p/:pad{/:rev}/export/:type')
    │             │
    │             ├─ hasPadAccess() - アクセス権確認
    │             ├─ readOnlyManager.getPadId() - ID解決
    │             ├─ padManager.doesPadExists() - 存在確認
    │             └─ exportHandler.doExport()
    │
    └─ ExportHandler.ts (doExport)
           │
           ├─ hooks.aCallFirst('exportFileName') - ファイル名フック
           ├─ res.attachment() - ダウンロード設定
           │
           └─ (type === 'etherpad') の場合:
                  │
                  └─ ExportEtherpad.ts
                         │
                         └─ getPadRaw()
                                ├─ padManager.getPad()
                                ├─ hooks.aCallAll('exportEtherpadAdditionalContent')
                                ├─ pad.getAllAuthors() → authorManager.getAuthor()
                                ├─ pad.getRevision(i) [0〜head]
                                ├─ pad.getChatMessage(i) [0〜chatHead]
                                ├─ プラグインカスタムデータ取得
                                ├─ Stream.batch(100).buffer(99) - バッチ処理
                                └─ hooks.aCallAll('exportEtherpad') - 最終フック
```

### データフロー図

```
[入力]                     [処理]                           [出力]

pad:{padId}            ┌────────────────┐
  │                    │                │
  ├─ atext ───────────▶│                │
  ├─ pool ────────────▶│  getPadRaw()   │
  ├─ head ────────────▶│                │
  └─ chatHead ────────▶│                │
                       │  ┌──────────┐  │
pad:{padId}:revs:N ───▶│  │リビジョン │  │
  (N: 0〜head)         │  │収集      │  │
                       │  └──────────┘  │
                       │  ┌──────────┐  │
pad:{padId}:chat:N ───▶│  │チャット  │  │──▶ JSONオブジェクト
  (N: 0〜chatHead)     │  │収集      │  │     (.etherpad)
                       │  └──────────┘  │
                       │  ┌──────────┐  │
globalAuthor:{id} ────▶│  │著者情報  │  │
                       │  │収集      │  │
                       │  └──────────┘  │
                       │  ┌──────────┐  │
プラグインデータ ─────▶│  │カスタム  │  │
                       │  │データ   │  │
                       │  └──────────┘  │
                       └────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| importexport.ts | `src/node/hooks/express/importexport.ts` | ソース | Expressルーティング定義 |
| ExportHandler.ts | `src/node/handler/ExportHandler.ts` | ソース | エクスポート処理の振り分け |
| ExportEtherpad.ts | `src/node/utils/ExportEtherpad.ts` | ソース | Etherpadエクスポートロジック本体 |
| Stream.ts | `src/node/utils/Stream.ts` | ソース | バッチ処理ユーティリティ |
| PadType.ts | `src/node/types/PadType.ts` | 型定義 | パッドデータ型 |
| Pad.ts | `src/node/db/Pad.ts` | ソース | パッドクラス（getRevision等） |
| PadManager.ts | `src/node/db/PadManager.ts` | ソース | パッドデータ取得 |
| AuthorManager.ts | `src/node/db/AuthorManager.ts` | ソース | 著者情報取得 |
| Settings.ts | `src/node/utils/Settings.ts` | 設定 | レートリミッター設定等 |
