Dart ディレクトリの状態監視

Dar言語StreamクラスtoSet()メソッドDart

Dartでディレクトリの状態を監視する方法についてメモ書き。

「ファイル作成」や「ファイル削除」のイベントを検知してバックエンド処理を行う場面って結構多い。
DartのDirectoryクラスに状態監視をするwatch()メソッド(公式サイト)があった。
早速試してみる。

watch()メソッドはプラットフォームのAPIを使用する。
公式サイトを見ると、プラットフォーム毎に違いがある。
ちなみに検証機はmacOS 13.2.1。

OSAPI説明
WindowsReadDirectoryChangesWディレクトリ監視のみサポート。
再帰的な監視がOK。
Linuxinotifyファイルとディレクトリ監視をサポート。
再起的な監視はNG。
ファイルを直接監視している場合、削除イベントが期待通り動かないかもしれない。
OS XFile System Events APIファイルとディレクトリ監視をサポート。
再起的な監視がOK。
以下の制限がある。
・watch()メソッド実行前に発生したイベントを検知する場合がある。
・短時間でイベントが複数発生した場合は順不同で検知する事がある。
・1つのディレクトリで複数の変更が行われた場合は、一つのFileSystemEventに纏められる事がある。

状態監視はwatch()メソッドが実行された時ではなく、Streamのlisten()メソッドを実行した後に開始される。listen()メソッドの終了条件は以下3つある。意図的に終了させる方法は1か2。

  1. StreamSubscriptionのcancel()メソッドを実行する。
  2. 監視対象が削除される。
  3. 監視中に何らかのエラーが発生する。

ファイル、ディレクトリの「移動(move)」は「削除(delete)と作成(create)」イベントとして検知する場合がある。

watch()メソッドの条件が分かったのでコーディングしてみる。

import 'dart:async';
import 'dart:io';

void main() {
  // ①監視対象のディレクトリが存在しない場合は作成
  if (Directory('dir').existsSync() == false) {
    Directory('dir').createSync();
  }

  // ②宣言時点ではStream<FileSystemEvent>型の実体が存在しないため、late修飾子で遅延させる
  late StreamSubscription<FileSystemEvent> sub;

  // ③Directoryクラスのwatchメソッドを実行
  Stream<FileSystemEvent> watchDir =
      Directory('dir').watch(events: FileSystemEvent.all);

  // ④StreamSubscriptionを使ってディレクトリの状態を監視
  sub = watchDir.listen(
    (event) {
      // ⑤Streamで取得したデータを加工して出力
      print('Event:$event\nPath:${event.path}\nType:${event.type}\n');

      // ⑥exit.txtが作成された場合、監視を終了する
      if (File('dir/exit.txt').existsSync()) {
        // ⑦StreamSubscriptionのcancel()メソッドで終了
        sub.cancel();
      }
    },
  );
}

以下解説。

import 'dart:async';
import 'dart:io';

先ずはStreamとDirectoryクラスを使うので、「dart:async」と「dart:io」をimport。

  // ①監視対象のディレクトリが存在しない場合は作成
  if (Directory('dir').existsSync() == false) {
    Directory('dir').createSync();
  }

①は監視ディレクトリの存在確認。
ディレクトリ削除でlisten()メソッドが終了するか試したい。
削除する度に作成し直すのが面倒なので、存在しない場合は作成するようにした。

  // ②宣言時点ではStreamSubscription<FileSystemEvent>型の実体が存在しないため、late修飾子で遅延させる
  late StreamSubscription<FileSystemEvent> sub;

  // ③Directoryクラスのwatchメソッドを実行
  Stream<FileSystemEvent> watchDir =
      Directory('dir').watch(events: FileSystemEvent.all);

  // ④StreamSubscriptionを使ってディレクトリの状態を監視
  sub = watchDir.listen(
    (event) {
      // ⑤Streamで取得したデータを加工して出力
      print('Event:$event\nPath:${event.path}\nType:${event.type}\n');

      // ⑥exit.txtが作成された場合、監視を終了する
      if (File('dir/exit.txt').existsSync()) {
        // ⑦StreamSubscriptionのcancel()メソッドで終了
        sub.cancel();
      }
    },
  );

②〜④がメインの監視処理。
③でwatch()メソッドを実行。引数は「events」と「recursive」がある。
eventsではFileSystemEventで監視対象を設定する。使用できる値は以下の通り。allを選択すると「作成(create)、削除(delete)、変更(modify)、移動(move)」全てが監視対象になる。

recursiveに「true」を設定すると、再帰的に監視を行う。デフォルトは「false」。

③で取得したwatchDir変数にはStreamクラスのインスタンスが格納されており、そこから④のlisten()メソッドを実行する。④のlisten()メソッドはStreamSubscription<FileSystemEvent>を返す。
⑦のlisten()を終了させるcancel()処理は、listen()が返すStreamSubscriptionクラスのメソッド。通常はlisten()自身が返す値を内部で使用できない。これを解決するため、事前に②でsubを宣言している。②の段階では実体を代入できないため、late修飾子で初期化を遅延させている。こちらはナレッジサイトstack overflowを参考にさせて頂いた。
ちなみに、「exit.txt」作成時にcancel()メソッドを実行するようにした。

⑤ではlisten()で取得したイベントをevent変数に代入し、print()でevent変数、pathプロパティ、typeプロパティを出力している。

それではdart runコマンドで処理を実行してみる。
ディレクトリ監視が開始される。

% dart run bin/dir1.dart 

ディレクトリを作成。

% mkdir test_dir

FileSystemCreateEventを検知する。
ディレクトリ作成は「Type」値「1」。

% dart run bin/dir1.dart  
Event:FileSystemCreateEvent('dir/test_dir')
Path:dir/test_dir
Type:1

今度は作成したtest_dirをmoveしてみる。

% mv test_dir mv_dir

削除(FileSystemDeleteEvent)と作成(FileSystemCreateEvent)を検知。
削除の「Type」値は「4」。

Event:FileSystemDeleteEvent('dir/test_dir')
Path:dir/test_dir
Type:4

Event:FileSystemCreateEvent('dir/mv_dir')
Path:dir/mv_dir
Type:1

次はファイルを作成し、変更を加える。

% touch test.txt
% ll > test.txt

ディレクトリもファイルも作成は「FileSystemCreateEvent」で検知する。「Type」値も同じ「1」。
変更は「FileSystemModifyEvent」で検知し、「contentChanged=true」が付与された。「Type」値は「2」

Event:FileSystemCreateEvent('dir/test.txt')
Path:dir/test.txt
Type:1

Event:FileSystemModifyEvent('dir/test.txt', contentChanged=true)
Path:dir/test.txt
Type:2

監視対象のdirディレクトリを削除。

% rm -fr dir

削除イベントを検知し、想定通りstreamのlisten()メソッドが停止する。

Event:FileSystemDeleteEvent('dir')
Path:dir
Type:4

%

次はサイズが大きいファイルを作成し、ファイル作成開始で検知されるのか、ファイル作成完了後に検知されるか試してみる。
先ずはスクリプトを実行し、監視対象のディレクトリを作成する。

% dart run bin/dir1.dart
Event:FileSystemCreateEvent('dir')
Path:dir
Type:1

5GBのファイルを作成。

% mkfile 5g 5G.txt

ファイル作成開始時に「FileSystemCreateEvent」を検知。
ファイル作成完了後、作成開始時と内容が変わるので「FileSystemModifyEvent」を検知した。
ファイル保存後にファイルを読み込んで処理を行うような場合は、ファイル作成完了後にフラグファイルを作成し、それを契機に読み込み処理を開始する等対策が必要。

Event:FileSystemCreateEvent('dir/5G.txt')
Path:dir/5G.txt
Type:1

Event:FileSystemModifyEvent('dir/5G.txt', contentChanged=true)
Path:dir/5G.txt
Type:2

最後にcancel()メソッドを試すため、exit.txtを作成。

% touch exit.txt

正常にlisten()が終了した。

Event:FileSystemCreateEvent('dir/exit.txt')
Path:dir/exit.txt
Type:1

%

コメント

タイトルとURLをコピーしました