ファイルの変更をリアルタイムで監視する

Recipe ID: fs-025

指定したファイルやディレクトリの変更(作成、更新、削除)をリアルタイムで検知する方法を解説します。
Tauri v2 では plugin-fswatch 関数または watchImmediate 関数を使用します。
(※ v1 では独立した tauri-plugin-fs-watch がありましたが、v2 では fs プラグインに統合されました。)

前提条件

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

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/**" },
        { "path": "$APPCONFIG" },
        { "path": "$APPCONFIG/**" }
      ]
    },
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPCONFIG/**" }]
    }
  ]
}

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

使用方法

@tauri-apps/plugin-fs から watch をインポートして使用します。

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

// 監視を開始
const unwatch = await watch('/path/to/directory', (event) => {
    console.log(event);
});

// 監視を停止する場合
// unwatch();

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

@tauri-apps/plugin-fswatch 関数を使用します。

ディレクトリ内の変更をログ出力する

watch 関数は非同期でクリーンアップ関数(unsubscribe function)を返します。
コンポーネントのアンマウント時などにこれを呼び出して監視を停止することを忘れないでください。

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

let unwatchFn: (() => void) | null = null;

async function startWatching() {
    try {
        const dataDir = await appLocalDataDir();
        console.log(`Watching directory: ${dataDir}`);

        // ディレクトリを再帰的に監視する場合、オプション { recursive: true } を指定(サポート状況による)
        // Tauri v2 の watch は再帰監視をサポートしています
        unwatchFn = await watch(
            dataDir,
            (event) => {
                // event: WatchEvent { type: WatchEventKind, paths: string[], attrs: unknown }
                // WatchEventKind は Rust の Enum がシリアライズされたものなので、
                // 単純な文字列 'create' の場合と、オブジェクト { create: { ... } } の場合があります。
                console.log('Watch Event:', event);

                if (typeof event === 'object' && event !== null && 'type' in event) {
                    const type = (event as any).type;
                    const paths = (event as any).paths as string[];

                    // 作成 (create) イベントの判定
                    let isCreate = false;
                    
                    if (typeof type === 'string' && type === 'create') {
                        isCreate = true;
                    } else if (typeof type === 'object' && type !== null && 'create' in type) {
                        isCreate = true;
                    }

                    if (isCreate) {
                        console.log("New file created!", paths);
                    }
                }
            },
            { recursive: true }
        );
        
    } catch (err) {
        console.error('Failed to start watcher:', err);
    }
}

function stopWatching() {
    if (unwatchFn) {
        unwatchFn();
        unwatchFn = null;
        console.log('Stopped watching.');
    }
}

設定ファイルの変更を検知してリロードする

設定ファイルのホットリロード機能を実装する例です。

import { watch, readTextFile } from '@tauri-apps/plugin-fs';
import { appConfigDir, join } from '@tauri-apps/api/path';

async function watchConfigFile() {
    const configDir = await appConfigDir();
    const configPath = await join(configDir, 'settings.json');

    await watch(
        configPath,
        async (event) => {
            console.log('Config Watch Event:', event);

            if (typeof event === 'object' && event !== null && 'type' in event) {
               const type = (event as any).type;
               
               // 変更 (modify), リネーム (rename), その他 (any) などを検知
               // 構造例: { type: { modify: { kind: 'any' } }, ... }
               let isModified = false;

               if (typeof type === 'string') {
                   if (['modify', 'rename', 'any', 'other'].includes(type)) isModified = true;
               } else if (typeof type === 'object' && type !== null) {
                   if ('modify' in type || 'rename' in type || 'any' in type || 'other' in type) {
                       isModified = true;
                   }
               }

               if (isModified) {
                   console.log('Config file changed, reloading...');
                   try {
                       const content = await readTextFile(configPath);
                       const settings = JSON.parse(content);
                       console.log('New settings:', settings);
                       // ここでアプリの状態を更新
                   } catch (e) {
                       console.error('Reload failed:', e);
                   }
               }
            }
        }
    );
}

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

Rust 側でファイルシステムの変更を監視するには、通常 notify クレートなどを使用します。
Tauri v2 の plugin-fs も内部で notify を使用していますが、バックエンドロジックとして実装する場合は自分で notify を導入するのが一般的です。

notify クレートを使用した監視機能の実装例

まず、Cargo.tomlnotify を追加する必要があります。

[dependencies]
notify = "6.1"
use tauri::State;
use notify::{Watcher, RecursiveMode, Event, RecommendedWatcher};
use std::sync::Mutex;
use std::path::Path;

// Watcher のインスタンスを保持するための State 構造体
struct WatcherState {
    watcher: Mutex<Option<RecommendedWatcher>>,
}

#[tauri::command]
fn start_watching_backend(
    state: State<'_, WatcherState>,
    path: String
) -> Result<(), String> {
    // イベントハンドラ
    let event_handler = move |res: notify::Result<Event>| {
        match res {
            Ok(event) => println!("changed: {:?}", event),
            Err(e) => println!("watch error: {:?}", e),
        }
    };

    // Watcherの作成
    let mut watcher = notify::recommended_watcher(event_handler)
        .map_err(|e| e.to_string())?;

    // 監視開始
    watcher.watch(Path::new(&path), RecursiveMode::Recursive)
        .map_err(|e| e.to_string())?;

    // State に保存。以前の Watcher があればドロップされて停止します。
    *state.watcher.lock().unwrap() = Some(watcher);
    
    Ok(())
}

#[tauri::command]
fn stop_watching_backend(state: State<'_, WatcherState>) -> Result<(), String> {
    // Watcher を None にしてドロップさせることで監視を停止
    *state.watcher.lock().unwrap() = None;
    println!("Watcher stopped.");
    Ok(())
}

fn main() {
    tauri::Builder::default()
        // State を初期化して登録
        .manage(WatcherState { watcher: Mutex::new(None) })
        .invoke_handler(tauri::generate_handler![
            start_watching_backend,
            stop_watching_backend
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

補足

  • バックエンドの実装: 内部的には notify (Rust crate) が使用されています。OS によってイベントの発火タイミングや種類に若干の差異がある場合があります。