LoginSignup
3

More than 5 years have passed since last update.

posted at

updated at

Promiseを使ってディレクトリを再帰的に辿ってファイル一覧を取得する

参考にしたのはディレクトリを再帰的にたどってファイル一覧を出力する - Qiitaです。こちらではコールバックを使っているのですが、Promiseを使って同じことをやる方法を紹介します。

ダメな例

すごく単純に考えると次のようなコードになります。

var fs = require("fs")
var path = require("path")
var dir = process.argv[2] || '.';

var fileList = (dir) => {
  var results = [];
  return new Promise((res, rej) => {
    fs.readdir(dir, (err, files) => {
      if (err) {
        return rej(err);
      }
      files.forEach(file => {
        var fp = path.join(dir, file);
        if (fs.statSync(fp).isDirectory()) {
          fileList(fp)
            .then(files => {
              results = results.concat(files);
            });
        } else {
          results.push(fp);
        }
      });
      res(results);
    });
  });
}

fileList(dir)
  .then(files => {
    console.log(files);
  }, (err) => {
    console.log('error', err);
  })

最初にディレクトリ内のファイル/フォルダを fs.readdir で読み込んで、それがディレクトリだったら再帰的に fileList を実行します。それ自体 Promise なので、thenで結果を受け取って results に追加していくといった具合です。

これを実行すると、次のような結果になります。

[ '/path/to/LICENSE',
  '/path/to/MailSlurperLogo.ico',
  '/path/to/MailSlurperLogo.png',
  '/path/to/README.md',
  '/path/to/config.json',
  '/path/to/mailslurper',
  '/path/to/mailslurper.db' ]

ディレクトリが省かれています…。これは、ディレクトリを辿って results に追加する前に Promiseのresが実行されてしまっているためです。Promiseの中でPromiseを使うとこんなことが起きます。

解決策

ということで、ディレクトリを辿る部分をその場で実行するのではなく、Promiseの配列としてまとめて処理するのが正しい方法になります。

var fs = require("fs")
var path = require("path")
var dir = process.argv[2] || '.';

var fileList = (dir) => {
  var results = [];
  var promises = []; // Promiseの配列を入れる変数を用意
  return new Promise((res, rej) => {
    fs.readdir(dir, (err, files) => {
      if (err) {
        return rej(err);
      }
      files.forEach(file => {
        var fp = path.join(dir, file);
        if (fs.statSync(fp).isDirectory()) {
          // とりあえず配列に追加するだけ
          promises.push(fileList(fp));
        } else {
          results.push(fp);
        }
      });
      // 処理対象がすべて集まったらPromise.all
      Promise.all(promises)
        .then(ary => {
          ary.forEach(files => {
            results = results.concat(files);
          });
          res(results);
        })
    });
  });
}

fileList(dir)
  .then(files => {
    console.log(files);
  }, (err) => {
    console.log('error', err);
  })

こうするとディレクトリ構造が正しく取得できます。Promise.allの結果はディレクトリ毎に配列になっているので、改めてresultsに入れ直す必要があります。

[ '/path/to/.DS_Store',
  '/path/to/LICENSE',
  '/path/to/MailSlurperLogo.ico',
  '/path/to/MailSlurperLogo.png',
  '/path/to/README.md',
  '/path/to/config.json',
  '/path/to/mailslurper',
  '/path/to/mailslurper.db',
  '/path/to/scripts/.DS_Store',
  '/path/to/scripts/create-mssql.sql',
  '/path/to/scripts/create-mysql.sql',
  '/path/to/scripts/generate-cert.sh',
  '/path/to/test/create-mssql.sql',
  '/path/to/test/create-mysql.sql',
  '/path/to/test/generate-cert.sh' ]

なお、Promiseを配列で実行すると、どのディレクトリを辿ったのか(今回で言うところのdir相当)という情報が取れないので分かりづらくなります。そういった時には単純に配列で返すのではなくオブジェクトにするのが良いでしょう。

let obj = {};
obj[dir] = results;
res(obj);

Promiseは一つしか結果を返せないので、配列にするかオブジェクトにするかといった選択になるかと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3