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

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

## ダメな例

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

```javascript
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 に追加していくといった具合です。

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

```javascript
[ '/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の配列としてまとめて処理するのが正しい方法になります。

```javascript
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に入れ直す必要があります。

```javascript
[ '/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は一つしか結果を返せないので、配列にするかオブジェクトにするかといった選択になるかと思います。