# 機能設計書 24-ファイルダウンロード

## 概要

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

### 本機能の処理概要

本機能は、サーバー上に保存されたファイルをクライアントにダウンロードさせるための共通機能を提供する。Excelエクスポート結果の一時ファイルダウンロードと、アップロード済みリソースファイルのダウンロードの2種類のAPIを提供する。

**業務上の目的・背景**：業務システムでは帳票出力、データエクスポート、添付ファイル取得など、様々な場面でファイルのダウンロードが必要となる。特にExcelエクスポート機能では、サーバー側で生成したファイルをクライアントに配信する必要がある。共通のダウンロード機能を提供することで、開発効率の向上とセキュリティ対策の統一化を図る。

**機能の利用シーン**：
- ユーザー管理画面でユーザー一覧をExcelエクスポートする場面
- ロール管理画面でロール一覧をExcelエクスポートする場面
- 各種マスタ管理画面でデータをExcelエクスポートする場面
- 操作ログ、ログインログのExcelエクスポート
- アップロード済みの添付ファイルを取得する場面

**主要な処理内容**：
1. リクエストからファイル名またはリソースパスを取得
2. ファイル名の安全性検証（ディレクトリトラバーサル防止、拡張子検証）
3. 実ファイルパスの構築
4. HTTPレスポンスヘッダーの設定（Content-Type、Content-Disposition）
5. ファイル内容のストリーム出力
6. （オプション）ダウンロード後の一時ファイル削除

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

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

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | ユーザー管理一覧 | 補助機能 | ユーザーデータのExcelエクスポート処理 |
| 19 | ロール管理一覧 | 補助機能 | ロールデータのExcelエクスポート処理 |
| 34 | 岗位管理一覧 | 補助機能 | 役職データのExcelエクスポート処理 |
| 37 | 字典類型管理一覧 | 補助機能 | 辞書タイプデータのExcelエクスポート処理 |
| 41 | 字典データ管理一覧 | 補助機能 | 辞書データのExcelエクスポート処理 |
| 44 | パラメータ管理一覧 | 補助機能 | パラメータデータのExcelエクスポート処理 |
| 52 | 定時任務管理一覧 | 補助機能 | タスクデータのExcelエクスポート処理 |
| 56 | 定時任務ログ | 補助機能 | タスク実行ログのExcelエクスポート処理 |
| 58 | 操作ログ一覧 | 補助機能 | 操作ログのExcelエクスポート処理 |
| 60 | ログインログ一覧 | 補助機能 | ログインログのExcelエクスポート処理 |

## 機能種別

ファイルダウンロード / ストリーム出力 / セキュリティ検証

## 入力仕様

### 入力パラメータ

#### 一般ファイルダウンロード（GET /common/download）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| fileName | String | Yes | ダウンロードファイル名 | ディレクトリトラバーサル禁止、許可拡張子 |
| delete | Boolean | No | ダウンロード後にファイル削除するか（デフォルト: false） | - |

#### リソースファイルダウンロード（GET /common/download/resource）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| resource | String | Yes | リソースパス（/profile/から始まるパス） | ディレクトリトラバーサル禁止、許可拡張子 |

### 入力データソース

- HTTPリクエストパラメータ（GETパラメータ）

### 許可ファイル形式

ダウンロード可能なファイル形式はアップロードと同じ拡張子リスト：

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

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| ファイルバイナリ | byte[] | ファイル内容のバイナリストリーム |
| Content-Type | String | application/octet-stream |
| Content-Disposition | String | attachment; filename={encodedFileName} |

### 出力先

- HTTPレスポンスストリーム（ファイルバイナリ）

## 処理フロー

### 処理シーケンス

```
【一般ファイルダウンロード】
1. リクエスト受信
   └─ GET /common/download?fileName={fileName}&delete={delete}

2. ファイル名検証
   └─ FileUtils.checkAllowDownload(fileName)
       ├─ ディレクトリトラバーサルチェック（".."禁止）
       └─ 拡張子ホワイトリストチェック

3. 実ファイル名生成
   └─ System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1)

4. ファイルパス構築
   └─ RuoYiConfig.getDownloadPath() + fileName

5. レスポンスヘッダー設定
   ├─ Content-Type: application/octet-stream
   └─ Content-Disposition: attachment; filename={encodedFileName}

6. ファイル出力
   └─ FileUtils.writeBytes(filePath, response.getOutputStream())

7. 後処理（delete=trueの場合）
   └─ FileUtils.deleteFile(filePath)

【リソースファイルダウンロード】
1. リクエスト受信
   └─ GET /common/download/resource?resource={resource}

2. リソースパス検証
   └─ FileUtils.checkAllowDownload(resource)

3. ファイルパス構築
   ├─ localPath = RuoYiConfig.getProfile()
   └─ downloadPath = localPath + FileUtils.stripPrefix(resource)

4. ダウンロード名抽出
   └─ StringUtils.substringAfterLast(downloadPath, "/")

5. レスポンスヘッダー設定
   ├─ Content-Type: application/octet-stream
   └─ Content-Disposition: attachment; filename={downloadName}

6. ファイル出力
   └─ FileUtils.writeBytes(downloadPath, response.getOutputStream())
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B{ダウンロード種別}
    B -->|一般ファイル| C[GET /common/download]
    B -->|リソース| D[GET /common/download/resource]

    C --> E{checkAllowDownload?}
    E -->|No| F[例外: 非法ファイル]
    E -->|Yes| G[実ファイル名生成]
    G --> H[ダウンロードパス構築]

    D --> I{checkAllowDownload?}
    I -->|No| F
    I -->|Yes| J[プロファイルパス + リソースパス]

    H --> K[レスポンスヘッダー設定]
    J --> K

    K --> L[writeBytes でファイル出力]
    L --> M{delete = true?}
    M -->|Yes| N[ファイル削除]
    M -->|No| O[終了]
    N --> O
    F --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | ディレクトリトラバーサル禁止 | ファイル名/パスに".."が含まれる場合はエラー | 常時 |
| BR-02 | 拡張子ホワイトリスト | 許可された拡張子のファイルのみダウンロード可能 | 常時 |
| BR-03 | 一時ファイル削除オプション | delete=trueの場合、ダウンロード後にファイルを削除 | 一般ファイルダウンロード時 |
| BR-04 | ファイル名エンコード | ダウンロードファイル名はRFC 5987に従ってエンコード | 常時 |
| BR-05 | 一般ダウンロードのファイル名形式 | {timestamp}_{originalName}形式のファイル名から元ファイル名を抽出 | 一般ファイルダウンロード時 |

### 計算ロジック

**ダウンロードファイル名抽出ロジック（一般ファイル）**：
```
入力: 1704672000000_users_export.xlsx
処理: System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1)
出力: 1736323200000users_export.xlsx
```

**リソースパス変換ロジック**：
```
入力: /profile/upload/2026/01/08/sample.pdf
処理:
  localPath = /data/ruoyi/uploadPath
  downloadPath = localPath + stripPrefix(resource)
           = /data/ruoyi/uploadPath/upload/2026/01/08/sample.pdf
出力ファイル名: sample.pdf
```

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

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

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

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

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | セキュリティエラー | ファイル名に".."が含まれる | 「文件名称({})非法，不允许下载」メッセージをログ出力 |
| - | セキュリティエラー | 許可されていない拡張子 | 「文件名称({})非法，不允许下载」メッセージをログ出力 |
| - | FileNotFoundException | 指定ファイルが存在しない | FileNotFoundException例外をスロー |
| - | IOException | ファイル読み込み時のI/Oエラー | 「下载文件失败」ログ出力 |

### リトライ仕様

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

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

本機能はデータベース操作を行わないため、トランザクション管理は不要。
ファイル削除はダウンロード完了後に実行される。

## パフォーマンス要件

- 小ファイル（1MB以下）: 1s以内
- 中ファイル（1-10MB）: 5s以内
- 大ファイル（10-50MB）: 30s以内
- ストリーミング方式のため、大容量ファイルでもメモリ消費を抑制

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

- ディレクトリトラバーサル攻撃対策（".."パターンの拒否）
- 拡張子ホワイトリスト方式により、機密ファイル（.properties, .xml等）のダウンロードを防止
- ダウンロードディレクトリはWebルート外に配置
- ファイル名のパーセントエンコードによりヘッダーインジェクションを防止
- Content-Disposition: attachmentにより、ブラウザ内表示を防止

## 備考

- ダウンロード元ディレクトリ（一般）: RuoYiConfig.getDownloadPath()（{profile}/download/）
- ダウンロード元ディレクトリ（リソース）: RuoYiConfig.getProfile()
- Content-Type: application/octet-stream（バイナリファイルとして扱う）
- ファイル名エンコード: RFC 5987準拠のパーセントエンコード

---

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

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

### 推奨読解順序

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

まず、ダウンロード元ディレクトリの設定を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | RuoYiConfig.java | `ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java` | ダウンロードパスの設定 |

**読解のコツ**:
- `getDownloadPath()`は`getProfile() + "/download/"`を返す
- `getProfile()`はapplication.ymlで設定されたベースディレクトリ

**主要処理フロー（RuoYiConfig）**:
- **73-76行目**: getProfile() - ベースディレクトリ取得
- **110-115行目**: getDownloadPath() - ダウンロードディレクトリパス

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

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

**読解のコツ**:
- `fileDownload()`が一般ファイル、`resourceDownload()`がリソースファイル対応
- deleteパラメータでダウンロード後の削除を制御

**主要処理フロー**:
- **46-70行目**: fileDownload() - 一般ファイルダウンロード
- **51-54行目**: checkAllowDownload()でセキュリティチェック
- **55行目**: タイムスタンプ付き実ファイル名の生成
- **56行目**: ダウンロードパスの構築
- **58-60行目**: レスポンスヘッダー設定
- **61-64行目**: ファイル出力と削除処理
- **140-163行目**: resourceDownload() - リソースファイルダウンロード
- **145-148行目**: セキュリティチェック
- **150-152行目**: ローカルパスとリソースパスの結合
- **155-157行目**: レスポンスヘッダー設定とファイル出力

#### Step 3: ファイルユーティリティを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | FileUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java` | ファイル操作ユーティリティ |

**読解のコツ**:
- `checkAllowDownload()`がセキュリティの要
- `writeBytes()`がファイル出力の実装
- `setAttachmentResponseHeader()`がContent-Disposition設定

**主要処理フロー**:
- **39-66行目**: writeBytes() - ファイル内容をOutputStreamに出力
- **44-48行目**: ファイル存在チェック
- **50-55行目**: 1KBバッファでストリーミング出力
- **113-116行目**: stripPrefix() - リソースプレフィックスを除去
- **124-134行目**: deleteFile() - ファイル削除
- **153-169行目**: checkAllowDownload() - ダウンロード許可チェック
- **156-159行目**: ".."パターンの拒否
- **162-163行目**: 拡張子ホワイトリストチェック
- **213-226行目**: setAttachmentResponseHeader() - Content-Disposition設定
- **234-238行目**: percentEncode() - RFC 5987準拠のエンコード

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

```
CommonController
    │
    ├─ fileDownload(fileName, delete)
    │      │
    │      ├─ FileUtils.checkAllowDownload(fileName)
    │      │      ├─ StringUtils.contains(resource, "..")
    │      │      └─ ArrayUtils.contains(DEFAULT_ALLOWED_EXTENSION, ...)
    │      │
    │      ├─ RuoYiConfig.getDownloadPath() + fileName
    │      │
    │      ├─ response.setContentType(APPLICATION_OCTET_STREAM_VALUE)
    │      │
    │      ├─ FileUtils.setAttachmentResponseHeader(response, realFileName)
    │      │      └─ percentEncode(realFileName)
    │      │
    │      ├─ FileUtils.writeBytes(filePath, response.getOutputStream())
    │      │      ├─ new FileInputStream(file)
    │      │      └─ os.write(b, 0, length)
    │      │
    │      └─ [if delete] FileUtils.deleteFile(filePath)
    │
    └─ resourceDownload(resource)
           │
           ├─ FileUtils.checkAllowDownload(resource)
           │
           ├─ RuoYiConfig.getProfile()
           │
           ├─ FileUtils.stripPrefix(resource)
           │      └─ substringAfter(filePath, RESOURCE_PREFIX)
           │
           ├─ response.setContentType(APPLICATION_OCTET_STREAM_VALUE)
           │
           ├─ FileUtils.setAttachmentResponseHeader(response, downloadName)
           │
           └─ FileUtils.writeBytes(downloadPath, response.getOutputStream())
```

### データフロー図

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

【一般ファイルダウンロード】
fileName ──────────▶ CommonController.fileDownload() ──▶ バイナリストリーム
delete                      │                                │
                            ├─▶ checkAllowDownload()         └─▶ Content-Disposition
                            │      └─ セキュリティ検証
                            │
                            ├─▶ getDownloadPath() + fileName
                            │      └─ {profile}/download/{fileName}
                            │
                            └─▶ writeBytes()
                                   └─ FileInputStream → OutputStream

【リソースファイルダウンロード】
resource ─────────▶ CommonController.resourceDownload() ──▶ バイナリストリーム
(/profile/...)              │                                  │
                            ├─▶ checkAllowDownload()           └─▶ Content-Disposition
                            │
                            ├─▶ getProfile() + stripPrefix(resource)
                            │      └─ {profile}/{path}
                            │
                            └─▶ writeBytes()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| CommonController.java | `ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java` | コントローラー | ダウンロードエンドポイント |
| FileUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java` | ユーティリティ | ファイル操作、セキュリティチェック |
| RuoYiConfig.java | `ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java` | 設定 | ダウンロードパス設定 |
| MimeTypeUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java` | ユーティリティ | 許可ファイル形式定義 |
| FileTypeUtils.java | `ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java` | ユーティリティ | ファイルタイプ判定 |
| Constants.java | `ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java` | 定数 | RESOURCE_PREFIX定数 |
