サブフォルダも含めて再帰的に一覧取得する

Recipe ID: fs-014

フォルダの中にあるサブフォルダ、さらにその中にあるファイル...と、深い階層まですべてのファイルを走査して取得する方法を紹介します。
ファイルバックアップや、プロジェクトフォルダ全体の構造を解析する場合などに使用します。

前提条件

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

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

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

npm run tauri add fs

2. Permissions (権限) の設定

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

{
  "permissions": [
    ...,
    {
      "identifier": "fs:allow-read-dir",
      "allow": [{ "path": "$DOCUMENT" }, { "path": "$DOCUMENT/**" }]
    }
  ]
}

※ 上記の例では $DOCUMENTBaseDirectory.Document)そのもの、およびその配下のすべてのディレクトリ一覧取得を許可しています。アクセスするディレクトリに応じて、パス変数を指定してください。スコープが設定されていないディレクトリにはアクセスできません。

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

@tauri-apps/plugin-fsreadDir 関数を使用し、再帰的に呼び出します。

シンプルな再帰的ファイル走査

指定したディレクトリ以下のすべてのファイルパスをコンソールに出力します。

import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs';

// 再帰的にファイルを探索する関数
async function scanFilesRecursively(path: string) {
  // 指定パスのエントリ一覧を取得
  const entries = await readDir(path, {
    baseDir: BaseDirectory.Document,
  });

  for (const entry of entries) {
    // パスを結合(最初の path が空文字の場合はスラッシュをつけないように調整)
    const fullPath = path ? `${path}/${entry.name}` : entry.name;
    
    if (entry.isDirectory) {
      console.log(`[DIR]  ${fullPath}`);
      // ディレクトリなら再帰的に自分自身を呼び出す
      await scanFilesRecursively(fullPath);
    } else {
      console.log(`[FILE] ${fullPath}`);
    }
  }
}

// 実行例
async function main() {
    // 'projects' フォルダ以下を走査
    await scanFilesRecursively('projects');
    
    // ドキュメントフォルダ全体を走査する場合は空文字を渡す
    // await scanFilesRecursively('');
}
main();

ツリー構造のデータを構築する

UIなどでフォルダツリーを表示するために、ファイル構造をオブジェクト(JSON)形式で取得する例です。

import { readDir, BaseDirectory } from '@tauri-apps/plugin-fs';

// ツリー構造の型定義
interface FileNode {
  name: string;
  path: string;
  kind: 'file' | 'directory';
  children?: FileNode[]; // ディレクトリの場合のみ子要素を持つ
}

async function buildFileTree(path: string): Promise<FileNode[]> {
  const entries = await readDir(path, {
    baseDir: BaseDirectory.Document,
  });

  const nodes: FileNode[] = [];

  // 並列で処理して高速化(Promise.all)
  await Promise.all(entries.map(async (entry) => {
    const childPath = path ? `${path}/${entry.name}` : entry.name;
    const node: FileNode = {
      name: entry.name,
      path: childPath,
      kind: entry.isDirectory ? 'directory' : 'file',
    };

    if (entry.isDirectory) {
      // 再帰的に子要素を取得して children に格納
      node.children = await buildFileTree(childPath);
    }

    nodes.push(node);
  }));

  // 名前順にソートして返す(オプション)
  return nodes.sort((a, b) => a.name.localeCompare(b.name));
}

// 使用例
// const tree = await buildFileTree('projects');
// console.log(JSON.stringify(tree, null, 2));

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

Rust では walkdir クレートを使うのが最も簡単で確実ですが、ここでは標準ライブラリのみで実装する例を示します。

再帰的なディレクトリ走査

use std::fs;
use std::path::Path;

#[tauri::command]
fn list_all_files(path: String) -> Result<Vec<String>, String> {
    let mut files = Vec::new();
    visit_dirs(Path::new(&path), &mut files).map_err(|e| e.to_string())?;
    Ok(files)
}

// ヘルパー関数: 再帰的にディレクトリを訪問
fn visit_dirs(dir: &Path, cb: &mut Vec<String>) -> std::io::Result<()> {
    if dir.is_dir() {
        for entry in fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                visit_dirs(&path, cb)?;
            } else {
                if let Some(path_str) = path.to_str() {
                    cb.push(path_str.to_string());
                }
            }
        }
    }
    Ok(())
}

補足

  • 再帰処理: readDir 関数自体は「指定したディレクトリの直下」しか返しません。深い階層を取得するには、プログラミングによる再帰呼び出しが必要です。
  • Scope の **: Tauri の Scope 設定で path/* は「直下のみ」、path/ は「再帰的にすべて」を意味します。再帰処理を行う場合は必ず を使用してください。
  • パフォーマンス: ファイル数が膨大な場合、Promise.all による並列化などが有効ですが、メモリ使用量に注意してください。
  • Rustでの実装: 本格的なファイル探索には walkdir クレートの導入を検討してください。ループ検出や並列処理(ignore クレートなど)が容易になります。