ファイルをダウンロードして保存する

Recipe ID: net-005

インターネット上のファイルをダウンロードし、ローカルのファイルシステムに保存する方法を解説します。
Tauri の plugin-http でデータを取得し、plugin-fs を使用してバイナリデータとして保存します。

前提条件

この機能を使用するには、以下の2つのプラグインが必要です。

1. @tauri-apps/plugin-http (通信用)
2. @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 のドメインを明示的に指定する必要があります。

以下の例では、httpexample.com を許可し、fs でダウンロードディレクトリへの書き込み等を許可しています(fs の権限ついてはファイルシステム関連のレシピも参照してください)。

{
  "permissions": [
    {
      "identifier": "http:default",
      "allow": [
        { "url": "https://example.com/*" }
      ],
      "deny": []
    },
    // ファイルへの書き込みを許可するには fs:allow-write-file を使用します
    {
      "identifier": "fs:allow-write-file",
      "allow": [{ "path": "$DOWNLOAD/**" }],
      "deny": []
    }
  ]
}

使用方法

fetch でレスポンスを取得し、arrayBuffer() メソッドでバイナリデータとして取り出します。
その後、Uint8Array に変換して writeFile で保存します。

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

const response = await fetch('https://example.com/file.zip');
const buffer = await response.arrayBuffer();
const data = new Uint8Array(buffer);

await writeFile('file.zip', data, { baseDir: BaseDirectory.Download });

コード例

例1: 画像をダウンロードしてダウンロードフォルダに保存する

ユーザーのダウンロードフォルダに画像を保存する例です。

import { fetch } from '@tauri-apps/plugin-http';
import { writeFile } from '@tauri-apps/plugin-fs';
import { downloadDir, join } from '@tauri-apps/api/path';

async function downloadImage(url: string, filename: string): Promise<string | undefined> {
    try {
        // 1. ダウンロード
        const response = await fetch(url);
        if (!response.ok) throw new Error('Download failed');
        
        const buffer = await response.arrayBuffer();
        const start = Date.now();

        // 2. 保存パスの構築
        const dir = await downloadDir();
        const path = await join(dir, filename);

        // 3. ファイル書き込み
        await writeFile(path, new Uint8Array(buffer));
        
        console.log(`Saved to ${path} (${buffer.byteLength} bytes) in ${Date.now() - start}ms`);
        return path;

    } catch (err) {
        console.error('Error downloading image:', err);
        return undefined;
    }
}

例2: プログレス(進捗)表示付きダウンロード

fetchbody (ReadableStream) を使用して、ダウンロードの進捗状況を計算しながら読み込む例です。
(※ 最終的な保存は一括で行う簡易実装です)

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

async function downloadWithProgress(url: string): Promise<Uint8Array | undefined> {
    const response = await fetch(url);
    if (!response.body) {
         console.error('Response body is null');
         return undefined;
    }

    const contentLength = response.headers.get('Content-Length');
    const total = contentLength ? parseInt(contentLength, 10) : 0;
    
    const reader = response.body.getReader();
    const chunks: Uint8Array[] = [];
    let received = 0;

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        if (value) {
            chunks.push(value);
            received += value.length;
        }
        
        if (total > 0) {
            const percent = ((received / total) * 100).toFixed(1);
            console.log(`Progress: ${percent}% (${received}/${total})`);
        } else {
            console.log(`Received: ${received} bytes`);
        }
    }

    // 全チャンクを結合
    const all = new Uint8Array(received);
    let position = 0;
    for (const chunk of chunks) {
        all.set(chunk, position);
        position += chunk.length;
    }

    return all; // これを writeFile に渡す
}

注意点

* メモリ使用量: 上記の方法はファイルを一度メモリ(ArrayBuffer)に展開するため、数百MBを超えるような巨大なファイルのダウンロードには適していません。
* 巨大ファイルの場合: 巨大なファイルを扱う場合は、JavaScript 側で完結させようとせず、Rust 側で reqweststd::fs を使用したストリーミングダウンロードコマンド(download_file コマンド等)を作成して呼び出すことを強く推奨します。これにより、メモリ消費を最小限に抑えられます。