# 機能設計書 21-S3ストレージ

## 概要

本ドキュメントは、PayloadCMSにおけるAmazon S3互換ストレージアダプターの機能設計を記述する。このアダプターはファイルアップロード機能と連携し、ファイルをAmazon S3またはS3互換ストレージサービス（MinIO、DigitalOcean Spacesなど）に保存・配信する機能を提供する。

### 本機能の処理概要

S3ストレージアダプターは、PayloadCMSのアップロードコレクションで生成されたファイルをAmazon S3バケットに保存し、取得・削除を行うための包括的なストレージソリューションを提供する。

**業務上の目的・背景**：多くのWebアプリケーションでは、ユーザーがアップロードする画像やドキュメントなどのファイルを効率的に管理する必要がある。ローカルファイルシステムへの保存は、スケーラビリティ、冗長性、CDN連携の観点で限界がある。S3ストレージを使用することで、高い耐久性（99.999999999%）、グローバルCDN連携、コスト効率の良いストレージ階層化が可能となる。

**機能の利用シーン**：
- 大量のメディアファイル（画像、動画、PDF）を管理するメディアライブラリ
- 複数サーバー環境でのファイル共有が必要な分散システム
- CloudFront等のCDNと連携した高速コンテンツ配信
- サーバーレス環境（Vercel、AWS Lambda）でのファイル永続化

**主要な処理内容**：
1. ファイルアップロード処理：50MB未満は単一PUT、50MB以上はマルチパートアップロード
2. ファイル削除処理：S3オブジェクトの削除
3. ファイル配信処理：Range Requestに対応した静的ファイルハンドラー
4. 署名付きURL生成：プライベートファイルへの一時的なアクセス許可
5. クライアント直接アップロード：サーバーを経由せずS3に直接アップロード

**関連システム・外部連携**：
- AWS S3 API（@aws-sdk/client-s3）
- S3互換ストレージ（MinIO、DigitalOcean Spaces、Cloudflare R2等）
- CloudFront等のCDNサービス

**権限による制御**：
- ACL設定（private/public-read）によるオブジェクトレベルの公開設定
- 署名付きURLによる一時的なアクセス制御
- クライアントアップロード時のアクセス制御関数によるカスタム認可

## 関連画面

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

## 機能種別

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

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| bucket | string | Yes | S3バケット名 | AWS命名規則準拠 |
| config | S3ClientConfig | Yes | AWS SDK設定（credentials、region、endpoint等） | AWS SDK仕様に準拠 |
| collections | Record<string, CollectionOptions\|true> | Yes | 適用対象コレクション設定 | 存在するコレクションスラッグ |
| acl | 'private'\|'public-read' | No | アクセス制御リスト | - |
| disableLocalStorage | boolean | No | ローカルストレージ無効化（デフォルト: true） | - |
| signedDownloads | SignedDownloadsConfig | No | 署名付きダウンロード設定 | - |
| clientUploads | ClientUploadsConfig | No | クライアント直接アップロード設定 | - |
| clientCacheKey | string | No | S3クライアントキャッシュキー | - |

### 入力データソース

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

## 出力仕様

### 出力データ

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

### 出力先

- Amazon S3バケット（オブジェクトストレージ）
- HTTPレスポンス（ファイル配信、URLレスポンス）

## 処理フロー

### 処理シーケンス

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

2. ファイルアップロード（サーバー経由）
   └─ ファイルデータ受信
   └─ 50MB未満: putObject API
   └─ 50MB以上: マルチパートアップロード
   └─ ドキュメントデータ返却

3. ファイルアップロード（クライアント直接）
   └─ 署名付きURL生成リクエスト
   └─ アクセス権限チェック
   └─ PutObject用署名付きURL生成
   └─ クライアントがS3に直接PUT

4. ファイル配信
   └─ ファイル取得リクエスト
   └─ 署名付きダウンロード判定
   └─ Range Requestヘッダー解析
   └─ S3からオブジェクト取得
   └─ ストリーミングレスポンス

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

### フローチャート

```mermaid
flowchart TD
    A[ファイルアップロード開始] --> B{ファイルサイズ確認}
    B -->|50MB未満| C[putObject API]
    B -->|50MB以上| D[マルチパートアップロード]
    C --> E[アップロード完了]
    D --> E
    E --> F[ドキュメント更新]
    F --> G[終了]

    H[ファイル取得リクエスト] --> I{署名付きダウンロード?}
    I -->|Yes| J[署名付きURL生成]
    J --> K[302リダイレクト]
    I -->|No| L{Range Request?}
    L -->|Yes| M[部分コンテンツ取得]
    L -->|No| N[全コンテンツ取得]
    M --> O[ストリーミングレスポンス]
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | マルチパートアップロード閾値 | 50MB以上のファイルはマルチパートアップロードを使用 | ファイルサイズ >= 50MB |
| BR-002 | 署名付きURL有効期限 | デフォルト7200秒（2時間） | signedDownloadsが有効時 |
| BR-003 | クライアントアップロード署名有効期限 | 600秒（10分） | clientUploadsが有効時 |
| BR-004 | デフォルトアクセス制御 | クライアントアップロード時、認証済みユーザーのみ許可 | accessが未指定時 |
| BR-005 | ファイルサイズ制限 | Payload設定のupload.limits.fileSizeを適用 | クライアントアップロード時 |

### 計算ロジック

- S3オブジェクトキー: `${prefix}/${filename}`
- 公開URL: `${endpoint}/${bucket}/${prefix}/${encodeURIComponent(filename)}`

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

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

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

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

#### uploadsコレクション

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

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| NoSuchKey | 404 Not Found | 存在しないS3オブジェクトへのアクセス | ファイルパスの確認 |
| AccessDenied | 403 Forbidden | IAMポリシーまたはACLによるアクセス拒否 | AWS権限設定の確認 |
| InvalidBucketName | 400 Bad Request | 不正なバケット名 | バケット名の修正 |
| APIError | 400 Bad Request | クライアントアップロードでファイルサイズ超過 | ファイルサイズの縮小 |
| Forbidden | 403 Forbidden | クライアントアップロードのアクセス制御違反 | 認証状態の確認 |

### リトライ仕様

AWS SDKのデフォルトリトライポリシーを使用（指数バックオフ、最大3回）

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

S3操作はトランザクション非対応のため、PayloadのDB操作とは独立して実行される。アップロード失敗時、DBへの保存も行われないため一貫性は保たれる。削除操作時は、DBレコード削除後にS3オブジェクト削除が行われる。

## パフォーマンス要件

- S3クライアントは接続プール（keepAlive、maxSockets: 100）を使用
- S3クライアントインスタンスはキャッシュされ再利用される
- 大容量ファイルはマルチパートアップロードで並列処理（queueSize: 4）
- ファイル配信はストリーミングで処理、メモリ効率を最適化

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

- AWS認証情報は環境変数またはIAMロールで管理（コード内にハードコードしない）
- プライベートファイルには署名付きURLを使用
- SVGファイルには`Content-Security-Policy: script-src 'none'`を付与しXSS防止
- クライアントアップロードではアクセス制御関数でユーザー認証を確認
- ファイル名はsanitizeFilename関数でサニタイズ
- CORSは別途S3バケットポリシーで設定が必要

## 備考

- S3互換サービス（MinIO、DigitalOcean Spaces等）を使用する場合は、endpointを設定
- Vercel等のサーバーレス環境では、クライアント直接アップロード機能が有効
- alwaysInsertFieldsオプションを有効にすると、プラグイン無効時もフィールドスキーマが維持される

---

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

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

### 推奨読解順序

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

S3ストレージオプションの型定義と、クラウドストレージプラグインの共通インターフェースを理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.ts | `packages/storage-s3/src/index.ts` | S3StorageOptions型定義（23-96行目）でプラグインオプションを把握 |
| 1-2 | staticHandler.ts | `packages/storage-s3/src/staticHandler.ts` | SignedDownloadsConfig型（12-22行目）で署名付きダウンロード設定を理解 |

**読解のコツ**: S3StorageOptionsはcloudStoragePluginの共通型を拡張しており、`@payloadcms/plugin-cloud-storage/types`の型も参照すると全体像が把握できる。

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

s3Storage関数がプラグインのエントリーポイント。Payloadの設定を受け取り、S3アダプターを注入した設定を返す。

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

**主要処理フロー**:
1. **116行目**: S3クライアントキャッシュキーの決定
2. **118-132行目**: S3クライアントの遅延初期化とキャッシュ
3. **136-152行目**: クライアント直接アップロードの初期化
4. **181行目**: s3StorageInternal関数でアダプター生成
5. **184-195行目**: 各コレクションにアダプターを注入
6. **215-218行目**: cloudStoragePluginを呼び出し設定を返却

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

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

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

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

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

ファイルのS3へのアップロード処理。サイズによって単一PUT/マルチパートを切り替える。

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

**主要処理フロー**:
- **17行目**: マルチパート閾値定義（50MB）
- **26行目**: S3オブジェクトキーの構築
- **28-30行目**: ファイルデータソースの決定（tempFile or buffer）
- **32-42行目**: 50MB未満の場合、putObject API使用
- **44-57行目**: 50MB以上の場合、マルチパートアップロード使用

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

S3オブジェクトの削除処理。シンプルなdeleteObject API呼び出し。

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

**主要処理フロー**:
- **12-16行目**: prefixとfilenameからキーを構築し、deleteObject API呼び出し

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

静的ファイルハンドラー。署名付きURL、Range Request、ストリーミングに対応。

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

**主要処理フロー**:
- **81-99行目**: 署名付きダウンロードの判定と302リダイレクト
- **102-106行目**: headObjectでファイルサイズ取得
- **113-121行目**: Range Requestの解析と検証
- **128-135行目**: getObjectでオブジェクト取得
- **152-154行目**: SVGファイルへのCSPヘッダー付与
- **183-193行目**: ストリーミングレスポンス返却

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

サーバーを経由せずにクライアントから直接S3にアップロードする機能。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 7-1 | generateSignedURL.ts | `packages/storage-s3/src/generateSignedURL.ts` | サーバー側署名URL生成（26-96行目） |
| 7-2 | S3ClientUploadHandler.ts | `packages/storage-s3/src/client/S3ClientUploadHandler.ts` | クライアント側アップロード処理（6-45行目） |

**主要処理フロー**:
- **generateSignedURL.ts 58行目**: アクセス権限チェック
- **generateSignedURL.ts 62-63行目**: ファイル名のサニタイズとキー構築
- **generateSignedURL.ts 79-92行目**: PutObject用署名付きURL生成
- **S3ClientUploadHandler.ts 37-41行目**: クライアントからS3へ直接PUT

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

```
s3Storage (Plugin Entry)
    │
    ├─ getStorageClient()
    │      └─ new AWS.S3() [キャッシュ]
    │
    ├─ initClientUploads()
    │      └─ getGenerateSignedURLHandler()
    │             └─ getSignedUrl() [署名付きURL生成]
    │
    └─ s3StorageInternal()
           │
           ├─ getGenerateURL()
           │
           ├─ getHandleDelete()
           │      └─ S3.deleteObject()
           │
           ├─ getHandleUpload()
           │      ├─ S3.putObject() [小ファイル]
           │      └─ Upload.done() [大ファイル]
           │
           └─ getHandler()
                  ├─ getSignedUrl() [署名付きダウンロード]
                  ├─ S3.headObject() [サイズ取得]
                  └─ S3.getObject() [ストリーミング]
```

### データフロー図

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

ファイルデータ ────────▶ getHandleUpload() ────────────▶ S3バケット
(multipart/form-data)     │
                          ├─ < 50MB: putObject
                          └─ >= 50MB: マルチパートアップロード

ファイル取得リクエスト ──▶ getHandler() ────────────────▶ HTTPレスポンス
(GET /api/.../file.jpg)   │                                (ストリーミング)
                          ├─ 署名付き: 302リダイレクト
                          └─ 直接: getObject → Response

クライアント署名リクエスト ▶ getGenerateSignedURLHandler() ▶ 署名付きURL
(POST JSON)               │
                          └─ getSignedUrl(PutObject)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/storage-s3/src/index.ts` | ソース | プラグインエントリーポイント、S3StorageOptions型定義 |
| handleUpload.ts | `packages/storage-s3/src/handleUpload.ts` | ソース | ファイルアップロード処理（単一PUT/マルチパート） |
| handleDelete.ts | `packages/storage-s3/src/handleDelete.ts` | ソース | ファイル削除処理 |
| staticHandler.ts | `packages/storage-s3/src/staticHandler.ts` | ソース | 静的ファイル配信（署名付きURL、Range Request対応） |
| generateURL.ts | `packages/storage-s3/src/generateURL.ts` | ソース | 公開URL生成 |
| generateSignedURL.ts | `packages/storage-s3/src/generateSignedURL.ts` | ソース | クライアントアップロード用署名付きURL生成 |
| S3ClientUploadHandler.ts | `packages/storage-s3/src/client/S3ClientUploadHandler.ts` | ソース | クライアント側直接アップロード処理 |
| client.ts | `packages/storage-s3/src/exports/client.ts` | ソース | クライアントエクスポート |
