# 機能設計書 24-Vercel Blob ストレージ

## 概要

本ドキュメントは、PayloadCMSにおけるVercel Blob Storageアダプターの機能設計を記述する。このアダプターはファイルアップロード機能と連携し、ファイルをVercel Blob Storageに保存・配信する機能を提供する。Vercelプラットフォーム上でのデプロイに最適化されたストレージソリューションである。

### 本機能の処理概要

Vercel Blob Storageアダプターは、PayloadCMSのアップロードコレクションで生成されたファイルをVercel Blob Storageに保存し、取得・削除を行うための包括的なストレージソリューションを提供する。Vercelのエッジネットワークと統合されており、グローバルな低レイテンシ配信が可能。

**業務上の目的・背景**：Vercelプラットフォームでホストされているアプリケーションでは、Vercel Blob Storageが最も統合度の高いストレージ選択肢となる。Vercelのサーバーレス環境では、ファイルシステムへの永続的な書き込みができないため、外部ストレージが必須である。Vercel Blobはトークンベースの認証でシンプルに設定でき、Vercelのグローバルエッジネットワークを活用した高速配信が可能。

**機能の利用シーン**：
- Vercelでホストされているアプリケーション
- サーバーレス環境でのファイル永続化が必要なケース
- Vercelのエッジネットワークを活用した高速コンテンツ配信
- シンプルなストレージ設定を求める開発者

**主要な処理内容**：
1. ファイルアップロード処理：put APIによる直接アップロード
2. ファイル削除処理：del APIによる削除
3. ファイル配信処理：Range Requestに対応した静的ファイルハンドラー
4. クライアント直接アップロード：Vercel Blob Client SDKによる直接アップロード
5. ランダムサフィックス：ファイル名衝突回避のためのランダム文字列付加

**関連システム・外部連携**：
- Vercel Blob Storage API（@vercel/blob）
- Vercelエッジネットワーク（コンテンツ配信）
- Vercel Platform（認証・デプロイ）

**権限による制御**：
- トークンベース認証（BLOB_READ_WRITE_TOKEN）
- クライアントアップロード時のアクセス制御関数によるカスタム認可
- 現在はpublicアクセスのみサポート（privateは将来対応予定）

## 関連画面

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

## 機能種別

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

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| token | string\|undefined | Yes | Vercel Blob読み書きトークン | vercel_blob_rw_*形式 |
| collections | Record<string, CollectionOptions\|true> | Yes | 適用対象コレクション設定 | 存在するコレクションスラッグ |
| access | 'public' | No | アクセス制御（デフォルト: 'public'） | 現在publicのみ |
| addRandomSuffix | boolean | No | ファイル名にランダムサフィックスを追加（デフォルト: false） | - |
| cacheControlMaxAge | number | No | Cache-Control max-age秒数（デフォルト: 1年） | - |
| clientUploads | ClientUploadsConfig | No | クライアント直接アップロード設定 | - |
| enabled | boolean | No | プラグイン有効化（デフォルト: true） | - |

### 入力データソース

- Payloadコレクション設定（payload.config.ts）
- ファイルアップロードリクエスト（multipart/form-data）
- クライアント直接アップロードリクエスト（Vercel Blob Client SDK）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| url | string | Vercel Blobオブジェクトの公開URL |
| filename | string | 保存されたファイル名（サフィックス付きの場合あり） |
| prefix | string | Blobキーのプレフィックス |
| filesize | number | ファイルサイズ（バイト） |
| mimeType | string | MIMEタイプ |

### 出力先

- Vercel Blob Storage（オブジェクトストレージ）
- HTTPレスポンス（ファイル配信、URLレスポンス）

## 処理フロー

### 処理シーケンス

```
1. プラグイン初期化
   └─ トークンからstoreIdを抽出
   └─ baseURLの構築
   └─ コレクション設定へのアダプター注入
   └─ クライアントアップロード用ルート設定

2. ファイルアップロード（サーバー経由）
   └─ ファイルデータ受信
   └─ put APIでアップロード
   └─ ランダムサフィックス付きの場合、返却URLからファイル名を更新
   └─ ドキュメントデータ返却

3. ファイルアップロード（クライアント直接）
   └─ Vercel Blob Client SDKのupload API
   └─ onBeforeGenerateTokenでアクセス権限チェック
   └─ ランダムサフィックスの適用
   └─ アップロード完了後、ファイル名を更新

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

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

### フローチャート

```mermaid
flowchart TD
    A[ファイルアップロード開始] --> B[put API呼び出し]
    B --> C{addRandomSuffix?}
    C -->|Yes| D[返却URLからファイル名を抽出]
    C -->|No| E[元のファイル名を使用]
    D --> F[data.filenameを更新]
    E --> G[アップロード完了]
    F --> G
    G --> H[ドキュメント更新]
    H --> I[終了]

    J[ファイル取得リクエスト] --> K[head APIでメタデータ取得]
    K --> L[ETag生成]
    L --> M{ETagマッチ?}
    M -->|Yes| N[304 Not Modified]
    M -->|No| O{Range Request?}
    O -->|Yes| P[Range付きfetch]
    O -->|No| Q[通常fetch]
    P --> R[ストリーミングレスポンス]
    Q --> R
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | トークン形式検証 | vercel_blob_rw_<store_id>_<random_string>形式を要求 | プラグイン初期化時 |
| BR-002 | storeIdの抽出 | トークンからstoreIdを正規表現で抽出 | プラグイン初期化時 |
| BR-003 | デフォルトキャッシュ期間 | 1年（31536000秒） | cacheControlMaxAge未指定時 |
| BR-004 | デフォルトアクセス制御 | クライアントアップロード時、認証済みユーザーのみ許可 | accessが未指定時 |
| BR-005 | ランダムサフィックス更新 | addRandomSuffix時、返却URLからファイル名を逆算して更新 | addRandomSuffix=true時 |
| BR-006 | ETag生成 | fileKey + uploadedAtで一意のETagを生成 | ファイル配信時 |

### 計算ロジック

- storeId抽出: `token.match(/^vercel_blob_rw_([a-z\d]+)_[a-z\d]+$/i)?.[1]`
- baseURL: `https://${storeId}.public.blob.vercel-storage.com`
- オブジェクトURL: `${baseUrl}/${prefix}/${encodeURIComponent(filename)}`
- ETag: `"${fileKey}-${uploadedAt.toISOString()}"`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| アップロード | uploads（コレクション名に依存） | UPDATE | filename、prefix、url等のフィールド更新 |
| 削除 | uploads（コレクション名に依存） | - | Vercel Blobオブジェクトのみ削除（DBはPayloadコアが管理） |

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

#### uploadsコレクション

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | filename | ランダムサフィックス付きファイル名（addRandomSuffix時） | URLから逆算 |
| UPDATE | prefix | クライアントアップロード時にクライアントから渡される値 | Blobキープレフィックス |
| UPDATE | url | generateURL関数で生成されたURL | 公開アクセス用URL |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| BlobNotFoundError | 404 Not Found | 存在しないBlobへのアクセス | ファイルパスの確認 |
| Invalid token format | Error | トークン形式不正 | トークン形式の確認 |
| APIError | 400 Bad Request | クライアントアップロードでcollectionSlug未指定 | リクエスト形式の確認 |
| Forbidden | 403 Forbidden | クライアントアップロードのアクセス制御違反 | 認証状態の確認 |

### リトライ仕様

Vercel Blob SDKのデフォルトリトライポリシーを使用。

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

Vercel Blob操作はトランザクション非対応のため、PayloadのDB操作とは独立して実行される。アップロード失敗時、DBへの保存も行われないため一貫性は保たれる。

## パフォーマンス要件

- Vercelエッジネットワークによるグローバル低レイテンシ配信
- Cache-Controlヘッダーによるブラウザ/CDNキャッシュ活用（デフォルト1年）
- ETagによる条件付きリクエストで帯域節約
- Range Requestによる部分コンテンツ配信対応

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

- BLOB_READ_WRITE_TOKENは環境変数で管理（コード内にハードコードしない）
- 現在はpublicアクセスのみサポート（privateは将来対応予定）
- SVGファイルには`Content-Security-Policy: script-src 'none'`を付与しXSS防止
- クライアントアップロードではアクセス制御関数でユーザー認証を確認
- トークンがない場合はプラグインが自動的に無効化され、ローカルストレージにフォールバック

## 備考

- Vercelプラットフォーム上では環境変数BLOB_READ_WRITE_TOKENが自動設定される
- addRandomSuffixを有効にするとファイル名の衝突を回避できるが、予測不能なURLになる
- privateアクセスは将来のVercel Blobアップデートで対応予定

---

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

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

### 推奨読解順序

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

Vercel Blob Storageオプションの型定義と、デフォルト値を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.ts | `packages/storage-vercel-blob/src/index.ts` | VercelBlobStorageOptions型定義（21-80行目）でプラグインオプションを把握 |
| 1-2 | index.ts | `packages/storage-vercel-blob/src/index.ts` | defaultUploadOptions（82-87行目）でデフォルト値を確認 |

**読解のコツ**: tokenパラメータは必須だが、undefinedを許容する。tokenがない場合はプラグインが無効化される。

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

vercelBlobStorage関数がプラグインのエントリーポイント。トークンからstoreIdを抽出してbaseURLを構築する。

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

**主要処理フロー**:
1. **95-97行目**: トークンからstoreIdを正規表現で抽出
2. **99行目**: プラグイン無効化条件の判定（enabled=falseまたはtoken未設定）
3. **102-106行目**: 不正なトークン形式の場合はエラー
4. **113行目**: baseURLの構築
5. **115-137行目**: クライアント直接アップロードの初期化
6. **144行目**: vercelBlobStorageInternal関数でアダプター生成

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

vercelBlobStorageInternal関数でコレクションごとのアダプターを生成する。

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

**主要処理フロー**:
- **190-192行目**: トークンの存在チェック
- **194-208行目**: GeneratedAdapterオブジェクトの生成
  - generateURL: 公開URL生成
  - handleDelete: 削除処理
  - handleUpload: アップロード処理
  - staticHandler: ファイル配信

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

ファイルのVercel Blobへのアップロード処理。put API使用、ランダムサフィックス対応。

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

**主要処理フロー**:
- **22行目**: ファイルキーの構築
- **24-30行目**: put APIでアップロード（access、addRandomSuffix、cacheControlMaxAge設定付き）
- **33-35行目**: addRandomSuffix時、返却URLからファイル名を逆算して更新

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

Vercel Blobオブジェクトの削除処理。del APIを使用。

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

**主要処理フロー**:
- **14行目**: baseURLとprefixからファイルURLを構築
- **15行目**: del API呼び出し

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

静的ファイルハンドラー。head APIでメタデータ取得後、fetchでコンテンツ取得。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 6-1 | staticHandler.ts | `packages/storage-vercel-blob/src/staticHandler.ts` | getStaticHandler関数（15-101行目） |

**主要処理フロー**:
- **25-26行目**: head APIでBlobメタデータ取得
- **28行目**: ETag生成（fileKey + uploadedAt）
- **31-39行目**: Range Requestの解析と検証
- **48行目**: Cache-Controlヘッダー設定
- **54-56行目**: SVGファイルへのCSPヘッダー付与
- **66-71行目**: ETagによる304レスポンス判定
- **73-81行目**: fetchでコンテンツ取得（Range対応）
- **89-92行目**: ストリーミングレスポンス返却

#### Step 7: クライアント直接アップロードを理解する

Vercel Blob Client SDKを使用したクライアントからの直接アップロード機能。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 7-1 | getClientUploadRoute.ts | `packages/storage-vercel-blob/src/getClientUploadRoute.ts` | サーバー側ルート処理（18-50行目） |
| 7-2 | VercelBlobClientUploadHandler.ts | `packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts` | クライアント側アップロード処理（12-42行目） |

**主要処理フロー**:
- **getClientUploadRoute.ts 24-43行目**: handleUpload APIでアップロード処理
- **getClientUploadRoute.ts 26-33行目**: onBeforeGenerateTokenでアクセス権限チェック
- **VercelBlobClientUploadHandler.ts 28-33行目**: upload APIでVercel Blobに直接アップロード
- **VercelBlobClientUploadHandler.ts 36-38行目**: addRandomSuffix時、ファイル名を更新

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

```
vercelBlobStorage (Plugin Entry)
    │
    ├─ storeId抽出（正規表現）
    │
    ├─ initClientUploads()
    │      └─ getClientUploadRoute()
    │             └─ handleUpload() [Vercel Blob SDK]
    │
    └─ vercelBlobStorageInternal()
           │
           ├─ getGenerateUrl()
           │
           ├─ getHandleDelete()
           │      └─ del() [Vercel Blob SDK]
           │
           ├─ getHandleUpload()
           │      └─ put() [Vercel Blob SDK]
           │
           └─ getStaticHandler()
                  ├─ head() [メタデータ取得]
                  └─ fetch() [コンテンツ取得]
```

### データフロー図

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

ファイルデータ ────────▶ getHandleUpload() ────────────▶ Vercel Blob
(multipart/form-data)     │
                          └─ put() [access, addRandomSuffix設定]

ファイル取得リクエスト ──▶ getStaticHandler() ──────────▶ HTTPレスポンス
(GET /api/.../file.jpg)   │                                (ストリーミング)
                          ├─ head() [メタデータ]
                          └─ fetch() [コンテンツ]

クライアントアップロード ──▶ getClientUploadRoute() ────▶ Vercel Blob
(Vercel Blob Client SDK)  │
                          └─ handleUpload() [トークン生成]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/storage-vercel-blob/src/index.ts` | ソース | プラグインエントリーポイント、VercelBlobStorageOptions型定義 |
| handleUpload.ts | `packages/storage-vercel-blob/src/handleUpload.ts` | ソース | ファイルアップロード処理（put API） |
| handleDelete.ts | `packages/storage-vercel-blob/src/handleDelete.ts` | ソース | ファイル削除処理（del API） |
| staticHandler.ts | `packages/storage-vercel-blob/src/staticHandler.ts` | ソース | 静的ファイル配信（head + fetch） |
| generateURL.ts | `packages/storage-vercel-blob/src/generateURL.ts` | ソース | 公開URL生成 |
| getClientUploadRoute.ts | `packages/storage-vercel-blob/src/getClientUploadRoute.ts` | ソース | クライアントアップロード用サーバールート |
| VercelBlobClientUploadHandler.ts | `packages/storage-vercel-blob/src/client/VercelBlobClientUploadHandler.ts` | ソース | クライアント側直接アップロード処理 |
| client.ts | `packages/storage-vercel-blob/src/exports/client.ts` | ソース | クライアントエクスポート |
