ファイル変更イベントを受け取って処理する

Recipe ID: fs-026

ファイルの監視 (watch) で発生する大量のイベントを効率的に処理する方法を解説します。
ファイルシステムイベントは「作成」「変更」などが短期間に連続して発生することが多いため、デバウンス(Debounce)処理やイベントのフィルタリングが重要になります。

前提条件

この機能を使用するには、@tauri-apps/plugin-fs プラグインが必要です。
また、解説には lodash などのユーティリティライブラリを使用する例を含みますが、標準の setTimeout でも実装可能です。

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

プロジェクトのルートディレクトリで以下のコマンドを実行してプラグインを追加します。

npm run tauri add fs

このレシピではファイルの変更監視を行うため、src-tauri/Cargo.tomlwatch 機能を有効にする必要があります。

[dependencies]
tauri-plugin-fs = { version = "2.0", features = ["watch"] }

2. Permissions (権限) の設定

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

{
  "permissions": [
    ...,
    {
      "identifier": "fs:allow-watch",
      "allow": [{ "path": "$APPLOCALDATA" }, { "path": "$APPLOCALDATA/**" }]
    }
  ]
}

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

使用方法とパターン

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

イベントの種類でフィルタリングする

OS によっては、ファイルの保存時に renamemodify が同時に発生したり、一時ファイルが作成されたりします。
必要なイベントタイプだけを処理対象にします。

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

async function watchSpecificEvents(path: string) {
    await watch(path, (event) => {
        // event.type は 'create', 'modify', 'remove', 'rename', 'any' など
        // 文字列の場合と、オブジェクト(例: { remove: { kind: 'any' } })の場合があります
        
        // TypeScript の型定義と実際のランタイムでの値(Rustからのシリアライズ)が異なる場合があるため、any にキャストして判定
        const type = event.type as any;
        const isRemove = type === 'remove' || (typeof type === 'object' && 'remove' in type);

        // 削除イベントは無視する場合
        if (isRemove) return;

        console.log('Update detected:', event.paths);
    });
}

デバウンス処理(連続イベントの抑制)

エディタがファイルを保存する際、一瞬で複数回の書き込みイベントが発生することがあります。これを1回の処理にまとめるためにデバウンスを使用します。

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

// 簡易デバウンス関数
function debounce(func: Function, wait: number) {
    let timeout: ReturnType<typeof setTimeout>;
    return (...args: any[]) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
    };
}

async function watchWithDebounce(path: string) {
    const processChange = debounce((paths: string[]) => {
        console.log('Processing changes for:', paths);
        // ここで重い処理(リロードやビルドなど)を実行
    }, 500); // 500ms 待機

    await watch(path, (event) => {
        console.log('Raw event:', event.type);
        processChange(event.paths as string[]);
    });
}

特定の拡張子のみを対象にする

ディレクトリ監視の場合、関係のないファイル(.tmp.DS_Store など)の変更も検知されます。
path モジュールや文字列操作で拡張子を確認し、フィルタリングします。

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

async function watchTextFiles(dirPath: string) {
    await watch(dirPath, (event) => {
        // 変更されたパスのリストをチェック
        for (const filePath of event.paths) {
            // 拡張子判定(簡易実装)
            if (filePath.endsWith('.txt') || filePath.endsWith('.md')) {
                 console.log('Text file changed:', filePath);
                 // 処理を実行
            }
        }
    }, { recursive: true });
}

補足

  • イベントの多重発生: modify イベントは、ファイルのオープン、書き込み、クローズなどで複数回発生することがあります。デバウンスはこれらを「人間が意図した1回の保存」として扱うために非常に有効です。
  • UI更新: 頻繁なファイル変更イベントをそのまま React/Vue のステート更新につなげると、UI がちらついたりパフォーマンスが低下したりするため、必ずデバウンスやスロットリングを挟むようにしましょう。