# 機能設計書 7-メディアアップロード

## 概要

本ドキュメントは、Ghost CMSにおけるメディアアップロード機能について、その設計仕様を記載する。メディアアップロード機能は、画像・動画・音声・ファイルのアップロードと保存を担う。

### 本機能の処理概要

メディアアップロード機能は、Ghost CMSで使用する各種メディアファイル（画像、動画、音声、一般ファイル）をアップロードし、適切なストレージに保存する機能である。画像については自動最適化（リサイズ）が行われ、オリジナル画像も保持される。ストレージはローカルファイルシステムまたはS3互換ストレージを選択可能。

**業務上の目的・背景**：コンテンツ管理において、記事やページに画像・動画・音声などのメディアを埋め込むことは必須要件である。本機能により、ユーザーは管理画面から簡単にメディアをアップロードし、コンテンツに挿入できる。また、画像の自動最適化により、ページ表示速度の向上とストレージ容量の節約を実現する。

**機能の利用シーン**：
- ライターが記事にアイキャッチ画像をアップロードする
- 編集者が記事内に動画コンテンツを埋め込む
- 管理者がPDFなどのダウンロードファイルを提供する
- ポッドキャスト配信者が音声ファイルをアップロードする

**主要な処理内容**：
1. 画像アップロード（POST /ghost/api/admin/images/upload/）
2. メディアアップロード（POST /ghost/api/admin/media/upload/）
3. サムネイルアップロード（POST /ghost/api/admin/media/thumbnail/upload/）
4. ファイルアップロード（POST /ghost/api/admin/files/upload/）

**関連システム・外部連携**：
- ローカルファイルシステムストレージ（デフォルト）
- S3互換オブジェクトストレージ（オプション）
- 画像変換ライブラリ（@tryghost/image-transform）

**権限による制御**：
- すべてのアップロード操作はAdmin API認証が必要（permissions: false は認証不要ではなく、細かい権限チェックを行わないことを意味する）
- 認証済みユーザーのみがアップロード可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 13 | エディタ画面 | 主機能 | 記事内への画像・メディア挿入 |
| 14 | 設定画面 | 補助機能 | サイトアイコン・ロゴのアップロード |

## 機能種別

ファイル処理

## 入力仕様

### 入力パラメータ

#### 画像アップロード（images/upload）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| file | multipart/form-data | Yes | 画像ファイル | 拡張子: .jpg, .jpeg, .gif, .png, .svg, .svgz, .ico, .webp |

#### メディアアップロード（media/upload）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| file | multipart/form-data | Yes | メディアファイル | 拡張子: .mp4, .webm, .ogv, .mp3, .wav, .ogg, .m4a |
| thumbnail | multipart/form-data | No | サムネイル画像 | 拡張子: .jpg, .jpeg, .gif, .png, .svg, .svgz, .ico, .webp |

#### ファイルアップロード（files/upload）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| file | multipart/form-data | Yes | 一般ファイル | 拡張子: .pdf, .json, .jsonld, .odp, .ods, .odt, .ppt, .pptx, .rtf, .txt, .xls, .xlsx, .xml |

### 入力データソース

- 管理画面（Ghost Admin）のエディタからのファイル選択
- 設定画面からのアイコン・ロゴアップロード
- Admin APIを通じた外部クライアントからのリクエスト

## 出力仕様

### 出力データ

#### 画像アップロード

| 項目名 | 型 | 説明 |
|--------|-----|------|
| url | string | アップロードされた画像のURL |

#### メディアアップロード

| 項目名 | 型 | 説明 |
|--------|-----|------|
| filePath | string | アップロードされたメディアファイルのURL |
| thumbnailPath | string | サムネイル画像のURL（存在する場合） |

#### ファイルアップロード

| 項目名 | 型 | 説明 |
|--------|-----|------|
| filePath | string | アップロードされたファイルのURL |

### 出力先

- APIレスポンス（JSON形式）
- ファイルシステム（content/images/, content/media/, content/files/）
- または S3互換ストレージ

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信（multipart/form-data）
   └─ multerによるファイル受信（一時ディレクトリに保存）
2. ファイルバリデーション
   ├─ ファイル存在チェック
   ├─ MIMEタイプチェック
   ├─ 拡張子チェック
   └─ SVGの場合はサニタイズ処理
3. 画像最適化（画像アップロードの場合）
   ├─ リサイズ対象判定（GIF以外かつresizeオプションがtrue）
   ├─ 最大幅2000pxにリサイズ
   └─ オリジナル画像を_o付きで保存
4. ストレージ保存
   ├─ ストレージアダプター取得（images/media/files）
   ├─ ユニークファイル名生成
   └─ ファイル保存
5. レスポンス返却
   └─ 保存されたファイルのURLを返却
6. 一時ファイル削除
   └─ レスポンス完了後に自動削除
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B[multerでファイル受信]
    B --> C{ファイル存在?}
    C -->|No| D[400 ValidationError]
    C -->|Yes| E{MIMEタイプ・拡張子チェック}
    E -->|不正| F[415 UnsupportedMediaTypeError]
    E -->|正常| G{SVGファイル?}
    G -->|Yes| H[DOMPurifyでサニタイズ]
    H --> I{サニタイズ成功?}
    I -->|No| F
    I -->|Yes| J{画像アップロード?}
    G -->|No| J
    J -->|Yes| K{リサイズ対象?}
    K -->|Yes| L[画像リサイズ処理]
    L --> M[最適化画像保存]
    M --> N[オリジナル画像保存]
    K -->|No| O[そのまま保存]
    J -->|No| O
    N --> P[URLを返却]
    O --> P
    P --> Q[一時ファイル削除]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 画像最適化 | 画像は最大幅2000pxにリサイズされる | imageOptimization.resize=trueかつGIF以外 |
| BR-002 | オリジナル保持 | リサイズ時はオリジナル画像も_o付きで保存 | リサイズ処理実行時 |
| BR-003 | ファイル名サフィックス制限 | ファイル名の_oサフィックスは削除される | 画像アップロード時 |
| BR-004 | ユニークファイル名 | 同名ファイル存在時は連番付与 | 全ファイルアップロード時 |
| BR-005 | SVGサニタイズ | SVGファイルはDOMPurifyでサニタイズ | SVG/SVGZアップロード時 |
| BR-006 | サムネイル名規則 | サムネイルはメディアファイル名_thumbで保存 | メディアサムネイルアップロード時 |

### 計算ロジック

- **ファイル保存パス**: `content/{type}/YYYY/MM/filename.ext` 形式で年月別に保存
- **URL生成**: `/{subdir}/{staticFileURLPrefix}/{path}` 形式で相対パスを生成
- **リサイズ後の寸法**: アスペクト比を維持して最大幅2000pxに縮小

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

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

本機能はデータベースに直接書き込みを行わない（ファイルシステムのみ）。

| 操作 | 対象 | 操作種別 | 概要 |
|-----|------|---------|------|
| 画像アップロード | content/images/ | ファイル作成 | 画像ファイルの保存 |
| メディアアップロード | content/media/ | ファイル作成 | 動画・音声ファイルの保存 |
| ファイルアップロード | content/files/ | ファイル作成 | 一般ファイルの保存 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | ValidationError | ファイルが選択されていない | ファイルを選択して再実行 |
| 400 | BadRequestError | 画像処理に失敗（破損ファイル等） | 有効な画像ファイルを使用 |
| 400 | BadRequestError | ファイル名が長すぎる（ENAMETOOLONG） | 短いファイル名を使用 |
| 415 | UnsupportedMediaTypeError | サポートされていないファイル形式 | 許可されたファイル形式を使用 |
| 415 | UnsupportedMediaTypeError | SVGサニタイズ失敗 | 有効なSVGファイルを使用 |

### リトライ仕様

特別なリトライ処理は実装されていない。アップロード失敗時はユーザーによる再実行が必要。

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

データベース操作がないため、トランザクション制御は不要。ファイル操作は個別に行われる。

## パフォーマンス要件

- 画像リサイズ処理: 画像サイズに依存（大きな画像は処理時間増加）
- ファイル保存: ストレージI/O性能に依存
- 一時ファイルはレスポンス完了後に自動削除

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

- **認証**: Admin API認証（Sessionまたは Admin API Key）が必要
- **MIMEタイプ検証**: Content-Typeと拡張子の両方をチェック
- **SVGサニタイズ**: DOMPurifyによるXSS対策（script要素等の除去）
- **一時ファイル管理**: 処理完了後に自動削除（uploadClear設定）
- **ファイル名正規化**: _oサフィックスの削除（オリジナル画像との衝突防止）

## 備考

- ストレージアダプターはadapter-managerで管理され、設定で切り替え可能
- 静的ファイル配信時のキャッシュ期間は1年（maxAge: 365日）
- GIFファイルはアニメーション保持のためリサイズ対象外

---

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

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

### 推奨読解順序

#### Step 1: アップロード設定を理解する

許可されるファイル形式と画像最適化設定を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | overrides.json | `ghost/core/core/shared/config/overrides.json` | uploads設定（20-101行目）とimageOptimization設定（111-118行目） |

**主要設定項目**:
- **20-27行目**: images設定（拡張子: .jpg, .jpeg, .gif, .png, .svg, .svgz, .ico, .webp）
- **29-43行目**: media設定（拡張子: .mp4, .webm, .ogv, .mp3, .wav, .ogg, .m4a）
- **45-76行目**: files設定（拡張子: .pdf, .json等のドキュメント）
- **78-80行目**: thumbnails設定（画像形式のみ）
- **111-112行目**: imageOptimization.defaultMaxWidth = 2000

#### Step 2: アップロードミドルウェアを理解する

ファイルバリデーションとサニタイズ処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | upload.js | `ghost/core/core/server/web/api/middleware/upload.js` | アップロード処理とバリデーション |

**主要処理フロー**:
- **59行目**: multerの初期化（一時ディレクトリ = os.tmpdir()）
- **69-104行目**: single() - 単一ファイルアップロード処理
- **106-148行目**: media() - メディア+サムネイルアップロード処理
- **154-162行目**: checkFileIsValid() - MIMEタイプと拡張子の検証
- **173-188行目**: sanitizeSvg() - SVGサニタイズ処理
- **198-213行目**: sanitizeSvgContent() - DOMPurifyによるサニタイズ
- **255-300行目**: validation() - ファイルバリデーションミドルウェア
- **308-357行目**: mediaValidation() - メディアファイルバリデーション

**重要ポイント**:
- SVGファイルはDOMPurifyでXSS対策としてサニタイズされる（288-296行目）
- レスポンス完了後に一時ファイルが自動削除される（84-100行目）

#### Step 3: APIエンドポイントを理解する

各種アップロードAPIの実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | images.js | `ghost/core/core/server/api/endpoints/images.js` | 画像アップロードAPI |
| 3-2 | media.js | `ghost/core/core/server/api/endpoints/media.js` | メディアアップロードAPI |
| 3-3 | files.js | `ghost/core/core/server/api/endpoints/files.js` | ファイルアップロードAPI |

**images.js の主要処理**:
- **22-23行目**: imageOptimization設定の取得
- **25行目**: ファイル名から_oサフィックスを削除
- **28行目**: shouldResizeFileExtension()でリサイズ対象判定
- **39-48行目**: imageTransform.resizeFromPath()で画像リサイズ
- **51-54行目**: 最適化画像の保存
- **71-75行目**: オリジナル画像を_o付きで保存

**media.js の主要処理**:
- **13-24行目**: upload - メディアとサムネイルの保存
- **37-48行目**: uploadThumbnail - 既存メディアへのサムネイル追加

**files.js の主要処理**:
- **12-20行目**: upload - 一般ファイルの保存

#### Step 4: ストレージアダプターを理解する

ファイル保存処理の実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | index.js | `ghost/core/core/server/adapters/storage/index.js` | ストレージアダプター取得 |
| 4-2 | LocalStorageBase.js | `ghost/core/core/server/adapters/storage/LocalStorageBase.js` | ローカルストレージ基底クラス |
| 4-3 | LocalImagesStorage.js | `ghost/core/core/server/adapters/storage/LocalImagesStorage.js` | 画像ストレージ |
| 4-4 | LocalMediaStorage.js | `ghost/core/core/server/adapters/storage/LocalMediaStorage.js` | メディアストレージ |

**LocalStorageBase.js の主要処理**:
- **49-85行目**: save() - ファイル保存（ユニーク名生成、ディレクトリ作成、コピー）
- **60行目**: getUniqueFileName()でユニークファイル名生成
- **77-82行目**: URL生成（相対パス形式）
- **114-135行目**: urlToPath() - URLからファイルパスへの変換
- **137-152行目**: exists() - ファイル存在確認
- **161-199行目**: serve() - 静的ファイル配信（maxAge: 1年）
- **205-213行目**: delete() - ファイル削除

**ストレージパス**:
- 画像: `content/images/` (staticFileURLPrefix: /content/images)
- メディア: `content/media/` (staticFileURLPrefix: /content/media)
- ファイル: `content/files/` (staticFileURLPrefix: /content/files)

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

```
HTTP Request (multipart/form-data)
    │
    ├─ Middleware Layer
    │      │
    │      ├─ upload.single('file') / upload.media('file', 'thumbnail')
    │      │      └─ multer - ファイル受信・一時保存
    │      │
    │      └─ validation({type}) / mediaValidation({type})
    │             ├─ checkFileExists() - ファイル存在確認
    │             ├─ checkFileIsValid() - MIMEタイプ・拡張子チェック
    │             └─ sanitizeSvg() - SVGサニタイズ（該当時）
    │
    └─ API Layer
           │
           ├─ images.js upload query()
           │      ├─ imageTransform.shouldResizeFileExtension()
           │      ├─ imageTransform.resizeFromPath() - リサイズ処理
           │      └─ storage.getStorage('images').save()
           │
           ├─ media.js upload query()
           │      ├─ storage.getStorage('media').save() - サムネイル
           │      └─ storage.getStorage('media').save() - メディア
           │
           └─ files.js upload query()
                  └─ storage.getStorage('files').save()

Storage Adapter Layer
    │
    ├─ adapter-manager.getAdapter('storage:{type}')
    │
    └─ LocalStorageBase
           ├─ getTargetDir() - 保存先ディレクトリ（年月別）
           ├─ getUniqueFileName() - ユニークファイル名生成
           ├─ fs.mkdirs() - ディレクトリ作成
           ├─ fs.copy() - ファイルコピー
           └─ URL生成（相対パス形式）
```

### データフロー図

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

Admin UI / API Client
       │
       ▼
┌─────────────────┐
│ multipart/form- │
│ data リクエスト │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ multer          │───▶│ 一時ファイル    │
│ ファイル受信    │    │ (os.tmpdir())   │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Validation      │
                       │ - MIMEタイプ    │
                       │ - 拡張子        │
                       │ - SVGサニタイズ │
                       └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ 画像の場合      │     │ その他の場合    │
           │ imageTransform  │     │                 │
           │ - リサイズ      │     │                 │
           │ - オリジナル保存│     │                 │
           └────────┬────────┘     └────────┬────────┘
                    │                       │
                    └───────────┬───────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Storage Adapter │
                       │ save()          │
                       │ - ユニーク名    │
                       │ - ファイル保存  │
                       └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ content/images/ │     │ content/media/  │
           │ content/files/  │     │                 │
           │ (Local FS)      │     │ (or S3)         │
           └────────┬────────┘     └────────┬────────┘
                    │                       │
                    └───────────┬───────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ JSON Response   │
                       │ { url/filePath }│
                       └─────────────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ 一時ファイル    │
                       │ 自動削除        │
                       └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| images.js | `ghost/core/core/server/api/endpoints/images.js` | API | 画像アップロードエンドポイント |
| media.js | `ghost/core/core/server/api/endpoints/media.js` | API | メディアアップロードエンドポイント |
| files.js | `ghost/core/core/server/api/endpoints/files.js` | API | ファイルアップロードエンドポイント |
| upload.js | `ghost/core/core/server/web/api/middleware/upload.js` | ミドルウェア | アップロードバリデーション |
| index.js | `ghost/core/core/server/adapters/storage/index.js` | アダプター | ストレージアダプター取得 |
| LocalStorageBase.js | `ghost/core/core/server/adapters/storage/LocalStorageBase.js` | アダプター | ローカルストレージ基底クラス |
| LocalImagesStorage.js | `ghost/core/core/server/adapters/storage/LocalImagesStorage.js` | アダプター | 画像ストレージ |
| LocalMediaStorage.js | `ghost/core/core/server/adapters/storage/LocalMediaStorage.js` | アダプター | メディアストレージ |
| LocalFilesStorage.js | `ghost/core/core/server/adapters/storage/LocalFilesStorage.js` | アダプター | ファイルストレージ |
| S3Storage.ts | `ghost/core/core/server/adapters/storage/S3Storage.ts` | アダプター | S3互換ストレージ |
| overrides.json | `ghost/core/core/shared/config/overrides.json` | 設定 | アップロード設定・画像最適化設定 |
