サーバーにファイルをアップロードする

Recipe ID: net-006

ローカルにあるファイルをサーバーへアップロード(Multipart POST)する方法を解説します。
Tauri アプリでは、input type="file" から取得したファイルオブジェクトを直接送信するだけでなく、plugin-fs を使ってファイルシステム上の任意のパスからファイルを読み込み、それをアップロードすることも可能です。

前提条件

この機能を使用するには、@tauri-apps/plugin-http プラグインが必要です。
ファイルシステムから直接読み込んでアップロードする場合は @tauri-apps/plugin-fs も必要です。

1. プラグインのインストール

npm run tauri add http
# 任意のパスから読み込む場合
npm run tauri add fs

2. Permissions (権限) の設定

src-tauri/capabilities/default.jsonhttp:default 権限と fs:default 権限を追加します。
Tauri v2 ではセキュリティの観点から allow: [{ "url": "*" }] のような無制限なワイルドカード指定は許可されていない場合があるため、使用する API のドメインを明示的に指定する必要があります。

以下の例では、httpapi.example.com などを許可し、fs でログファイルの読み込み等を許可しています。

{
  "permissions": [
    {
      "identifier": "http:default",
      "allow": [
        { "url": "https://api.example.com/*" },
        { "url": "https://upload.example.com/*" },
        { "url": "https://api.myapp.com/*" }
      ],
      "deny": []
    },
    // ファイルシステムから読み込むために fs:allow-read-file を使用します
    {
      "identifier": "fs:allow-read-file",
      "allow": [{ "path": "$APPLOCALDATA/**" }],
      "deny": []
    }
  ]
}
注意: 開発中は便利だからと広範囲な許可をしたくなりますが、本番ビルドでは実際に使用するドメインやディレクトリのみを許可することが強く推奨されます。

使用方法

Web 標準の FormData オブジェクトを使用します。
fetchbodyFormData を渡すと、Content-Type ヘッダー(multipart/form-data; boundary=...)は自動的に設定されます。

パターン1: ファイル選択ダイアログ (<input type="file">) からのアップロード

これは通常の Web アプリケーションと全く同じ方法で実装できます。特別な権限は不要です。

import { fetch } from '@tauri-apps/plugin-http';

async function uploadSelectedFile(): Promise<void> {
    const fileInput = document.getElementById('fileInput') as HTMLInputElement;
    if (!fileInput || !fileInput.files) return;

    const file = fileInput.files[0];
    
    if (!file) return;

    const formData = new FormData();
    formData.append('file', file);

    await fetch('https://api.example.com/upload', {
        method: 'POST',
        body: formData
    });
}

パターン2: ファイルパスを指定してアップロード

Tauri の強みである、任意のファイルパスを使用したアップロードです。
plugin-fs でファイルを読み込み、Blob オブジェクトに変換して FormData に追加します。

```typescript
import { fetch } from '@tauri-apps/plugin-http';
import { readFile, BaseDirectory } from '@tauri-apps/plugin-fs';

async function uploadByPath(fileName: string): Promise<void> {
    // 1. ファイルをバイナリとして読み込む (AppLocalDataからの相対パス)
    // ファイル名だけを渡し、baseDir オプションで場所を指定します。
    // 絶対パスを渡すと、baseDirと結合されて重複パスエラーになる可能性があります。
    const content = await readFile(fileName, { baseDir: BaseDirectory.AppLocalData });
    
    // 2. Blobを作成(必要ならMIMEタイプを指定)
    const blob = new Blob([content], { type: 'application/octet-stream' });
    
    // 3. FormDataに追加
    const formData = new FormData();
    // 第3引数でファイル名を指定できます
    formData.append('file', blob, fileName); 

    // 4. 送信
    await fetch('https://upload.example.com', {
        method: 'POST',
        body: formData
    });
}

注意点

* Content-Type ヘッダー: FormData を送信する際、手動で Content-Type: multipart/form-data を設定しないでください。ブラウザ(およびプラグイン)が自動的に正しい boundary パラメータ付きのヘッダーを生成します。手動で設定するとアップロードが失敗します。
* メモリ使用量: plugin-fsreadFile はファイル全体をメモリに読み込みます。GB単位のファイルをアップロードする場合は、この方法は避けてください。残念ながら v2 の JS API ではファイルパスからのストリーミングアップロードは標準で提供されていないため、巨大ファイルのアップロードには Rust コマンドを作成し、reqwest::multipart のファイルストリーム機能を使用することを推奨します。