# 機能設計書 25-R2 ストレージ

## 概要

本ドキュメントは、PayloadCMSにおけるCloudflare R2ストレージアダプターの機能設計を記述する。このアダプターはファイルアップロード機能と連携し、ファイルをCloudflare R2ストレージに保存・配信する機能を提供する。R2はS3互換のオブジェクトストレージで、エグレス料金無料という特徴を持つ。

### 本機能の処理概要

R2ストレージアダプターは、PayloadCMSのアップロードコレクションで生成されたファイルをCloudflare R2バケットに保存し、取得・削除を行うための包括的なストレージソリューションを提供する。Cloudflare Workers環境での動作に最適化されており、R2のBinding APIを直接使用する。

**業務上の目的・背景**：Cloudflareエコシステムを採用している企業では、R2が最適なストレージ選択肢となる。R2は他のクラウドストレージと異なりエグレス（データ送信）料金が無料であり、大量のファイル配信を行うアプリケーションでコスト削減が可能。また、CloudflareのグローバルCDNと統合されており、高速なコンテンツ配信が可能である。

**機能の利用シーン**：
- Cloudflare Workers/Pages上でホストされているアプリケーション
- エグレス料金を抑えたい大規模ファイル配信システム
- Cloudflare CDNによるグローバルコンテンツ配信
- S3互換APIを活用したマイグレーション

**主要な処理内容**：
1. ファイルアップロード処理：R2 Bucket.put APIによる直接アップロード
2. ファイル削除処理：R2 Bucket.delete APIによる削除
3. ファイル配信処理：Range Requestに対応した静的ファイルハンドラー
4. マルチパートアップロード：大容量ファイルのクライアント直接アップロード
5. 開発環境対応：Miniflare環境での動作考慮

**関連システム・外部連携**：
- Cloudflare R2 Binding API
- Cloudflare Workers/Pages
- Miniflare（ローカル開発環境）

**権限による制御**：
- R2 Bucketバインディングによるアクセス制御
- コレクションのcreateアクセス制御を参照
- カスタムアクセス制御関数によるカスタム認可

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 10 | ドキュメント編集画面 | 主画面 | ファイルアップロード時のR2保存処理 |
| 9 | 一覧画面 | 参照画面 | R2に保存されたファイルのURL表示 |

## 機能種別

外部サービス連携（クラウドストレージ）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| bucket | R2Bucket | Yes | R2バケットバインディング | Cloudflare R2Bucket型 |
| collections | Record<string, CollectionOptions\|true> | Yes | 適用対象コレクション設定 | 存在するコレクションスラッグ |
| clientUploads | ClientUploadsConfig | No | クライアント直接アップロード設定 | - |
| enabled | boolean | No | プラグイン有効化（デフォルト: true） | - |

### 入力データソース

- Payloadコレクション設定（payload.config.ts）
- Cloudflare R2バケットバインディング（wrangler.toml）
- ファイルアップロードリクエスト（multipart/form-data）
- マルチパートアップロードリクエスト（クエリパラメータ）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| key | string | R2オブジェクトキー |
| filename | string | 保存されたファイル名 |
| prefix | string | R2キーのプレフィックス |
| filesize | number | ファイルサイズ（バイト） |
| mimeType | string | MIMEタイプ |

### 出力先

- Cloudflare R2バケット（オブジェクトストレージ）
- HTTPレスポンス（ファイル配信、マルチパートアップロードレスポンス）

## 処理フロー

### 処理シーケンス

```
1. プラグイン初期化
   └─ R2アダプター生成
   └─ コレクション設定へのアダプター注入
   └─ マルチパートアップロード用ルート設定

2. ファイルアップロード（サーバー経由）
   └─ ファイルデータ受信
   └─ 開発環境（Miniflare）: Blob変換
   └─ bucket.put APIでアップロード
   └─ httpMetadata設定（contentType）

3. ファイルアップロード（クライアント直接マルチパート）
   └─ createMultipartUploadでアップロード開始
   └─ 各パートをuploadPartで送信
   └─ completeでアップロード完了

4. ファイル配信
   └─ ファイル取得リクエスト
   └─ bucket.headでメタデータ取得
   └─ Range Requestヘッダー解析
   └─ bucket.getでオブジェクト取得
   └─ ストリーミングレスポンス

5. ファイル削除
   └─ 削除リクエスト
   └─ bucket.delete API呼び出し
```

### フローチャート

```mermaid
flowchart TD
    A[ファイルアップロード開始] --> B{開発環境?}
    B -->|Yes| C[Blob変換]
    B -->|No| D[buffer使用]
    C --> E[bucket.put]
    D --> E
    E --> F[httpMetadata設定]
    F --> G[終了]

    H[クライアントマルチパート] --> I[createMultipartUpload]
    I --> J[uploadId/key取得]
    J --> K[パートループ]
    K --> L[uploadPart]
    L --> M{最終パート?}
    M -->|No| K
    M -->|Yes| N[complete]
    N --> O[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 大容量ファイル制限 | クライアントアップロードコンテキストがある場合、50MB以上のファイルは空レスポンス | 静的ハンドラー時 |
| BR-002 | マルチパートチャンクサイズ | デフォルト5MB | クライアントマルチパートアップロード時 |
| BR-003 | アクセス制御順序 | カスタムaccess > コレクションcreate access > ユーザー認証 | マルチパートアップロード時 |
| BR-004 | 開発環境対応 | NODE_ENV=developmentの場合、Blob変換を行う（Miniflare互換性） | アップロード/配信時 |
| BR-005 | Range Request制限 | Miniflare環境ではRange指定なしで全体取得 | 開発環境時 |

### 計算ロジック

- R2オブジェクトキー: `${prefix}/${filename}`
- パート数: `Math.ceil(file.size / chunkSize)`
- パートバイト範囲: `bytesStart = (part - 1) * chunkSize`, `bytesEnd = Math.min(part * chunkSize, file.size)`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| アップロード | uploads（コレクション名に依存） | - | R2オブジェクトのみ保存（DBはPayloadコアが管理） |
| 削除 | uploads（コレクション名に依存） | - | R2オブジェクトのみ削除（DBはPayloadコアが管理） |

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

R2アダプターはgenerateURL関数を持たないため、URLフィールドは直接更新されない。PayloadコアがデフォルトのURL生成を使用する。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 Not Found | Not Found | bucket.headで存在しないオブジェクト | ファイルパスの確認 |
| APIError | 400 Bad Request | 存在しないコレクションスラッグ | コレクション設定の確認 |
| Forbidden | 403 Forbidden | マルチパートアップロードのアクセス制御違反 | 認証状態・権限の確認 |
| Failed to initialize | Error | マルチパートアップロード開始失敗 | R2バケット設定の確認 |
| Failed to upload part | Error | パートアップロード失敗 | ネットワーク・R2設定の確認 |
| Failed to complete | Error | マルチパートアップロード完了失敗 | パート整合性の確認 |

### リトライ仕様

R2 Binding APIのデフォルトリトライポリシーを使用。クライアントマルチパートアップロードは個別パートごとの再試行が可能。

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

R2操作はトランザクション非対応。マルチパートアップロードは途中で失敗しても、abort操作でクリーンアップ可能（現在は未実装）。アップロード失敗時、DBへの保存も行われないため一貫性は保たれる。

## パフォーマンス要件

- R2 Binding APIは低レイテンシでアクセス可能
- 50MB以上のファイルはクライアントマルチパートで分割アップロード
- Range Requestによる部分コンテンツ配信対応（本番環境のみ）
- Cloudflare CDNによるグローバルキャッシュ

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

- R2バケットバインディングはwrangler.tomlで設定
- マルチパートアップロードではコレクションのcreateアクセス制御を参照
- SVGファイルには`Content-Security-Policy: script-src 'none'`を付与しXSS防止
- ファイル名はsanitizeFilename関数でサニタイズ
- クライアントアップロードではアクセス制御関数でユーザー認証を確認

## 備考

- R2はCloudflare Workers環境専用（Node.js単体では使用不可）
- 開発環境ではMiniflareを使用し、一部API挙動が異なる
- generateURL関数がないため、URLはPayloadコアのデフォルト生成を使用
- エグレス料金無料のため、大量配信に適している

---

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

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

### 推奨読解順序

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

R2固有の型定義を理解することが重要。R2BucketはCloudflare Workers APIに準拠。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types.ts | `packages/storage-r2/src/types.ts` | R2Bucket、R2Object、R2MultipartUpload等の型定義（10-84行目） |
| 1-2 | index.ts | `packages/storage-r2/src/index.ts` | R2StorageOptions型定義（20-42行目） |

**読解のコツ**: R2BucketはCloudflare Workers APIのバインディング型。Node.jsとWorkers両方で動作するよう、独自の型定義を使用している。

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

r2Storage関数がプラグインのエントリーポイント。R2バケットバインディングを受け取りアダプターを生成。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/storage-r2/src/index.ts` | r2Storage関数（46-116行目）でプラグイン初期化フローを理解 |

**主要処理フロー**:
1. **49行目**: r2StorageInternal関数でアダプター生成
2. **53-74行目**: クライアントマルチパートアップロードの初期化
3. **81-92行目**: 各コレクションにアダプターを注入
4. **112-115行目**: cloudStoragePluginを呼び出し設定を返却

#### Step 3: アダプター生成を理解する

r2StorageInternal関数でコレクションごとのアダプターを生成する。generateURLがない点に注意。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | index.ts | `packages/storage-r2/src/index.ts` | r2StorageInternal関数（118-132行目）でアダプター構造を把握 |

**主要処理フロー**:
- **120-130行目**: GeneratedAdapterオブジェクトの生成
  - handleDelete: 削除処理
  - handleUpload: アップロード処理
  - staticHandler: ファイル配信
  - generateURL: **未定義**（Payloadデフォルト使用）

#### Step 4: アップロード処理を理解する

ファイルのR2へのアップロード処理。開発環境（Miniflare）対応あり。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | handleUpload.ts | `packages/storage-r2/src/handleUpload.ts` | ファイルアップロード処理全体（14-22行目） |

**主要処理フロー**:
- **17行目**: 開発環境でのBlob変換（Miniflare互換性）
- **18-20行目**: bucket.put APIでアップロード（contentType設定付き）

#### Step 5: 削除処理を理解する

R2オブジェクトの削除処理。シンプルなbucket.delete呼び出し。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | handleDelete.ts | `packages/storage-r2/src/handleDelete.ts` | 削除処理全体（11-15行目） |

**主要処理フロー**:
- **13行目**: bucket.delete API呼び出し

#### Step 6: ファイル配信を理解する

静的ファイルハンドラー。Range Request対応、Miniflare環境考慮。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 6-1 | staticHandler.ts | `packages/storage-r2/src/staticHandler.ts` | getHandler関数（17-124行目） |

**主要処理フロー**:
- **15行目**: Miniflare判定（NODE_ENV === 'development'）
- **23-26行目**: bucket.headでメタデータ取得
- **31-33行目**: 50MB以上のクライアントアップロードは空レスポンス
- **36-44行目**: Range Requestの解析と検証
- **49-57行目**: Range対応取得（Miniflare環境では非対応）
- **71-91行目**: HTTPメタデータのヘッダー設定（Miniflare対応）
- **94-97行目**: SVGファイルへのCSPヘッダー付与
- **109-114行目**: ETagによる304レスポンス判定

#### Step 7: マルチパートアップロードを理解する

大容量ファイルのクライアント直接アップロード機能。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 7-1 | handleMultiPartUpload.ts | `packages/storage-r2/src/handleMultiPartUpload.ts` | サーバー側マルチパート処理（18-91行目） |
| 7-2 | R2ClientUploadHandler.ts | `packages/storage-r2/src/client/R2ClientUploadHandler.ts` | クライアント側マルチパート処理（14-82行目） |

**主要処理フロー**:
- **handleMultiPartUpload.ts 30-52行目**: アクセス制御（カスタム > コレクション > ユーザー）
- **handleMultiPartUpload.ts 62-77行目**: 既存アップロード継続（resume/uploadPart/complete）
- **handleMultiPartUpload.ts 79-89行目**: 新規アップロード開始（createMultipartUpload）
- **R2ClientUploadHandler.ts 36行目**: マルチパート開始
- **R2ClientUploadHandler.ts 49-65行目**: パートごとにアップロード
- **R2ClientUploadHandler.ts 67-78行目**: アップロード完了

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

```
r2Storage (Plugin Entry)
    │
    ├─ initClientUploads()
    │      └─ getHandleMultiPartUpload()
    │             ├─ bucket.createMultipartUpload()
    │             ├─ bucket.resumeMultipartUpload()
    │             │      ├─ uploadPart()
    │             │      └─ complete()
    │             └─ アクセス制御チェック
    │
    └─ r2StorageInternal()
           │
           ├─ getHandleDelete()
           │      └─ bucket.delete()
           │
           ├─ getHandleUpload()
           │      └─ bucket.put() [Blob変換（開発環境）]
           │
           └─ getHandler()
                  ├─ bucket.head() [メタデータ]
                  └─ bucket.get() [ストリーミング]
```

### データフロー図

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

ファイルデータ ────────▶ getHandleUpload() ────────────▶ R2バケット
(Workers環境)             │
                          ├─ 開発環境: Blob変換
                          └─ bucket.put()

マルチパートリクエスト ──▶ getHandleMultiPartUpload() ──▶ R2バケット
(クライアント分割)        │
                          ├─ createMultipartUpload()
                          ├─ uploadPart() x N
                          └─ complete()

ファイル取得リクエスト ──▶ getHandler() ────────────────▶ HTTPレスポンス
(GET /api/.../file.jpg)   │                                (ストリーミング)
                          ├─ bucket.head()
                          └─ bucket.get()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/storage-r2/src/index.ts` | ソース | プラグインエントリーポイント、R2StorageOptions型定義 |
| types.ts | `packages/storage-r2/src/types.ts` | ソース | R2 API互換の型定義 |
| handleUpload.ts | `packages/storage-r2/src/handleUpload.ts` | ソース | ファイルアップロード処理（bucket.put） |
| handleDelete.ts | `packages/storage-r2/src/handleDelete.ts` | ソース | ファイル削除処理（bucket.delete） |
| staticHandler.ts | `packages/storage-r2/src/staticHandler.ts` | ソース | 静的ファイル配信（Range Request対応） |
| handleMultiPartUpload.ts | `packages/storage-r2/src/handleMultiPartUpload.ts` | ソース | マルチパートアップロードサーバー処理 |
| R2ClientUploadHandler.ts | `packages/storage-r2/src/client/R2ClientUploadHandler.ts` | ソース | クライアント側マルチパートアップロード処理 |
| client.ts | `packages/storage-r2/src/exports/client.ts` | ソース | クライアントエクスポート |
