Node.js fs.readdir 递归目录搜索

Posted

技术标签:

【中文标题】Node.js fs.readdir 递归目录搜索【英文标题】:Node.js fs.readdir recursive directory search 【发布时间】:2022-01-21 06:05:16 【问题描述】:

关于使用 fs.readdir 进行异步目录搜索的任何想法?我意识到我们可以引入递归并使用下一个要读取的目录调用读取目录函数,但我有点担心它不是异步的......

有什么想法吗?我看过node-walk,这很棒,但不像 readdir 那样只给我数组中的文件。虽然

寻找类似...的输出

['file1.txt', 'file2.txt', 'dir/file3.txt']

【问题讨论】:

【参考方案1】:

基本上有两种方法可以做到这一点。在异步环境中,您会注意到有两种循环:串行和并行。串行循环在进入下一次迭代之前等待一次迭代完成 - 这保证了循环的每次迭代都按顺序完成。在并行循环中,所有的迭代都是同时开始的,一个迭代可能在另一个之前完成,但是它比串行循环快得多。所以在这种情况下,使用并行循环可能会更好,因为遍历完成的顺序并不重要,只要它完成并返回结果即可(除非您希望它们按顺序排列)。

并行循环如下所示:

var fs = require('fs');
var path = require('path');
var walk = function(dir, done) 
  var results = [];
  fs.readdir(dir, function(err, list) 
    if (err) return done(err);
    var pending = list.length;
    if (!pending) return done(null, results);
    list.forEach(function(file) 
      file = path.resolve(dir, file);
      fs.stat(file, function(err, stat) 
        if (stat && stat.isDirectory()) 
          walk(file, function(err, res) 
            results = results.concat(res);
            if (!--pending) done(null, results);
          );
         else 
          results.push(file);
          if (!--pending) done(null, results);
        
      );
    );
  );
;

串行循环如下所示:

var fs = require('fs');
var path = require('path');
var walk = function(dir, done) 
  var results = [];
  fs.readdir(dir, function(err, list) 
    if (err) return done(err);
    var i = 0;
    (function next() 
      var file = list[i++];
      if (!file) return done(null, results);
      file = path.resolve(dir, file);
      fs.stat(file, function(err, stat) 
        if (stat && stat.isDirectory()) 
          walk(file, function(err, res) 
            results = results.concat(res);
            next();
          );
         else 
          results.push(file);
          next();
        
      );
    )();
  );
;

并在您的主目录上对其进行测试(警告:如果您的主目录中有很多东西,结果列表将会很大):

walk(process.env.HOME, function(err, results) 
  if (err) throw err;
  console.log(results);
);

编辑:改进示例。

【讨论】:

当心,上面 chjj 的“并行循环”答案在遍历空文件夹的情况下存在错误。解决方法是: var pending = list.length;如果(!待定)完成(空,结果); // 添加这一行! list.forEach(function(file) ... 我投了反对票,因为当你在 2011 年第一次写它时,你的答案很棒,但在 2014 年,人们使用开源模块,自己编写的代码更少,并为他们和其他许多人所依赖的模块做出贡献在。例如,尝试node-dir 使用以下代码行获得@crawf 所需的准确输出:require('node-dir').files(__dirname, function(err, files) console.log(files); ); 对于任何对!-- 语法感到困惑的人,a question has been asked about it 您使用fs 而不是fs.promises 有什么特别的原因吗? fs.promises 会不会更好? 在符号链接的情况下是否有可能陷入无限循环?【参考方案2】:

A. 看看file module。它有一个叫做walk的函数:

file.walk(开始,回调)

导航文件树,为每个目录调用回调,传入 (null, dirPath, dirs, files)。

这可能适合你!是的,它是异步的。但是,如果需要,我认为您必须自己汇总完整路径。

B. 另一种选择,甚至是我的最爱之一:为此使用 unix find。为什么要再做一些已经编程好的事情?也许不完全是您所需要的,但仍然值得一试:

var execFile = require('child_process').execFile;
execFile('find', [ 'somepath/' ], function(err, stdout, stderr) 
  var file_list = stdout.split('\n');
  /* now you've got a list with full path file names */
);

Find 有一个很好的内置缓存机制,可以让后续搜索非常快速,只要只有少数文件夹发生了变化。

【讨论】:

对示例 B 有疑问:对于 execFile() (和 exec() ),stderr 和 stdout 是缓冲区。所以你不需要做 stdout.toString.split("\n ") 因为缓冲区不是字符串? 不错,但不是跨平台。 顺便说一句:不,A 不只是 Unix!只有 B 只是 Unix。但是,Windows 10 现在带有一个 Linux 子系统。所以即使是 B 现在也只能在 Windows 上工作。【参考方案3】:

如果你想使用 npm 包,wrench 很不错。

var wrench = require("wrench");

var files = wrench.readdirSyncRecursive("directory");

wrench.readdirRecursive("directory", function (error, files) 
    // live your dreams
);

编辑(2018 年):最近阅读的人:作者在 2015 年弃用了这个包:

wrench.js 已被弃用,并且已经有一段时间没有更新了。 I heavily recommend using fs-extra 进行任何额外的文件系统操作。

【讨论】:

@Domenic,你怎么denodify这个?回调被触发多次(递归)。所以使用Q.denodify(wrench.readdirRecursive) 只返回第一个结果。 @OnurYıldırım 是的,这不适合按原样进行承诺。您需要编写一些返回多个承诺的东西,或者等到所有子目录都被枚举后才返回承诺的东西。对于后者,请参阅github.com/kriskowal/q-io#listdirectorytreepath【参考方案4】:

另一个不错的 npm 包是 glob。

npm install glob

它非常强大,应该可以满足你所有的递归需求。

编辑:

实际上我对 glob 并不十分满意,所以我创建了readdirp。

我非常有信心,它的 API 使得递归查找文件和目录以及应用特定过滤器变得非常容易。

通读它的documentation 以更好地了解它的作用并通过以下方式安装:

npm install readdirp

【讨论】:

我认为最好的module。并且与许多其他项目一样,如 Grunt、Mocha 等以及其他 80'000 多个其他项目。只是说。 能否请您详细说明创建 readdrip @Thorsten Lorenz 的原因【参考方案5】:

我喜欢上面的chjj 中的the answer,如果没有那个开始,我就无法创建我的并行循环版本。

var fs = require("fs");

var tree = function(dir, done) 
  var results = 
        "path": dir
        ,"children": []
      ;
  fs.readdir(dir, function(err, list) 
    if (err)  return done(err); 
    var pending = list.length;
    if (!pending)  return done(null, results); 
    list.forEach(function(file) 
      fs.stat(dir + '/' + file, function(err, stat) 
        if (stat && stat.isDirectory()) 
          tree(dir + '/' + file, function(err, res) 
            results.children.push(res);
            if (!--pending) done(null, results); 
          );
         else 
          results.children.push("path": dir + "/" + file);
          if (!--pending)  done(null, results); 
        
      );
    );
  );
;

module.exports = tree;

我也创建了a Gist。欢迎评论。我仍然在 NodeJS 领域起步,所以这是我希望了解更多信息的一种方式。

【讨论】:

【参考方案6】:

因为每个人都应该自己写,所以我做了一个。

walk(dir, cb, endCb) cb(文件) endCb(err | null)

module.exports = walk;

function walk(dir, cb, endCb) 
  var fs = require('fs');
  var path = require('path');

  fs.readdir(dir, function(err, files) 
    if (err) 
      return endCb(err);
    

    var pending = files.length;
    if (pending === 0) 
      endCb(null);
    
    files.forEach(function(file) 
      fs.stat(path.join(dir, file), function(err, stats) 
        if (err) 
          return endCb(err)
        

        if (stats.isDirectory()) 
          walk(path.join(dir, file), cb, function() 
            pending--;
            if (pending === 0) 
              endCb(null);
            
          );
         else 
          cb(path.join(dir, file));
          pending--;
          if (pending === 0) 
            endCb(null);
          
        
      )
    );

  );

【讨论】:

【参考方案7】:

我最近编写了这个代码,并认为在这里分享它是有意义的。该代码使用了async library。

var fs = require('fs');
var async = require('async');

var scan = function(dir, suffix, callback) 
  fs.readdir(dir, function(err, files) 
    var returnFiles = [];
    async.each(files, function(file, next) 
      var filePath = dir + '/' + file;
      fs.stat(filePath, function(err, stat) 
        if (err) 
          return next(err);
        
        if (stat.isDirectory()) 
          scan(filePath, suffix, function(err, results) 
            if (err) 
              return next(err);
            
            returnFiles = returnFiles.concat(results);
            next();
          )
        
        else if (stat.isFile()) 
          if (file.indexOf(suffix, file.length - suffix.length) !== -1) 
            returnFiles.push(filePath);
          
          next();
        
      );
    , function(err) 
      callback(err, returnFiles);
    );
  );
;

你可以这样使用它:

scan('/some/dir', '.ext', function(err, files) 
  // Do something with files that ends in '.ext'.
  console.log(files);
);

【讨论】:

这个。这非常整洁且易于使用。我把它抽到一个模块中,需要它,它就像一个 mcdream 三明治。【参考方案8】:

以防万一有人觉得它有用,我还整理了一个同步版本。

var walk = function(dir) 
    var results = [];
    var list = fs.readdirSync(dir);
    list.forEach(function(file) 
        file = dir + '/' + file;
        var stat = fs.statSync(file);
        if (stat && stat.isDirectory())  
            /* Recurse into a subdirectory */
            results = results.concat(walk(file));
         else  
            /* Is a file */
            results.push(file);
        
    );
    return results;

提示:过滤时使用更少的资源。在此函数本身内过滤。例如。将results.push(file); 替换为以下代码。根据需要调整:

    file_type = file.split(".").pop();
    file_name = file.split(/(\\|\/)/g).pop();
    if (file_type == "json") results.push(file);

【讨论】:

这很简单。但也有点幼稚。如果目录包含指向父目录的链接,可能会导致堆栈溢出。也许改用lstat?或者添加一个递归检查来限制递归级别。 考虑使用 file = require("path").join(dir,file) @mpen 分号是多余的 而不是 file = dir + '/' + file;文件 = path.join(目录,文件);会更优雅【参考方案9】:

查看final-fs 库。它提供了一个readdirRecursive 函数:

ffs.readdirRecursive(dirPath, true, 'my/initial/path')
    .then(function (files) 
        // in the `files` variable you've got all the files
    )
    .otherwise(function (err) 
        // something went wrong
    );

【讨论】:

【参考方案10】:

查看加载目录 https://npmjs.org/package/loaddir

npm install loaddir

  loaddir = require('loaddir')

  alljavascripts = []
  loaddir(
    path: __dirname + '/public/javascripts',
    callback: function()  allJavascripts.push(this.relativePath + this.baseName); 
  )

如果您还需要扩展名,可以使用fileName 代替baseName

一个额外的好处是它也会监视文件并再次调用回调。有大量的配置选项使其非常灵活。

我刚刚使用 loaddir 在短时间内从 ruby​​ 重新制作了 guard gem

【讨论】:

【参考方案11】:

独立的承诺实现

我在这个例子中使用了when.js promise 库。

var fs = require('fs')
, path = require('path')
, when = require('when')
, nodefn = require('when/node/function');

function walk (directory, includeDir) 
    var results = [];
    return when.map(nodefn.call(fs.readdir, directory), function(file) 
        file = path.join(directory, file);
        return nodefn.call(fs.stat, file).then(function(stat) 
            if (stat.isFile())  return results.push(file); 
            if (includeDir)  results.push(file + path.sep); 
            return walk(file, includeDir).then(function(filesInDir) 
                results = results.concat(filesInDir);
            );
        );
    ).then(function() 
        return results;
    );
;

walk(__dirname).then(function(files) 
    console.log(files);
).otherwise(function(error) 
    console.error(error.stack || error);
);

我包含了一个可选参数includeDir,如果设置为true,它将在文件列表中包含目录。

【讨论】:

【参考方案12】:

使用node-dir 准确生成您喜欢的输出

var dir = require('node-dir');

dir.files(__dirname, function(err, files) 
  if (err) throw err;
  console.log(files);
  //we have an array of files now, so now we can iterate that array
  files.forEach(function(path) 
    action(null, path);
  )
);

【讨论】:

node-dir 工作正常,但是当我将它与 webpack 一起使用时,我遇到了一些奇怪的问题。一个 Â 插入到 readFiles 函数中,如“if (err) Â ” 导致“uncaught SyntaxError: Unexpected token ”错误。我被这个问题难住了,我的直接反应是用类似的东西替换 node-dir @Parth 这个评论不会给你答案。在 SO 上写一个新的完整问题或在 GitHub 存储库中创建一个问题。当您详细阐述您的问题时,您甚至可能无需发布即可解决您的问题 @Parth 的评论对于正在考虑将您的建议作为他们问题的解决方案的其他人来说可能仍然是一个有用的警告。他们可能没有在这个 cmets 部分寻找答案 :)【参考方案13】:

这是另一个实现。上述解决方案都没有任何限制,因此如果您的目录结构很大,它们都会崩溃并最终耗尽资源。

var async = require('async');
var fs = require('fs');
var resolve = require('path').resolve;

var scan = function(path, concurrency, callback) 
    var list = [];

    var walker = async.queue(function(path, callback) 
        fs.stat(path, function(err, stats) 
            if (err) 
                return callback(err);
             else 
                if (stats.isDirectory()) 
                    fs.readdir(path, function(err, files) 
                        if (err) 
                            callback(err);
                         else 
                            for (var i = 0; i < files.length; i++) 
                                walker.push(resolve(path, files[i]));
                            
                            callback();
                        
                    );
                 else 
                    list.push(path);
                    callback();
                
            
        );
    , concurrency);

    walker.push(path);

    walker.drain = function() 
        callback(list);
    
;

使用 50 的并发效果非常好,并且几乎与小型目录结构的简单实现一样快。

【讨论】:

【参考方案14】:

这是我的答案。希望它可以帮助某人。

我的重点是让搜索程序可以在任何地方停止,并且对于找到的文件,告诉原始路径的相对深度。

var _fs = require('fs');
var _path = require('path');
var _defer = process.nextTick;

// next() will pop the first element from an array and return it, together with
// the recursive depth and the container array of the element. i.e. If the first
// element is an array, it'll be dug into recursively. But if the first element is
// an empty array, it'll be simply popped and ignored.
// e.g. If the original array is [1,[2],3], next() will return [1,0,[[2],3]], and
// the array becomes [[2],3]. If the array is [[[],[1,2],3],4], next() will return
// [1,2,[2]], and the array becomes [[[2],3],4].
// There is an infinity loop `while(true) ...`, because I optimized the code to
// make it a non-recursive version.
var next = function(c) 
    var a = c;
    var n = 0;
    while (true) 
        if (a.length == 0) return null;
        var x = a[0];
        if (x.constructor == Array) 
            if (x.length > 0) 
                a = x;
                ++n;
             else 
                a.shift();
                a = c;
                n = 0;
            
         else 
            a.shift();
            return [x, n, a];
        
    


// cb is the callback function, it have four arguments:
//    1) an error object if any exception happens;
//    2) a path name, may be a directory or a file;
//    3) a flag, `true` means directory, and `false` means file;
//    4) a zero-based number indicates the depth relative to the original path.
// cb should return a state value to tell whether the searching routine should
// continue: `true` means it should continue; `false` means it should stop here;
// but for a directory, there is a third state `null`, means it should do not
// dig into the directory and continue searching the next file.
var ls = function(path, cb) 
    // use `_path.resolve()` to correctly handle '.' and '..'.
    var c = [ _path.resolve(path) ];
    var f = function() 
        var p = next(c);
        p && s(p);
    ;
    var s = function(p) 
        _fs.stat(p[0], function(err, ss) 
            if (err) 
                // use `_defer()` to turn a recursive call into a non-recursive call.
                cb(err, p[0], null, p[1]) && _defer(f);
             else if (ss.isDirectory()) 
                var y = cb(null, p[0], true, p[1]);
                if (y) r(p);
                else if (y == null) _defer(f);
             else 
                cb(null, p[0], false, p[1]) && _defer(f);
            
        );
    ;
    var r = function(p) 
        _fs.readdir(p[0], function(err, files) 
            if (err) 
                cb(err, p[0], true, p[1]) && _defer(f);
             else 
                // not use `Array.prototype.map()` because we can make each change on site.
                for (var i = 0; i < files.length; i++) 
                    files[i] = _path.join(p[0], files[i]);
                
                p[2].unshift(files);
                _defer(f);
            
        );
    
    _defer(f);
;

var printfile = function(err, file, isdir, n) 
    if (err) 
        console.log('-->   ' + ('[' + n + '] ') + file + ': ' + err);
        return true;
     else 
        console.log('... ' + ('[' + n + '] ') + (isdir ? 'D' : 'F') + ' ' + file);
        return true;
    
;

var path = process.argv[2];
ls(path, printfile);

【讨论】:

【参考方案15】:

recursive-readdir 模块具有此功能。

【讨论】:

【参考方案16】:

我修改了 Trevor Senior 的基于 Promise 的答案以使用 Bluebird

var fs = require('fs'),
    path = require('path'),
    Promise = require('bluebird');

var readdirAsync = Promise.promisify(fs.readdir);
var statAsync = Promise.promisify(fs.stat);
function walkFiles (directory) 
    var results = [];
    return readdirAsync(directory).map(function(file) 
        file = path.join(directory, file);
        return statAsync(file).then(function(stat) 
            if (stat.isFile()) 
                return results.push(file);
            
            return walkFiles(file).then(function(filesInDir) 
                results = results.concat(filesInDir);
            );
        );
    ).then(function() 
        return results;
    );


//use
walkDir(__dirname).then(function(files) 
    console.log(files);
).catch(function(e) 
    console.error(e); 
);

【讨论】:

【参考方案17】:

我建议使用node-glob 来完成该任务。

var glob = require( 'glob' );  

glob( 'dirname/**/*.js', function( err, files ) 
  console.log( files );
);

【讨论】:

【参考方案18】:

为了好玩,这里有一个基于流的版本,它与 highland.js 流库一起使用。它由 Victor Vu 合着。

###
  directory >---m------> dirFilesStream >---------o----> out
                |                                 |
                |                                 |
                +--------< returnPipe <-----------+

  legend: (m)erge  (o)bserve

 + directory         has the initial file
 + dirListStream     does a directory listing
 + out               prints out the full path of the file
 + returnPipe        runs stat and filters on directories

###

_ = require('highland')
fs = require('fs')
fsPath = require('path')

directory = _(['someDirectory'])
mergePoint = _()
dirFilesStream = mergePoint.merge().flatMap((parentPath) ->
  _.wrapCallback(fs.readdir)(parentPath).sequence().map (path) ->
    fsPath.join parentPath, path
)
out = dirFilesStream
# Create the return pipe
returnPipe = dirFilesStream.observe().flatFilter((path) ->
  _.wrapCallback(fs.stat)(path).map (v) ->
    v.isDirectory()
)
# Connect up the merge point now that we have all of our streams.
mergePoint.write directory
mergePoint.write returnPipe
mergePoint.end()
# Release backpressure.  This will print files as they are discovered
out.each H.log
# Another way would be to queue them all up and then print them all out at once.
# out.toArray((files)-> console.log(files))

【讨论】:

【参考方案19】:

使用 Promises (Q) 以函数式风格解决此问题:

var fs = require('fs'),
    fsPath = require('path'),
    Q = require('q');

var walk = function (dir) 
  return Q.ninvoke(fs, 'readdir', dir).then(function (files) 

    return Q.all(files.map(function (file) 

      file = fsPath.join(dir, file);
      return Q.ninvoke(fs, 'lstat', file).then(function (stat) 

        if (stat.isDirectory()) 
          return walk(file);
         else 
          return [file];
        
      );
    ));
  ).then(function (files) 
    return files.reduce(function (pre, cur) 
      return pre.concat(cur);
    );
  );
;

它返回一个数组的promise,所以你可以把它当作:

walk('/home/mypath').then(function (files)  console.log(files); );

【讨论】:

【参考方案20】:

我必须将基于 Promise 的 sander 库添加到列表中。

 var sander = require('sander');
 sander.lsr(directory).then( filenames =>  console.log(filenames)  );

【讨论】:

【参考方案21】:

递归

var fs = require('fs')
var path = process.cwd()
var files = []

var getFiles = function(path, files)
    fs.readdirSync(path).forEach(function(file)
        var subpath = path + '/' + file;
        if(fs.lstatSync(subpath).isDirectory())
            getFiles(subpath, files);
         else 
            files.push(path + '/' + file);
        
    );     

打电话

getFiles(path, files)
console.log(files) // will log all files in directory

【讨论】:

我建议不要使用/ 加入路径字符串,而是使用path 模块:path.join(searchPath, file)。这样,您将获得独立于操作系统的正确路径。【参考方案22】:

这是完整的工作代码。根据您的要求。您可以递归获取所有文件和文件夹。

var recur = function(dir) 
            fs.readdir(dir,function(err,list)
                list.forEach(function(file)
                    var file2 = path.resolve(dir, file);
                    fs.stat(file2,function(err,stats)
                        if(stats.isDirectory()) 
                            recur(file2);
                        
                        else 
                            console.log(file2);
                        
                    )
                )
            );
        ;
        recur(path);

in path 提供您要在其中搜索的目录路径,例如“c:\test”

【讨论】:

【参考方案23】:

使用bluebird promise.coroutine:

let promise = require('bluebird'),
    PC = promise.coroutine,
    fs = promise.promisifyAll(require('fs'));
let getFiles = PC(function*(dir)
    let files = [];
    let contents = yield fs.readdirAsync(dir);
    for (let i = 0, l = contents.length; i < l; i ++) 
        //to remove dot(hidden) files on MAC
        if (/^\..*/.test(contents[i])) contents.splice(i, 1);
    
    for (let i = 0, l = contents.length; i < l; i ++) 
        let content = path.resolve(dir, contents[i]);
        let contentStat = yield fs.statAsync(content);
        if (contentStat && contentStat.isDirectory()) 
            let subFiles = yield getFiles(content);
            files = files.concat(subFiles);
         else 
            files.push(content);
        
    
    return files;
);
//how to use
//easy error handling in one place
getFiles(your_dir).then(console.log).catch(err => console.log(err));

【讨论】:

【参考方案24】:

名为 Filehound 的库是另一种选择。它将递归搜索给定目录(默认为工作目录)。它支持各种过滤器、回调、承诺和同步搜索。

例如,在当前工作目录中搜索所有文件(使用回调):

const Filehound = require('filehound');

Filehound.create()
.find((err, files) => 
    if (err) 
        return console.error(`error: $err`);
    
    console.log(files); // array of files
);

或承诺并指定特定目录:

const Filehound = require('filehound');

Filehound.create()
.paths("/tmp")
.find()
.each(console.log);

有关更多用例和使用示例,请参阅文档:https://github.com/nspragg/filehound

免责声明:我是作者。

【讨论】:

【参考方案25】:

klaw 和klaw-sync 对于这类事情值得考虑。这些were part of node-fs-extra。

【讨论】:

【参考方案26】:

这是获取所有文件(包括子目录)的递归方法。

const FileSystem = require("fs");
const Path = require("path");

//...

function getFiles(directory) 
    directory = Path.normalize(directory);
    let files = FileSystem.readdirSync(directory).map((file) => directory + Path.sep + file);

    files.forEach((file, index) => 
        if (FileSystem.statSync(file).isDirectory()) 
            Array.prototype.splice.apply(files, [index, 1].concat(getFiles(file)));
        
    );

    return files;

【讨论】:

【参考方案27】:

使用 async/await,这应该可以工作:

const FS = require('fs');
const readDir = promisify(FS.readdir);
const fileStat = promisify(FS.stat);

async function getFiles(dir) 
    let files = await readDir(dir);

    let result = files.map(file => 
        let path = Path.join(dir,file);
        return fileStat(path).then(stat => stat.isDirectory() ? getFiles(path) : path);
    );

    return flatten(await Promise.all(result));


function flatten(arr) 
    return Array.prototype.concat(...arr);

您可以使用bluebird.Promisify 或这个:

/**
 * Returns a function that will wrap the given `nodeFunction`. Instead of taking a callback, the returned function will return a promise whose fate is decided by the callback behavior of the given node function. The node function should conform to node.js convention of accepting a callback as last argument and calling that callback with error as the first argument and success value on the second argument.
 *
 * @param Function nodeFunction
 * @returns Function
 */
module.exports = function promisify(nodeFunction) 
    return function(...args) 
        return new Promise((resolve, reject) => 
            nodeFunction.call(this, ...args, (err, data) => 
                if(err) 
                    reject(err);
                 else 
                    resolve(data);
                
            )
        );
    ;
;

节点 8+ 有 Promisify built-in

请参阅我的 other answer,了解可以更快地生成结果的生成器方法。

【讨论】:

【参考方案28】:

另一个简单而有用的

function walkDir(root) 
    const stat = fs.statSync(root);

    if (stat.isDirectory()) 
        const dirs = fs.readdirSync(root).filter(item => !item.startsWith('.'));
        let results = dirs.map(sub => walkDir(`$root/$sub`));
        return [].concat(...results);
     else 
        return root;
    

【讨论】:

你假设根目录中的每个文件都是一个文件夹。【参考方案29】:

这就是我使用 nodejs fs.readdir 函数递归搜索目录的方式。

const fs = require('fs');
const mime = require('mime-types');
const readdirRecursivePromise = path => 
    return new Promise((resolve, reject) => 
        fs.readdir(path, (err, directoriesPaths) => 
            if (err) 
                reject(err);
             else 
                if (directoriesPaths.indexOf('.DS_Store') != -1) 
                    directoriesPaths.splice(directoriesPaths.indexOf('.DS_Store'), 1);
                
                directoriesPaths.forEach((e, i) => 
                    directoriesPaths[i] = statPromise(`$path/$e`);
                );
                Promise.all(directoriesPaths).then(out => 
                    resolve(out);
                ).catch(err => 
                    reject(err);
                );
            
        );
    );
;
const statPromise = path => 
    return new Promise((resolve, reject) => 
        fs.stat(path, (err, stats) => 
            if (err) 
                reject(err);
             else 
                if (stats.isDirectory()) 
                    readdirRecursivePromise(path).then(out => 
                        resolve(out);
                    ).catch(err => 
                        reject(err);
                    );
                 else if (stats.isFile()) 
                    resolve(
                        'path': path,
                        'type': mime.lookup(path)
                    );
                 else 
                    reject(`Error parsing path: $path`);
                
            
        );
    );
;
const flatten = (arr, result = []) => 
    for (let i = 0, length = arr.length; i < length; i++) 
        const value = arr[i];
        if (Array.isArray(value)) 
            flatten(value, result);
         else 
            result.push(value);
        
    
    return result;
;

假设您的节点项目根目录中有一个名为“/database”的路径。一旦这个promise被解决,它应该在'/database'下输出一个包含每个文件的数组。

readdirRecursivePromise('database').then(out => 
    console.log(flatten(out));
).catch(err => 
    console.log(err);
);

【讨论】:

【参考方案30】:

这个使用了节点 8 中可用的大量新的流行语功能,包括 Promises、util/promisify、解构、async-await、map+reduce 等等,让您的同事在尝试时摸不着头脑弄清楚发生了什么。

节点 8+

没有外部依赖。

const  promisify  = require('util');
const  resolve  = require('path');
const fs = require('fs');
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);

async function getFiles(dir) 
  const subdirs = await readdir(dir);
  const files = await Promise.all(subdirs.map(async (subdir) => 
    const res = resolve(dir, subdir);
    return (await stat(res)).isDirectory() ? getFiles(res) : res;
  ));
  return files.reduce((a, f) => a.concat(f), []);

用法

getFiles(__dirname)
  .then(files => console.log(files))
  .catch(e => console.error(e));

节点 10.10+

为节点 10+ 更新了更多功能:

const  resolve  = require('path');
const  readdir  = require('fs').promises;

async function getFiles(dir) 
  const dirents = await readdir(dir,  withFileTypes: true );
  const files = await Promise.all(dirents.map((dirent) => 
    const res = resolve(dir, dirent.name);
    return dirent.isDirectory() ? getFiles(res) : res;
  ));
  return Array.prototype.concat(...files);

请注意,从节点 11.15.0 开始,您可以使用 files.flat() 而不是 Array.prototype.concat(...files) 来展平文件数组。

节点 11+

如果您想彻底颠覆所有人,您可以使用以下版本的 异步迭代器。除了非常酷之外,它还允许消费者一次提取一个结果,使其更适合非常大的目录。

const  resolve  = require('path');
const  readdir  = require('fs').promises;

async function* getFiles(dir) 
  const dirents = await readdir(dir,  withFileTypes: true );
  for (const dirent of dirents) 
    const res = resolve(dir, dirent.name);
    if (dirent.isDirectory()) 
      yield* getFiles(res);
     else 
      yield res;
    
  

用法已经改变,因为返回类型现在是异步迭代器而不是承诺

;(async () => 
  for await (const f of getFiles('.')) 
    console.log(f);
  
)()

如果有人感兴趣,我在这里写了更多关于异步迭代器的文章:https://qwtel.com/posts/software/async-generators-in-the-wild/

【讨论】:

subdirsubdirs 的命名具有误导性,因为它们实际上可能是文件(我建议使用类似 itemInDiritem_in_dir 甚至简单的 item 代替。),但是这个解决方案感觉比公认的更干净,而且代码少得多。我也没有发现它比接受的答案中的代码复杂得多。 +1 您可以通过使用require(fs).promises 来使这更加精彩,完全放弃util.promisify。我个人将 fs 别名为 fs.promises。 我们可以通过一个小改动来加快速度:将第二个参数传递给readdir 也就是像readdir(dir, withFileTypes: true) 这样的选项对象,这将返回所有项目及其类型信息,所以我们赢了根本不需要打电话给stat 来获取readdir 现在给我们的信息。这使我们无需进行额外的系统调用。 Details here @cacoder 更新为包含withFileTypes。感谢您的提示。 这是已知的无法陷入符号链接的无限循环吗?

以上是关于Node.js fs.readdir 递归目录搜索的主要内容,如果未能解决你的问题,请参考以下文章

Node.js fs.readdir 递归目录搜索

fs.readdir 递归搜索深度=1

Node.js 内置模块fs的readdir方法 查看某个文件夹里面包含的文件内容

Node.js——fs模块(文件系统),创建删除目录(文件),读取写入文件流

在 node.js 中读取文件和读取目录

Node.js之文件及文件流(fs,path,buffer,stream)