c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

node.jsでdirectoryを再帰的に強制削除 (rm -rf)

追記 2015-05-03
delといふライブラリを使ふと便利だ。glob指定ができる。使ふべきだ。
cf. del https://www.npmjs.com/package/del

node.jsのFile System moduleには、directoryを再帰的に扱うAPIが無いから、自分で再帰を書く。然も遅延させて再帰するから結構アレ。
cf. node.jsで非同期にdirectory中の全てのfileを読み込む http://c4se.hatenablog.com/entry/2013/10/18/124837

rm -rf .に当たる処理を書いた。結構アレ。

/**
 * Extends standard FileSystem module.
 *
 * <p><a href="http://c4se.hatenablog.com/entry/2013/10/18/124837">
 * node.jsで非同期にdirectory中の全てのfileを読み込む - c4se記:さっちゃんですよ☆</a></p>
 * <p><a href="http://c4se.hatenablog.com/entry/2013/10/25/222643">
 * node.jsでdirectoryを再帰的に強制削除 (rm -rf) - c4se記:さっちゃんですよ☆</a></p>
 *
 * @author ne_Sachirou <utakata.c4se@gmail.com>
 * @license Public Domain
 * @module fs
 */

'use strict';

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

/**
 * rmdir -rf (recursive, force) async.
 *
 * @static
 * @param {string} dirpath
 * @param {function(?Error)} callback
 */
function rmdirRF(dirpath, callback) {
  rmdirRFRecur(dirpath).
    then(function() { callback(null); }).
    fail(function(error) { callback(error); }).
    done();
}

/**
 * Remove a directory that is not empty in NodeJS ≪ geedew
 * http://www.geedew.com/2012/10/24/remove-a-directory-that-is-not-empty-in-nodejs/
 *
 * @static
 * @param {string} dirpath
 */
function rmdirRFSync(dirpath) {
  if (fs.existsSync(dirpath)) {
    fs.readdirSync(dirpath).forEach(function(filepath) {
      filepath = path.join(dirpath, filepath);
      if (fs.lstatSync(filepath).isDirectory()) {
        rmdirRFSync(filepath);
      } else {
        fs.unlinkSync(filepath);
      }
    });
    fs.rmdirSync(dirpath);
  }
}

/*
 * @param {string} dirpath
 * @return {Q.defer.promise}
 */
function rmdirRFRecur(dirpath) {
  var deferred = Q.defer();

  fs.exists(dirpath, function(isExists) {
    if (! isExists) {
      deferred.resolve();
      return;
    }
    fs.lstat(dirpath, function(error, stats) {
      var promise;

      if (error) { deferred.reject(error); return; }
      if (stats.isDirectory()) {
        promise = removeDirectory(dirpath);
      } else {
        promise = removeFile(dirpath);
      }
      promise.
        then(deferred.resolve.bind(deferred)).
        fail(deferred.reject.bind(deferred)).
        done();
    });
  });
  return deferred.promise;
}

function removeDirectory(dirpath) {
  var deferred = Q.defer();

  fs.readdir(dirpath, function(error, files) {
    var promises = [ ];

    if (error) { deferred.reject(error); return; }
    files.forEach(function(filepath) {
      filepath = path.join(dirpath, filepath);
      promises.push(rmdirRFRecur(filepath));
    });
    Q.all(promises).then(function() {
      fs.rmdir(dirpath, function(error) {
        if (error) { deferred.reject(error); return; }
        deferred.resolve();
      });
    }).fail(deferred.reject.bind(deferred)).done();
  });
  return deferred.promise;
}

function removeFile(filepath) {
  var deferred = Q.defer();

  fs.unlink(filepath, function(error) {
    if (error) { deferred.reject(error); return; }
    deferred.resolve();
  });
  return deferred.promise;
}

fs.rmdirRF = rmdirRF;
fs.rmdirRFSync = rmdirRFSync;
module.exports = fs;

因みにChild Processでrm -rfを走らせる案は、Windowsで動かないので死にました。

Test

裏ではtest書いてるよ。でもfactoryが実際にdirectoryやfileを作ったり消したりしてて、クッソきちゃないのでアレ。
見ないで下さい。

factory.js

'use strict';

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

/**
 * @constructor
 * @prop {Array.<function()>} destructors
 */
function factory() {
  if (! (this instanceof factory)) { return new factory(); }
  if (factory.instance) { return factory.instance; }
  factory.instance = this;
  this.destructors = [ ];
}

/** @type {factory} */
factory.instance = null;

/**
 * Create a product.
 *
 * @param {string} name
 * @param {...Object}
 * @return {Object}
 */
factory.prototype.create = function(name) {
  return factory[name].apply(this, Array.prototype.slice.call(arguments, 1));
};

/**
 * Destruct all products.
 *
 * @return {factory}
 */
factory.prototype.destroy = function() {
  this.destructors.
    forEach(function(destructor) { destructor.call(this); }.bind(this));
  this.destructors = [ ];
  return this;
};

factory.files = function(dirpath) {
  var deferred = Q.defer(),
      promises = [ ];

  dirpath = dirpath || path.join(__dirname, 'tmp');
  ['.', 'sub', 'sub/sub'].forEach(function(subDirpath) {
    subDirpath = path.join(dirpath, subDirpath);
    fs.mkdirSync(subDirpath);
    promises.push(factory.files._createTmpFiles(subDirpath, factory.files._DATAS));
  });
  Q.all(promises).
    then(function() { deferred.resolve(factory.files._DATAS); }).
    fail(deferred.reject.bind(deferred));
  this.destructors.push(function() {
    lib.fs.rmdirRFSync(dirpath);
  });
  return deferred.promise;
};

factory.files._DATAS = [
  {
    name: 'index.html',
    data: '<!DOCTYPE html>\n' +
          '<html>\n'+
          '  <head lang="en">\n'+
          '    <meta charset="utf-8/">\n' +
          '    <title>Index</title>\n' +
          '    <link rel="stylesheet" href="index.css"/>\n' +
          '  </head>\n' +
          '  <body>\n' +
          '    <p>Body</p>\n' +
          '    <script src="index.js"></script>\n' +
          '  </body>\n' +
          '</html>\n'
  },
  {
    name: 'index.css',
    data: '@charset "utf-8";\n\n' +
          'p {\n' +
          '  margin: 1em;\n' +
          '  padding: 1em;\n' +
          '}\n'
  },
  {
    name: 'index.js',
    data: 'console.log("loaded");'
  }
];

factory.files._createTmpFiles = function(dirpath, datas) {
  var deferred = Q.defer(),
      promises = [ ];

  datas.forEach(function(data) {
    var deferred = Q.defer(),
        filepath = path.join(dirpath, data.name);

    fs.writeFile(filepath, data.data, function(error) {
      if (error) {
        deferred.reject(error);
        return;
      }
      deferred.resolve();
    });
    promises.push(deferred.promise);
  });
  Q.all(promises).
    then(deferred.resolve.bind(deferred)).
    fail(deferred.reject.bind(deferred));
  return deferred.promise;
};

module.exports = factory;

fs_test.js

'use strict';

var fs = require('../..').fs,
    factory = require('../factory.js')();

exports.testFsReadAllFilesWorks = function(test) {
  test.expect(4);
  factory.create('files').then(function(datas) {
    fs.readAllFiles(__dirname + '/../' + 'tmp', function(error, files) {
      if (error) {
        test.ifError(error);
        factory.destroy();
        test.done();
        return;
      }
      test.equal(files.length, 9);
      files = files.map(function(file) { return file.toString(); });
      test.equal(
        files.filter(function(file) { return file === datas[0].data; }).length,
        3);
      test.equal(
        files.filter(function(file) { return file === datas[1].data; }).length,
        3);
      test.equal(
        files.filter(function(file) { return file === datas[2].data; }).length,
        3);
      factory.destroy();
      test.done();
    });
  }).fail(function(error) {
    test.ifError(error);
    factory.destroy();
    test.done();
  });
};

exports.testFsRmdirRfWorks = function(test) {
  test.expect(1);
  factory.create('files').then(function() {
    fs.rmdirRF(__dirname + '/../' + 'tmp', function(error) {
      if (error) {
        test.ifError(error);
        factory.destroy();
        test.done();
        return;
      }
      test.ok(! fs.existsSync(__dirname + '/../' + 'tmp'));
      factory.destroy();
      test.done();
    });
  }).fail(function(error) {
    test.ifError(error);
    factory.destroy();
    test.done();
  });
};