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

## 概要

本ドキュメントは、Decap CMSのメディアアップロード機能について、ファイルのアップロード処理・重複検出・AssetProxy作成・ドラフトファイル管理の詳細を記述した機能設計書である。

### 本機能の処理概要

メディアファイルのアップロード処理を担当する機能である。persistMedia関数がファイル受取からバックエンドへの永続化までを統括し、アセットストアIntegration・通常バックエンド・ドラフト編集中の3つのパスで処理を分岐する。

**業務上の目的・背景**：CMSのコンテンツ編集において、画像や添付ファイルのアップロードは基本操作である。Git系バックエンドではファイルをリポジトリにコミットし、アセットストア連携時はAPI経由でアップロードする。編集中のドラフトに対するファイル追加は、保存前のローカル管理が必要となる。

**機能の利用シーン**：
- メディアライブラリ画面でのファイルアップロード操作
- エントリ編集中の画像/ファイルウィジェットからの直接アップロード
- 重複ファイル名の検出と上書き確認

**主要な処理内容**：
1. ファイル名をサニタイズ（sanitizeSlug + toLowerCase）
2. 既存ファイルとの重複チェック（Integration非使用時のみ）
3. アセットストアIntegration経由 / 通常バックエンド / ドラフト編集中の3パスで処理分岐
4. AssetProxyオブジェクトの作成（createAssetProxy）
5. BlobのSHAハッシュを使用したファイルIDの生成（getBlobSHA）
6. バックエンドへの永続化またはドラフトエントリへのメディアファイル追加

**関連システム・外部連携**：Git系バックエンド、アセットストアIntegration（Algolia等）。

**権限による制御**：認証済みユーザーのみがアップロード可能。privateUploadはアセットストアIntegration使用時のみ利用可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | メディアライブラリ画面 | 主画面 | ファイルアップロード処理。persistMediaアクションによるファイル保存。ファイルサイズ制限チェック付き |

## 機能種別

CRUD操作（Create）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| file | File | Yes | アップロードするファイルオブジェクト | ブラウザのFileオブジェクト |
| privateUpload | boolean | No | プライベートアップロードフラグ | アセットストアIntegration使用時のみ |
| field | EntryField | No | 呼び出し元フィールド定義 | フィールド固有のメディアフォルダ判定に使用 |

### 入力データソース

- ブラウザのファイル入力（input[type=file]またはドラッグ&ドロップ）
- Redux Store: state.config（設定情報、slugサニタイズ設定）
- Redux Store: state.entryDraft（編集中エントリ）
- Redux Store: state.collections（コレクション定義）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| mediaFile | ImplementationMediaFile | 永続化されたメディアファイル情報 |
| assetProxy | AssetProxy | メモリ内のアセットプロキシオブジェクト |

### 出力先

- Redux Store: state.medias（AssetProxy追加: addAsset）
- Redux Store: state.mediaLibrary.files（ファイル一覧に追加: mediaPersisted）
- Redux Store: state.entryDraft.entry.mediaFiles（ドラフト時: addDraftEntryMediaFile）
- Git バックエンド: リポジトリにファイルをコミット

## 処理フロー

### 処理シーケンス

```
1. persistMedia(file, opts)が呼び出される
   └─ MediaLibrary.jsのhandlePersistまたはウィジェットからの直接呼び出し
2. ファイル名をサニタイズ
   └─ sanitizeSlug(file.name.toLowerCase(), state.config.slug)
3. 重複ファイルチェック（Integration非使用時のみ）
   └─ 既存ファイルから同名ファイルを検索
   └─ 存在する場合、window.confirm()で上書き確認
   └─ 上書きの場合、先にdeleteMediaを実行
4. 処理パスの分岐:
   a. アセットストアIntegration使用時:
      └─ provider.upload(file, privateUpload)を呼び出し
      └─ レスポンスURLからAssetProxyを作成
   b. privateUpload指定でIntegration非使用時:
      └─ エラーをスロー（privateUploadはIntegration専用）
   c. 通常バックエンド使用時:
      └─ selectMediaFilePath()でファイルパスを決定
      └─ ファイルとパスからAssetProxyを作成
5. addAsset(assetProxy)でメモリにアセットを追加
6. ファイルIDの生成とメディアファイル作成:
   a. Integration使用時:
      └─ getBlobSHA(file)でID生成、draft: false
   b. ドラフト編集中:
      └─ getBlobSHA(file)でID生成、draft: editingDraft
      └─ addDraftEntryMediaFile(mediaFile)でドラフトに追加して終了
   c. スタンドアロン（非ドラフト・非Integration）:
      └─ backend.persistMedia(state.config, assetProxy)でバックエンドに保存
7. mediaPersisted(mediaFile)をディスパッチ
   └─ MEDIA_PERSIST_SUCCESSアクション
```

### フローチャート

```mermaid
flowchart TD
    A[persistMedia呼び出し] --> B[ファイル名サニタイズ]
    B --> C{Integration使用?}
    C -->|Yes| D[provider.upload実行]
    C -->|No| E{重複ファイルあり?}
    E -->|Yes| F{上書き確認?}
    F -->|Yes| G[既存ファイル削除]
    F -->|No| H[処理中止 return]
    G --> I{privateUpload?}
    E -->|No| I
    I -->|Yes| J[エラースロー]
    I -->|No| K[selectMediaFilePath]
    D --> L[AssetProxy作成]
    K --> L
    L --> M[addAsset]
    M --> N{editingDraft?}
    N -->|Yes| O[addDraftEntryMediaFile]
    N -->|No| P[backend.persistMedia]
    P --> Q[mediaPersisted dispatch]
    O --> R[終了]
    Q --> R
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-26-01 | ファイル名サニタイズ | ファイル名をlowerCase化し、slugサニタイズルールを適用 | 常時 |
| BR-26-02 | 重複ファイル名チェック | 同名ファイルが存在する場合、上書き確認ダイアログを表示 | Integration非使用時 |
| BR-26-03 | privateUpload制限 | privateUploadオプションはアセットストアIntegration使用時のみ有効 | 常時 |
| BR-26-04 | ドラフトファイル管理 | ドラフト編集中のアップロードはバックエンドに即座に永続化せず、ドラフトエントリのmediaFilesに追加 | editingDraft === true |
| BR-26-05 | BlobSHAによるID生成 | ファイルのSHAハッシュをIDとして使用し、同一ファイルの重複を検出可能にする | Integration使用時およびドラフト編集時 |

### 計算ロジック

- ファイルパス: `selectMediaFilePath(state.config, collection, entry, fileName, field)`
- ファイルID: `getBlobSHA(file)` - BlobのSHA-256ハッシュ

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| アセット追加 | Redux Store: state.medias | INSERT | AssetProxyオブジェクトをメモリに追加 |
| ドラフトファイル追加 | Redux Store: state.entryDraft.entry.mediaFiles | INSERT | ドラフト編集中のメディアファイル追加 |
| ファイル永続化 | Git Repository | INSERT | バックエンドにファイルをコミット |
| ファイル一覧更新 | Redux Store: state.mediaLibrary.files | INSERT | 永続化成功時にファイル一覧に追加 |

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

#### Redux Store: state.mediaLibrary

| 操作 | 項目（キー） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | isPersisting | true -> false | PERSIST_REQUEST -> PERSIST_SUCCESS |
| INSERT | files[0] | 新規ファイル（uuidキー付き） | ファイル一覧の先頭に追加 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | privateUploadエラー | Integration非使用時にprivateUploadを指定 | Errorをスローし、通知メッセージ表示 |
| - | アップロード失敗 | バックエンドAPI / Integration APIの通信失敗 | エラー通知を表示、MEDIA_PERSIST_FAILUREをディスパッチ |
| - | Integration uploadエラー | アセットストアへのアップロード失敗 | ローカルAssetProxyにフォールバック |

### リトライ仕様

自動リトライは実装されていない。ユーザーが再度アップロード操作を実行する必要がある。

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

ドラフト編集中のアップロードはメモリ上のみの操作（Redux Store更新）であり、バックエンドへの永続化はエントリ保存時にまとめて実行される。スタンドアロンモードのアップロードは即座にバックエンドに永続化される。

## パフォーマンス要件

- getBlobSHAによるSHAハッシュ計算はファイルサイズに比例
- バックエンドへの永続化はネットワーク速度に依存

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

- ファイル名のサニタイズにより、パストラバーサル攻撃を防止
- 認証済みユーザーのみがアップロード可能

## 備考

- Integration使用時のuploadエラーではフォールバックとしてローカルAssetProxyが作成される
- createMediaFileFromAssetはpersistMediaの内部で使用される共通ヘルパー関数
- ファイル名は常にlowerCaseに変換される

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | AssetProxy.ts | `packages/decap-cms-core/src/valueObjects/AssetProxy.ts` | **10-21行目**: AssetProxyクラス。url, fileObj, path, fieldプロパティ |
| 1-2 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **437-444行目**: MediaOptions型定義 |

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **213-309行目**: persistMedia関数。ファイルアップロードのメインロジック |

**主要処理フロー**:
1. **220行目**: ファイル名サニタイズ `sanitizeSlug(file.name.toLowerCase(), state.config.slug)`
2. **221行目**: 既存ファイルとの重複チェック
3. **231-237行目**: 重複時の確認ダイアログと既存ファイル削除
4. **245-262行目**: Integration使用時のprovider.upload()
5. **263-264行目**: privateUpload時のエラー
6. **265-273行目**: 通常バックエンドでのAssetProxy作成
7. **276行目**: addAsset(assetProxy)でメモリに追加
8. **279-291行目**: ファイルID生成とmediaFile作成の3パス分岐
9. **296行目**: mediaPersisted(mediaFile)ディスパッチ

#### Step 3: ヘルパー関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **188-211行目**: createMediaFileFromAsset関数。id, file, assetProxy, draftフラグからメディアファイルオブジェクトを生成 |

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

```
persistMedia(file, opts)                    [actions/mediaLibrary.ts:213]
    |
    +-- sanitizeSlug(fileName)              [lib/urlHelper.ts]
    |
    +-- selectMediaFiles(state, field)      [reducers/mediaLibrary.ts:259]
    |
    +-- [重複時] deleteMedia(existingFile)  [actions/mediaLibrary.ts:311]
    |
    +-- [Integration] provider.upload()     [integrations]
    |   +-- createAssetProxy({url, path})
    |
    +-- [通常] selectMediaFilePath()        [reducers/entries.ts]
    |   +-- createAssetProxy({file, path, field})
    |
    +-- addAsset(assetProxy)                [actions/media.ts:25]
    |
    +-- getBlobSHA(file)                    [decap-cms-lib-util]
    |
    +-- createMediaFileFromAsset()          [actions/mediaLibrary.ts:188]
    |
    +-- [ドラフト] addDraftEntryMediaFile() [actions/entries.ts]
    |
    +-- [非ドラフト] backend.persistMedia() [各バックエンド]
    |
    +-- mediaPersisted(mediaFile)           [actions/mediaLibrary.ts:462]
```

### データフロー図

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

File オブジェクト      sanitizeSlug                     AssetProxy
(ブラウザ)                |                               (メモリ)
        |                 v
        +---------> 重複チェック                          Redux Store
                         |                               (medias, mediaLibrary)
config.slug              v
(サニタイズ設定)     AssetProxy作成                      Git Repository
        |                |                               (ファイルコミット)
        +---------> addAsset
                         |
                         v
                    [3パス分岐]
                    Integration -> provider.upload
                    ドラフト    -> addDraftEntryMediaFile
                    通常       -> backend.persistMedia
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| mediaLibrary.ts | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | ソース | persistMedia関数（メインロジック） |
| AssetProxy.ts | `packages/decap-cms-core/src/valueObjects/AssetProxy.ts` | ソース | AssetProxyクラス・createAssetProxy関数 |
| media.ts | `packages/decap-cms-core/src/actions/media.ts` | ソース | addAsset / removeAssetアクション |
| entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | ソース | addDraftEntryMediaFile関数 |
| mediaLibrary.ts | `packages/decap-cms-core/src/reducers/mediaLibrary.ts` | ソース | selectMediaFiles / リデューサー |
| entries.ts | `packages/decap-cms-core/src/reducers/entries.ts` | ソース | selectMediaFilePath / selectEditingDraft |
| urlHelper.ts | `packages/decap-cms-core/src/lib/urlHelper.ts` | ソース | sanitizeSlug関数 |
