# 機能設計書 23-ファイルアップロード

## 概要

本ドキュメントは、RuoYiシステムにおけるファイルアップロード機能の詳細設計を記述する。システム全体で共通利用される汎用的なファイルアップロードAPIを提供し、単一ファイルおよび複数ファイルの一括アップロードに対応する。

### 本機能の処理概要

本機能は、Webアプリケーション内の各種画面からファイルをサーバーにアップロードするための共通機能を提供する。画像、ドキュメント、動画など多様なファイル形式に対応し、ファイルサイズ制限や拡張子検証によりセキュリティを確保する。

**業務上の目的・背景**：業務システムでは添付ファイル、インポートデータ、画像素材など、様々な場面でファイルのアップロードが必要となる。共通のアップロード機能を提供することで、開発効率の向上とセキュリティ対策の統一化を図る。

**機能の利用シーン**：
- ユーザー管理画面でExcelファイルをインポートする場面
- プロフィール画面でアバター画像をアップロードする場面（SysProfileController経由）
- 通知公告編集画面で添付ファイルを登録する場面
- その他、システム内でファイル添付が必要な場面全般

**主要な処理内容**：
1. HTTPマルチパートリクエストからファイルを受信
2. ファイルサイズの検証（上限50MB）
3. ファイル拡張子の検証（許可リストとの照合）
4. ファイル名の長さ検証（上限100文字）
5. 一意なファイル名の生成（日付パス + 元ファイル名 + シーケンス番号）
6. 指定ディレクトリへのファイル保存
7. アクセスURL、ファイル名等の情報を返却

**関連システム・外部連携**：
- ファイルシステム（ファイル保存先ディレクトリ）
- リソースマッピング（/profile/** パスでの静的ファイル配信）

**権限による制御**：本機能自体には特定の権限チェックはない。呼び出し元の機能で適切な権限制御が行われることを前提とする。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | ユーザー管理一覧 | 補助機能 | ユーザーデータのExcelインポート処理 |
| 17 | アバター変更 | 補助機能 | 画像ファイルのアップロード処理 |

## 機能種別

ファイルアップロード / ファイル保存 / バリデーション

## 入力仕様

### 入力パラメータ

#### 単一ファイルアップロード（POST /common/upload）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| file | MultipartFile | Yes | アップロードファイル | サイズ50MB以下、許可拡張子、ファイル名100文字以下 |

#### 複数ファイルアップロード（POST /common/uploads）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| files | List\<MultipartFile\> | Yes | アップロードファイルリスト | 各ファイルに対して単一アップロードと同じバリデーション |

### 入力データソース

- HTTPマルチパートリクエスト（multipart/form-data）

### 許可ファイル形式

以下の拡張子がデフォルトで許可されている：

| カテゴリ | 拡張子 |
|---------|--------|
| 画像 | bmp, gif, jpg, jpeg, png |
| ドキュメント | doc, docx, xls, xlsx, ppt, pptx, html, htm, txt, pdf |
| 圧縮ファイル | rar, zip, gz, bz2 |
| 動画 | mp4, avi, rmvb |

## 出力仕様

### 出力データ

#### 単一ファイルアップロード成功時

| 項目名 | 型 | 説明 |
|--------|-----|------|
| code | int | 処理結果コード（0:成功） |
| msg | String | 成功メッセージ |
| url | String | ファイルアクセスURL（サーバーURL + ファイルパス） |
| fileName | String | 保存先ファイルパス（/profile/upload/...） |
| newFileName | String | 保存後のファイル名のみ |
| originalFilename | String | 元のファイル名 |

#### 複数ファイルアップロード成功時

| 項目名 | 型 | 説明 |
|--------|-----|------|
| code | int | 処理結果コード（0:成功） |
| msg | String | 成功メッセージ |
| urls | String | ファイルアクセスURLのカンマ区切り文字列 |
| fileNames | String | 保存先ファイルパスのカンマ区切り文字列 |
| newFileNames | String | 保存後ファイル名のカンマ区切り文字列 |
| originalFilenames | String | 元ファイル名のカンマ区切り文字列 |

### 出力先

- JSONレスポンス（Ajax応答）
- ファイルシステム（{profile}/upload/yyyy/MM/dd/ディレクトリ）

## 処理フロー

### 処理シーケンス

```
【単一ファイルアップロード】
1. リクエスト受信
   └─ POST /common/upload (multipart/form-data)

2. アップロード処理
   └─ FileUploadUtils.upload(filePath, file)
       │
       ├─ 2-1. ファイル名長さチェック
       │        └─ file.getOriginalFilename().length() <= 100
       │
       ├─ 2-2. ファイルサイズ・拡張子チェック
       │        └─ assertAllowed(file, allowedExtension)
       │
       ├─ 2-3. ファイル名生成
       │        └─ extractFilename(file)
       │            └─ {datePath}/{baseName}_{seq}.{ext}
       │
       ├─ 2-4. ディレクトリ作成
       │        └─ getAbsoluteFile(baseDir, fileName)
       │
       └─ 2-5. ファイル保存
                └─ file.transferTo(Paths.get(absPath))

3. URL生成
   └─ serverConfig.getUrl() + fileName

4. レスポンス作成
   └─ AjaxResult.success() + url, fileName, newFileName, originalFilename

【複数ファイルアップロード】
1. リクエスト受信
   └─ POST /common/uploads (multipart/form-data)

2. ファイルリストをループ処理
   └─ for (MultipartFile file : files)
       └─ 単一ファイルと同じ処理

3. 結果を集約
   └─ urls, fileNames, newFileNames, originalFilenames をカンマ区切りで結合

4. レスポンス作成
   └─ AjaxResult.success() + 集約結果
```

### フローチャート

```mermaid
flowchart TD
    A[開始: POST /common/upload] --> B[ファイル受信]
    B --> C{ファイル名長さ <= 100?}
    C -->|No| D[FileNameLengthLimitExceededException]
    C -->|Yes| E{ファイルサイズ <= 50MB?}
    E -->|No| F[FileSizeLimitExceededException]
    E -->|Yes| G{拡張子は許可リスト内?}
    G -->|No| H[InvalidExtensionException]
    G -->|Yes| I[ファイル名生成]
    I --> J[日付ディレクトリ作成]
    J --> K[ファイル保存]
    K --> L[アクセスURL生成]
    L --> M[成功レスポンス返却]
    D --> N[エラーレスポンス返却]
    F --> N
    H --> N
    M --> O[終了]
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | ファイルサイズ制限 | 1ファイルあたり50MB以下 | 常時 |
| BR-02 | ファイル名長さ制限 | 元ファイル名は100文字以下 | 常時 |
| BR-03 | 拡張子ホワイトリスト | 許可された拡張子のみアップロード可能 | 常時 |
| BR-04 | 日付ディレクトリ構成 | ファイルは yyyy/MM/dd 形式のディレクトリに保存 | 常時 |
| BR-05 | ファイル名一意化 | シーケンス番号付与により同名ファイルの上書きを防止 | 常時 |
| BR-06 | 複数ファイル結果形式 | 複数ファイルの結果はカンマ区切り文字列で返却 | 複数アップロード時 |

### 計算ロジック

**ファイル名生成ロジック（extractFilename）**：
```
{datePath}/{baseName}_{sequenceId}.{extension}

例: 2026/01/08/sample_0001.xlsx
```

- `datePath`: DateUtils.datePath() で生成（yyyy/MM/dd形式）
- `baseName`: FilenameUtils.getBaseName() で拡張子を除いた元ファイル名
- `sequenceId`: Seq.getId(Seq.uploadSeqType) で生成される4桁シーケンス
- `extension`: getExtension(file) でファイル拡張子を取得

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

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

本機能はデータベースへのアクセスを行わない。

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| - | - | - | データベース操作なし |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | FileSizeLimitExceededException | ファイルサイズが50MBを超過 | エラーメッセージを表示、ファイル分割を推奨 |
| - | FileNameLengthLimitExceededException | ファイル名が100文字を超過 | エラーメッセージを表示、ファイル名短縮を要求 |
| - | InvalidExtensionException | 許可されていない拡張子 | エラーメッセージを表示、許可形式を案内 |
| - | InvalidImageExtensionException | 画像形式チェックで不正 | 画像として許可された形式を案内 |
| - | IOException | ファイル保存時のI/Oエラー | エラーメッセージを表示、再試行を促す |

### リトライ仕様

本機能にリトライ機能はない。エラー発生時はクライアント側で再アップロードを行う。

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

本機能はデータベース操作を行わないため、トランザクション管理は不要。
ファイル保存はファイルシステム操作であり、保存失敗時は例外がスローされる。

## パフォーマンス要件

- 小ファイル（1MB以下）: 1s以内
- 中ファイル（1-10MB）: 5s以内
- 大ファイル（10-50MB）: 30s以内
- 複数ファイル: ファイル数 × 単一ファイル所要時間

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

- 拡張子ホワイトリスト方式により、実行可能ファイル（.exe, .sh等）のアップロードを防止
- ファイル名に元ファイル名を含めるが、シーケンス番号により一意化
- アップロードディレクトリはWebルート外に配置（{profile}/upload/）
- RESOURCE_PREFIX（/profile/）経由でのアクセスにより、ディレクトリトラバーサルを防止
- ファイルサイズ制限によりDoS攻撃を緩和

## 備考

- 保存先ベースディレクトリ: RuoYiConfig.getUploadPath()（{profile}/upload）
- URLプレフィックス: Constants.RESOURCE_PREFIX（/profile/）
- デフォルト最大サイズ: 50MB（FileUploadUtils.DEFAULT_MAX_SIZE）
- デフォルトファイル名最大長: 100文字（FileUploadUtils.DEFAULT_FILE_NAME_LENGTH）
- 複数ファイル結果の区切り文字: カンマ（FILE_DELIMETER = ","）

---

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

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

### 推奨読解順序

#### Step 1: 定数・設定を理解する

まず、ファイルアップロードに関する定数と設定値を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | RuoYiConfig.java | `ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java` | アップロードパスの設定 |
| 1-2 | MimeTypeUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java` | 許可ファイル形式の定義 |
| 1-3 | Constants.java | `ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java` | RESOURCE_PREFIX定数 |

**読解のコツ**:
- `RuoYiConfig.getUploadPath()`は`getProfile() + "/upload"`を返す
- `MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION`に許可拡張子一覧が定義

**主要処理フロー（RuoYiConfig）**:
- **120-123行目**: getUploadPath() - アップロードディレクトリパスを返却

**主要処理フロー（MimeTypeUtils）**:
- **20行目**: IMAGE_EXTENSION - 画像形式の拡張子配列
- **29-39行目**: DEFAULT_ALLOWED_EXTENSION - デフォルト許可拡張子配列

#### Step 2: コントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | CommonController.java | `ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java` | アップロードエンドポイント |

**読解のコツ**:
- `uploadFile()`が単一ファイル、`uploadFiles()`が複数ファイル対応
- `ServerConfig.getUrl()`でサーバーのベースURLを取得

**主要処理フロー**:
- **75-97行目**: uploadFile() - 単一ファイルアップロード処理
- **102-135行目**: uploadFiles() - 複数ファイルアップロード処理
- **82行目**: RuoYiConfig.getUploadPath()でアップロードディレクトリ取得
- **84行目**: FileUploadUtils.upload()でファイル保存
- **85行目**: serverConfig.getUrl() + fileNameでアクセスURL生成
- **125行目**: StringUtils.join()でカンマ区切り結合

#### Step 3: ファイルアップロードユーティリティを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | FileUploadUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java` | アップロード処理のコアロジック |

**読解のコツ**:
- 複数のオーバーロードされた`upload()`メソッドがあり、最終的に全パラメータ版が呼ばれる
- `extractFilename()`と`uuidFilename()`の2種類のファイル名生成方式がある

**主要処理フロー**:
- **29行目**: DEFAULT_MAX_SIZE = 50MB
- **34行目**: DEFAULT_FILE_NAME_LENGTH = 100
- **102-139行目**: upload() - メインのアップロード処理
- **126-129行目**: ファイル名長さチェック
- **132行目**: assertAllowed() - サイズ・拡張子チェック
- **134行目**: extractFilename() または uuidFilename() でファイル名生成
- **136-138行目**: ファイル保存とパス返却
- **144-147行目**: extractFilename() - 元ファイル名ベースのファイル名生成
- **152-155行目**: uuidFilename() - UUIDベースのファイル名生成
- **186-224行目**: assertAllowed() - ファイルサイズと拡張子の検証

#### Step 4: サーバー設定を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | ServerConfig.java | `ruoyi-common/src/main/java/com/ruoyi/common/config/ServerConfig.java` | サーバーURL生成 |

**主要処理フロー**:
- **22-25行目**: getUrl() - リクエストから完全なサーバーURLを生成
- **27-32行目**: getDomain() - プロトコル、ホスト、ポート、コンテキストパスを結合

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

```
CommonController
    │
    ├─ uploadFile()
    │      ├─ RuoYiConfig.getUploadPath()
    │      │      └─ getProfile() + "/upload"
    │      │
    │      ├─ FileUploadUtils.upload(filePath, file)
    │      │      ├─ file.getOriginalFilename().length() チェック
    │      │      │
    │      │      ├─ assertAllowed(file, allowedExtension)
    │      │      │      ├─ file.getSize() <= DEFAULT_MAX_SIZE
    │      │      │      └─ isAllowedExtension(extension, allowedExtension)
    │      │      │
    │      │      ├─ extractFilename(file)
    │      │      │      ├─ DateUtils.datePath()
    │      │      │      ├─ FilenameUtils.getBaseName()
    │      │      │      ├─ Seq.getId(Seq.uploadSeqType)
    │      │      │      └─ getExtension(file)
    │      │      │
    │      │      ├─ getAbsoluteFile(baseDir, fileName)
    │      │      │      └─ mkdirs() if not exists
    │      │      │
    │      │      ├─ file.transferTo(Paths.get(absPath))
    │      │      │
    │      │      └─ getPathFileName(uploadDir, fileName)
    │      │             └─ RESOURCE_PREFIX + currentDir + fileName
    │      │
    │      ├─ serverConfig.getUrl()
    │      │      └─ ServletUtils.getRequest() → getDomain()
    │      │
    │      └─ AjaxResult.success() + put(url, fileName, ...)
    │
    └─ uploadFiles()
           └─ for each file: 同上の処理を繰り返し
                 └─ StringUtils.join(results, ",")
```

### データフロー図

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

【単一ファイルアップロード】
MultipartFile ─────▶ CommonController.uploadFile() ────▶ AjaxResult
                           │                                 ├─ url
                           ├─▶ assertAllowed()               ├─ fileName
                           │      ├─ size check              ├─ newFileName
                           │      └─ extension check         └─ originalFilename
                           │
                           ├─▶ extractFilename()
                           │      └─ {date}/{name}_{seq}.{ext}
                           │
                           └─▶ file.transferTo()
                                  └─ {profile}/upload/yyyy/MM/dd/

【複数ファイルアップロード】
List<MultipartFile> ──▶ CommonController.uploadFiles() ──▶ AjaxResult
                              │                                ├─ urls (CSV)
                              └─▶ for each file:               ├─ fileNames (CSV)
                                    └─ 単一と同じ処理           ├─ newFileNames (CSV)
                                                               └─ originalFilenames (CSV)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| CommonController.java | `ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java` | コントローラー | アップロードエンドポイント |
| FileUploadUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java` | ユーティリティ | アップロード処理のコアロジック |
| MimeTypeUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java` | ユーティリティ | 許可ファイル形式定義 |
| RuoYiConfig.java | `ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java` | 設定 | アップロードパス設定 |
| ServerConfig.java | `ruoyi-common/src/main/java/com/ruoyi/common/config/ServerConfig.java` | 設定 | サーバーURL生成 |
| DateUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java` | ユーティリティ | 日付パス生成 |
| Seq.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/uuid/Seq.java` | ユーティリティ | シーケンス番号生成 |
| FileSizeLimitExceededException.java | `ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileSizeLimitExceededException.java` | 例外 | ファイルサイズ超過例外 |
| FileNameLengthLimitExceededException.java | `ruoyi-common/src/main/java/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.java` | 例外 | ファイル名長さ超過例外 |
| InvalidExtensionException.java | `ruoyi-common/src/main/java/com/ruoyi/common/exception/file/InvalidExtensionException.java` | 例外 | 拡張子不正例外 |
