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

## 概要

本ドキュメントは、InvenTreeモバイルアプリにおけるファイルダウンロード機能の設計を記述するものである。サーバーからファイルをダウンロードし、端末上で開く機能の仕様を定義する。

### 本機能の処理概要

本機能は、InvenTreeサーバーからファイル（PDF、画像、文書等）をダウンロードし、デバイスの一時ディレクトリに保存した後、システムのデフォルトアプリで開く機能を提供する。添付ファイルの閲覧やドキュメントの確認などに使用される。

**業務上の目的・背景**：在庫管理システムでは、パーツのデータシート、作業手順書、品質検査報告書など様々な添付ファイルが登録される。これらのファイルをモバイルデバイスで直接閲覧できることで、現場作業の効率が向上する。

**機能の利用シーン**：
- パーツに添付されたデータシートPDFを閲覧したい場合
- 在庫アイテムの画像を拡大表示したい場合
- 発注書に添付された見積書を確認したい場合

**主要な処理内容**：
1. ダウンロードURLからファイル名を抽出
2. 一時ディレクトリへのローカルパスを構築
3. HTTPリクエストでファイルをダウンロード
4. ローカルファイルとして保存
5. open_filexパッケージでシステムのデフォルトアプリで開く

**関連システム・外部連携**：
- InvenTreeサーバー：ファイルの取得元
- デバイスファイルシステム：一時ディレクトリへの保存
- システムアプリ：ファイルタイプに対応したアプリで開く

**権限による制御**：ダウンロード元URLへのアクセス権限はサーバー側で制御される。

## 関連画面

本機能は特定の画面に紐づかず、添付ファイル表示など複数の機能から呼び出される汎用機能である。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | 添付ファイル一覧 | 呼び出し元 | 添付ファイルタップ時にダウンロード・表示 |

## 機能種別

ファイル操作 / 外部アプリ連携

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| url | String | Yes | ダウンロードするファイルのURL | 空文字列不可 |
| openOnDownload | bool | No | ダウンロード後にファイルを開くか | デフォルト: true |

### 入力データソース

- API応答：添付ファイル情報に含まれるURL
- 画面操作：添付ファイルタイルのタップ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| localFile | File | ローカルに保存されたファイル |

### 出力先

- デバイス一時ディレクトリ：ダウンロードしたファイル
- システムアプリ：ファイルを開いて表示

## 処理フロー

### 処理シーケンス

```
1. downloadFile()呼び出し
   └─ URLの検証
2. ローカルパス構築
   └─ getTemporaryDirectory()で一時ディレクトリ取得
   └─ URLからファイル名を抽出
3. HTTPリクエスト
   └─ HttpClient.openUrl()でリクエストを開く
   └─ 認証ヘッダーを設定
4. レスポンス処理
   └─ ステータスコード確認（200のみ成功）
   └─ レスポンスボディをバイト配列に変換
   └─ File.writeAsBytes()でローカル保存
5. ファイルを開く
   └─ openOnDownload=trueの場合、OpenFilex.open()で開く
```

### フローチャート

```mermaid
flowchart TD
    A[downloadFile呼び出し] --> B{URLが空?}
    B -->|Yes| C[終了]
    B -->|No| D[一時ディレクトリ取得]
    D --> E[ローカルパス構築]
    E --> F[URL検証]
    F --> G{URL有効?}
    G -->|No| H[エラー表示]
    H --> C
    G -->|Yes| I[ローディング表示]
    I --> J[HTTPリクエスト送信]
    J --> K{ステータス200?}
    K -->|No| L[ステータスエラー表示]
    L --> M[ローディング非表示]
    K -->|Yes| N[バイト配列変換]
    N --> O[ローカルファイル保存]
    O --> P{openOnDownload?}
    P -->|Yes| Q[OpenFilex.open]
    P -->|No| R[ローディング非表示]
    Q --> R
    M --> C
    R --> C
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-70-01 | URL必須 | URLが空の場合は処理しない | 常時 |
| BR-70-02 | 一時ディレクトリ保存 | ファイルは一時ディレクトリに保存 | 常時 |
| BR-70-03 | ファイル名抽出 | URLの最後のパスセグメントをファイル名として使用 | 常時 |
| BR-70-04 | 認証ヘッダー | APIトークンを認証ヘッダーに設定 | 常時 |
| BR-70-05 | デフォルト開く | openOnDownload未指定時はファイルを開く | デフォルト動作 |

### 計算ロジック

該当なし

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

本機能ではデータベース操作は発生しない。

### ファイル操作

| 操作 | 対象 | 概要 |
|-----|------|------|
| 書き込み | 一時ディレクトリ | ダウンロードしたファイルを保存 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | URL空 | URLが空文字列 | 処理を中止 |
| - | 無効なホスト | URLのパースに失敗 | invalidHostエラー表示 |
| - | 接続拒否 | SocketException | connectionRefusedエラー表示 |
| - | タイムアウト | TimeoutException | timeoutエラー表示 |
| - | 証明書エラー | HandshakeException | serverCertificateErrorエラー表示 |
| - | HTTPエラー | ステータスコード!=200 | ステータスコードエラー表示 |
| - | ダウンロードエラー | その他の例外 | downloadErrorエラー表示 |

### リトライ仕様

リトライは実装されていない。ユーザーが再度タップすることで再試行可能。

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

本機能ではトランザクションは発生しない。

## パフォーマンス要件

- 接続タイムアウト：10秒
- ダウンロードタイムアウト：ファイルサイズに依存
- ローディングオーバーレイ表示：ダウンロード中

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

- 認証トークンをヘッダーに含めてリクエスト
- strictHttps設定に基づいて証明書検証を制御
- 一時ディレクトリに保存（システムによる自動クリーンアップ対象）

## 備考

- open_filexパッケージを使用してシステムアプリでファイルを開く
- path_providerパッケージで一時ディレクトリを取得
- ローディングオーバーレイでダウンロード中を視覚的に表示
- consolidateHttpClientResponseBytesでレスポンスをバイト配列に変換

---

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

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

### 推奨読解順序

#### Step 1: ダウンロード関数を理解する

api.dartのdownloadFileメソッドを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | api.dart | `lib/api.dart` | downloadFileメソッド |

**読解のコツ**:
- **887-985行目**: `downloadFile`メソッドの全体
- **888-890行目**: URLが空の場合は早期リターン
- **893行目**: `getTemporaryDirectory()`で一時ディレクトリを取得
- **895行目**: URLからファイル名を抽出（最後のパスセグメント）
- **897行目**: ローカルパスを構築
- **899-910行目**: URI検証とホストチェック

#### Step 2: HTTPリクエストを理解する

ファイルダウンロードのHTTPリクエスト処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | api.dart | `lib/api.dart` | HTTPリクエスト部分 |

**主要処理フロー**:
- **918行目**: `createClient`でHttpClientを作成
- **920行目**: `showLoadingOverlay`でローディング表示
- **923-931行目**: `client.openUrl("GET", _uri)`でリクエストを開く
- **929-931行目**: `defaultHeaders()`で認証ヘッダーを設定

#### Step 3: レスポンス処理を理解する

ダウンロードレスポンスの処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | api.dart | `lib/api.dart` | レスポンス処理部分 |

**主要処理フロー**:
- **952-953行目**: リクエストをcloseしてレスポンスを取得
- **955行目**: ステータスコード200を確認
- **956行目**: `consolidateHttpClientResponseBytes`でバイト配列に変換
- **958行目**: `File(local_path)`でFileオブジェクト作成
- **960行目**: `writeAsBytes`でローカル保存
- **962-964行目**: `openOnDownload=true`の場合、`OpenFilex.open`で開く

#### Step 4: エラーハンドリングを理解する

各種エラーのハンドリングを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | api.dart | `lib/api.dart` | エラーハンドリング部分 |

**主要処理フロー**:
- **932-935行目**: SocketException（接続拒否）
- **936-938行目**: TimeoutException（タイムアウト）
- **939-943行目**: HandshakeException（証明書エラー）
- **944-950行目**: その他の例外
- **966行目**: ステータスコードエラー
- **969-982行目**: レスポンス処理中のエラー

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

```
添付ファイルタップ等
    │
    └─ InvenTreeAPI().downloadFile(url, openOnDownload: true)
           │
           ├─ getTemporaryDirectory()
           │      └─ 一時ディレクトリパス取得
           │
           ├─ Uri.tryParse(makeUrl(url))
           │      └─ URL検証
           │
           ├─ createClient(url, strictHttps)
           │      └─ HttpClient作成
           │
           ├─ showLoadingOverlay()
           │
           ├─ client.openUrl("GET", _uri)
           │      └─ HTTPリクエスト
           │
           ├─ defaultHeaders()
           │      └─ 認証ヘッダー設定
           │
           ├─ request.close()
           │      └─ レスポンス取得
           │
           ├─ consolidateHttpClientResponseBytes()
           │      └─ バイト配列変換
           │
           ├─ File(local_path).writeAsBytes()
           │      └─ ローカル保存
           │
           ├─ OpenFilex.open(local_path)
           │      └─ システムアプリで開く
           │
           └─ hideLoadingOverlay()
```

### データフロー図

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

                      ┌─────────────────────┐
ダウンロードURL ─────▶│ URL検証・パース     │
                      └──────────┬──────────┘
                                 │
                                 ▼
                      ┌─────────────────────┐
                      │ HTTP GETリクエスト  │
                      │ - 認証ヘッダー      │◀──────── InvenTreeサーバー
                      └──────────┬──────────┘         (ファイルデータ)
                                 │
                                 ▼
                      ┌─────────────────────┐
                      │ バイト配列変換      │
                      │ - consolidateBytes  │
                      └──────────┬──────────┘
                                 │
                                 ▼
                      ┌─────────────────────┐
                      │ ローカル保存        │──────▶ 一時ディレクトリ
                      │ - writeAsBytes      │        /{filename}
                      └──────────┬──────────┘
                                 │
                                 ▼
                      ┌─────────────────────┐
                      │ OpenFilex.open      │──────▶ システムアプリ
                      │ - デフォルトアプリ  │        (PDF/画像等)
                      └─────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| api.dart | `lib/api.dart` | ソース | downloadFileメソッド実装 |
| progress.dart | `lib/widget/progress.dart` | ソース | ローディングオーバーレイ |
| dialogs.dart | `lib/widget/dialogs.dart` | ソース | エラーダイアログ |
| l10.dart | `lib/l10.dart` | ソース | 多言語テキスト取得ヘルパー |
