user2727195 user2727195 - 4 months ago 53
Node.js Question

Iterating file directory with promises and recursion

I know I'm returning early in the following function, how can I chain the recursive promises to my result?

My goal is to get an array of list of files in the directory and all of it's subdirectories. Array is single dimension, I'm using concat in this example.

function iterate(body) {
return new Promise(function(resolve, reject){
var list = [];
fs.readdir(body.path, function(error, list){
list.forEach(function(file){
file = path.resolve(body.path, file);
fs.stat(file, function(error, stat){
console.log(file, stat.isDirectory());
if(stat.isDirectory()) {
return iterate({path: file})
.then(function(result){
list.concat(result);
})
.catch(reject);
} else {
list.push(file);
}
})
});
resolve(list);
});
});
};

Answer

There are numerous mistakes in your code. A partial list:

  1. .concat() returns a new array, so list.concat(result) by itself doesn't actually do anything.

  2. You're calling resolve() synchronously and not waiting for all async operations to be completed.

  3. You're trying to recursively return from deep inside several nested async callbacks. You can't do that. That won't get the results back anywhere.

I find this a ton easier to use by using a promisified version of the fs module. I use Bluebird to create that and then you can do this:

const path = require('path');
var Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

function iterate(dir) {
    return fs.readdirAsync(dir).map(function(file) {
        file = path.resolve(dir, file);
        return fs.statAsync(file).then(function(stat) {
            if (stat.isDirectory()) {
                return iterate(file);
            } else {
                return file;
            }
        })
    }).then(function(results) {
        // flatten the array of arrays
        return Array.prototype.concat.apply([], results);
    });
}

Note: I changed iterate() to just take the initial path so it's more generically useful. You can just pass body.path to it initially to adapt.


Here's a version using generic ES6 promises:

const path = require('path');
const fs = require('fs');

fs.readdirAsync = function(dir) {
    return new Promise(function(resolve, reject) {
        fs.readdir(dir, function(err, list) {
            if (err) {
                reject(err);
            } else {
                resolve(list);
            }
        });
    });
}

fs.statAsync = function(file) {
    return new Promise(function(resolve, reject) {
        fs.stat(file, function(err, stat) {
            if (err) {
                reject(err);
            } else {
                resolve(stat);
            }
        });
    });
}


function iterate2(dir) {
    return fs.readdirAsync(dir).then(function(list) {
        return Promise.all(list.map(function(file) {
            file = path.resolve(dir, file);
            return fs.statAsync(file).then(function(stat) {
                if (stat.isDirectory()) {
                    return iterate2(file);
                } else {
                    return file;
                }
            });
        }));
    }).then(function(results) {
        // flatten the array of arrays
        return Array.prototype.concat.apply([], results);
    });
}

iterate2(".").then(function(results) {
    console.log(results);
});
Comments