巨大なファイルを少しずつ読み込む

Recipe ID: fs-027

数GBを超えるような巨大なファイルを開く際、readFile で一度にすべてをメモリに読み込むと、アプリケーションがクラッシュしたり動作が重くなったりする可能性があります。
Tauri v2 では、ファイルハンドルを開いてストリームとして読み込むか、テキストファイルであれば readTextFileLines を使用して行ごとに効率的に処理することが可能です。

前提条件

この機能を使用するには、@tauri-apps/plugin-fs プラグインが必要です。

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
  "permissions": [
    ...,
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPLOCALDATA/**" }]
    },
    {
      "identifier": "fs:allow-read-text-file-lines",
      "allow": [{ "path": "$APPLOCALDATA/**" }]
    }
  ]
}

※ 上記の例では $APPLOCALDATABaseDirectory.AppLocalData)配下でのテキスト読み込みを許可しています。アクセスするディレクトリに応じて、パス変数を指定してください。スコープが設定されていないディレクトリにはアクセスできません。

1. フロントエンドから作成する (TypeScript)

readTextFileLines (テキストファイル向け)

巨大なログファイルやCSVなどを、メモリ消費を抑えながら先頭から順に処理する場合に最適です。
非同期イテレータを返すため、for await...of ループで処理できます。

import { readTextFileLines } from '@tauri-apps/plugin-fs';

async function readLines() {
    const lines = await readTextFileLines('/path/to/large_file.txt');

    for await (const line of lines) {
        console.log(line);
        // ループを抜けると自動的にファイルが閉じられます
        if (line.includes('TARGET_STRING')) break;
    }
}

2. バックエンドから作成する (Rust)

JavaScript での処理が遅い場合やバイナリ操作が必要な場合は、Rust コマンドを作成します。
std::io::BufReader を使用し、少しずつフロントエンドに送信するのが最も確実で高速な方法です。

Rust でのストリーム読み込み

// src-tauri/src/lib.rs
use std::fs::File;
use std::io::{BufReader, Read};
use tauri::{command, Emitter};

#[command]
fn read_file_chunked(window: tauri::Window, path: String, chunk_size: usize) -> Result<(), String> {
    let file = File::open(path).map_err(|e| e.to_string())?;
    let mut reader = BufReader::new(file);
    let mut buffer = vec![0; chunk_size];

    loop {
        let n = reader.read(&mut buffer).map_err(|e| e.to_string())?;
        if n == 0 { break; } // EOF

        // フロントエンドにイベントとしてチャンクを送信
        // バイナリデータは Vec<u8> なので自動的にシリアライズされます
        window.emit("file-data", &buffer[..n]).map_err(|e| e.to_string())?;
    }
    
    window.emit("file-end", ()).map_err(|e| e.to_string())?;
    Ok(())
}

フロントエンドでの受信 (TypeScript)

上記 Rust コマンドからのイベントを受け取る例です。

import { listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api/core';

async function startStreamRead(path: string) {
    // データ受信リスナー
    const unlistenData = await listen<Uint8Array>('file-data', (event) => {
        const chunk = event.payload; // Uint8Array
        console.log(`Received chunk of size: ${chunk.length}`);
        // ここでデコードや結合を行う
    });

    // 完了受信リスナー
    const unlistenEnd = await listen('file-end', () => {
        console.log('Reading finished');
        unlistenData();
        unlistenEnd();
    });

    // コマンド呼び出し
    await invoke('read_file_chunked', { path, chunkSize: 1024 * 64 });
}

補足

  • パフォーマンス: 巨大なファイルを扱う場合は、JavaScript のメモリ使用量(GC)に注意が必要です。不要になったオブジェクト(読み込んだ文字列など)は早めに解放するよう設計してください。
  • WebViewの制限: 一度に emit で送信できるデータサイズや頻度には限界があります(IPCのオーバーヘッド)。あまりに細かいチャンクで大量に送ると逆に遅くなるため、chunk_size は 64KB ~ 1MB 程度で調整すると良いでしょう。