'use strict'; var Promise, _, child_process, fs, fs_exists, nofs, npath; _ = require('./utils'); npath = require('./path'); child_process = require('child_process'); /** * Here I use [Yaku](https://github.com/ysmood/yaku) only as an ES6 shim for Promise. * No APIs other than ES6 spec will be used. In the * future it will be removed. */ Promise = _.Promise; fs = _.extend({}, require('fs')); fs_exists = fs.exists; fs.exists = function(path, fn) { return fs_exists(path, function(exists) { return fn(null, exists); }); }; (function() { var k, name, results; results = []; for (k in fs) { if (k.slice(-4) === 'Sync') { name = k.slice(0, -4); results.push(fs[name] = _.PromiseUtils.promisify(fs[name])); } else { results.push(void 0); } } return results; })(); nofs = _.extend({}, { /** * Copy an empty directory. * @param {String} src * @param {String} dest * @param {Object} opts * ```js * { * isForce: false, * mode: auto * } * ``` * @return {Promise} */ copyDir: function(src, dest, opts) { var copy; _.defaults(opts, { isForce: false }); copy = function() { return (opts.isForce ? fs.mkdir(dest, opts.mode)["catch"](function(err) { if (err.code !== 'EEXIST') { return Promise.reject(err); } }) : fs.mkdir(dest, opts.mode))["catch"](function(err) { if (err.code === 'ENOENT') { return nofs.mkdirs(dest); } else { return Promise.reject(err); } }); }; if (opts.mode) { return copy(); } else { return fs.stat(src).then(function(arg) { var mode; mode = arg.mode; opts.mode = mode; return copy(); }); } }, copyDirSync: function(src, dest, opts) { var copy, mode; _.defaults(opts, { isForce: false }); copy = function() { var err, error, error1; try { if (opts.isForce) { try { return fs.mkdirSync(dest, opts.mode); } catch (error) { err = error; if (err.code !== 'EEXIST') { throw err; } } } else { return fs.mkdirSync(dest, opts.mode); } } catch (error1) { err = error1; if (err.code === 'ENOENT') { return nofs.mkdirsSync(dest); } else { throw err; } } }; if (opts.mode) { return copy(); } else { mode = fs.statSync(src).mode; opts.mode = mode; return copy(); } }, /** * Copy a single file. * @param {String} src * @param {String} dest * @param {Object} opts * ```js * { * isForce: false, * mode: auto * } * ``` * @return {Promise} */ copyFile: function(src, dest, opts) { var copy, copyFile; _.defaults(opts, { isForce: false }); copyFile = function() { return new Promise(function(resolve, reject) { var err, error, sDest, sSrc; try { sDest = fs.createWriteStream(dest, opts); sSrc = fs.createReadStream(src); } catch (error) { err = error; reject(err); } sSrc.on('error', reject); sDest.on('error', reject); sDest.on('close', resolve); return sSrc.pipe(sDest); }); }; copy = function() { return (opts.isForce ? fs.unlink(dest)["catch"](function(err) { if (err.code !== 'ENOENT') { return Promise.reject(err); } }).then(function() { return copyFile(); }) : copyFile())["catch"](function(err) { if (err.code === 'ENOENT') { return nofs.mkdirs(npath.dirname(dest)).then(copyFile); } else { return Promise.reject(err); } }); }; if (opts.mode) { return copy(); } else { return fs.stat(src).then(function(arg) { var mode; mode = arg.mode; opts.mode = mode; return copy(); }); } }, copyFileSync: function(src, dest, opts) { var buf, blkSz, copy, copyFile, mode; _.defaults(opts, { blockSize: 64 * 1024, isForce: false }); blkSz = opts.blockSize; buf = Buffer.alloc(blkSz); copyFile = function() { var bytesRead, fdr, fdw, pos; fdr = fs.openSync(src, 'r'); fdw = fs.openSync(dest, 'w', opts.mode); bytesRead = 1; pos = 0; while (bytesRead > 0) { bytesRead = fs.readSync(fdr, buf, 0, blkSz, pos); fs.writeSync(fdw, buf, 0, bytesRead); pos += bytesRead; } fs.closeSync(fdr); return fs.closeSync(fdw); }; copy = function() { var err, error, error1; try { if (opts.isForce) { try { fs.unlinkSync(dest); } catch (error) { err = error; if (err.code !== 'ENOENT') { throw err; } } return copyFile(); } else { return copyFile(); } } catch (error1) { err = error1; if (err.code === 'ENOENT') { nofs.mkdirsSync(npath.dirname(dest)); return copyFile(); } else { throw err; } } }; if (opts.mode) { return copy(); } else { mode = fs.statSync(src).mode; opts.mode = mode; return copy(); } }, /** * Like `cp -r`. * @param {String} from Source path. * @param {String} to Destination path. * @param {Object} opts Extends the options of [eachDir](#eachDir-opts). * Defaults: * ```js * { * // Overwrite file if exists. * isForce: false, * isIterFileOnly: false * * filter: (fileInfo) => true * } * ``` * @return {Promise} * @example * Copy the contents of the directory rather than copy the directory itself. * ```js * nofs.copy('dir/path/**', 'dest/path'); * * nofs.copy('dir/path', 'dest/path', { * filter: (fileInfo) => { * return /\d+/.test(fileInfo.path); * } * }); * ``` */ copy: function(from, to, opts) { var flags, pm, filter; if (opts == null) { opts = {}; } _.defaults(opts, { isForce: false, isIterFileOnly: false, filter: function () { return true; } }); flags = opts.isForce ? 'w' : 'wx'; filter = opts.filter; opts.iter = function(src, dest, arg) { if (_.isFunction(filter) && !filter(arg)) return; var isDir, stats; isDir = arg.isDir, stats = arg.stats; if (isDir) { return nofs.copyDir(src, dest, { isForce: true, mode: opts.mode }); } else { return nofs.copyFile(src, dest, { isForce: opts.isForce, mode: opts.mode }); } }; if (pm = nofs.pmatch.isPmatch(from)) { from = nofs.pmatch.getPlainPath(pm); pm = npath.relative(from, pm.pattern); opts.filter = pm; } return nofs.dirExists(to).then(function(exists) { if (exists) { if (!pm) { return to = npath.join(to, npath.basename(from)); } } else { return nofs.mkdirs(npath.dirname(to)); } }).then(function() { return fs.stat(from); }).then(function(stats) { var isDir; isDir = stats.isDirectory(); if (isDir) { return nofs.mapDir(from, to, opts); } else { return opts.iter(from, to, { isDir: isDir, stats: stats }); } }); }, copySync: function(from, to, opts) { var flags, isDir, pm, stats, filter; if (opts == null) { opts = {}; } _.defaults(opts, { isForce: false, isIterFileOnly: false, filter: function () { return true; } }); flags = opts.isForce ? 'w' : 'wx'; filter = opts.filter; opts.iter = function(src, dest, arg) { if (_.isFunction(filter) && !filter(arg)) return; var isDir, stats; isDir = arg.isDir, stats = arg.stats; if (isDir) { return nofs.copyDirSync(src, dest, { isForce: true, mode: opts.mode }); } else { return nofs.copyFileSync(src, dest, { isForce: opts.isForce, mode: opts.mode }); } }; if (pm = nofs.pmatch.isPmatch(from)) { from = nofs.pmatch.getPlainPath(pm); pm = npath.relative(from, pm.pattern); opts.filter = pm; } if (nofs.dirExistsSync(to)) { if (!pm) { to = npath.join(to, npath.basename(from)); } } else { nofs.mkdirsSync(npath.dirname(to)); } stats = fs.statSync(from); isDir = stats.isDirectory(); if (isDir) { return nofs.mapDirSync(from, to, opts); } else { return opts.iter(from, to, { isDir: isDir, stats: stats }); } }, /** * Check if a path exists, and if it is a directory. * @param {String} path * @return {Promise} Resolves a boolean value. */ dirExists: function(path) { return fs.stat(path).then(function(stats) { return stats.isDirectory(); })["catch"](function() { return false; }); }, dirExistsSync: function(path) { if (fs.existsSync(path)) { return fs.statSync(path).isDirectory(); } else { return false; } }, /** * * Concurrently walks through a path recursively with a callback. * The callback can return a Promise to continue the sequence. * The resolving order is also recursive, a directory path resolves * after all its children are resolved. * @param {String} spath The path may point to a directory or a file. * @param {Object} opts Optional. Defaults: * ```js * { * // Callback on each path iteration. * iter: (fileInfo) => Promise | Any, * * // Auto check if the spath is a minimatch pattern. * isAutoPmatch: true, * * // Include entries whose names begin with a dot (.), the posix hidden files. * all: true, * * // To filter paths. It can also be a RegExp or a glob pattern string. * // When it's a string, it extends the Minimatch's options. * filter: (fileInfo) => true, * * // The current working directory to search. * cwd: '', * * // Call iter only when it is a file. * isIterFileOnly: false, * * // Whether to include the root directory or not. * isIncludeRoot: true, * * // Whehter to follow symbol links or not. * isFollowLink: true, * * // Iterate children first, then parent folder. * isReverse: false, * * // When isReverse is false, it will be the previous iter resolve value. * val: any, * * // If it return false, sub-entries won't be searched. * // When the `filter` option returns false, its children will * // still be itered. But when `searchFilter` returns false, children * // won't be itered by the iter. * searchFilter: (fileInfo) => true, * * // If you want sort the names of each level, you can hack here. * // Such as `(names) => names.sort()`. * handleNames: (names) => names * } * ``` * The argument of `opts.iter`, `fileInfo` object has these properties: * ```js * { * path: String, * name: String, * baseDir: String, * isDir: Boolean, * children: [fileInfo], * stats: fs.Stats, * val: Any * } * ``` * Assume we call the function: `nofs.eachDir('dir', { iter: (f) => f })`, * the resolved directory object array may look like: * ```js * { * path: 'some/dir/path', * name: 'path', * baseDir: 'some/dir', * isDir: true, * val: 'test', * children: [ * { * path: 'some/dir/path/a.txt', name: 'a.txt', * baseDir: 'dir', isDir: false, stats: { ... } * }, * { path: 'some/dir/path/b.txt', name: 'b.txt', ... } * ], * stats: { * size: 527, * atime: 'Mon, 10 Oct 2011 23:24:11 GMT', * mtime: 'Mon, 10 Oct 2011 23:24:11 GMT', * ctime: 'Mon, 10 Oct 2011 23:24:11 GMT' * ... * } * } * ``` * The `stats` is a native `fs.Stats` object. * @return {Promise} Resolves a directory tree object. * @example * ```js * // Print all file and directory names, and the modification time. * nofs.eachDir('dir/path', { * iter: (obj, stats) => * console.log(obj.path, stats.mtime) * }); * * // Print path name list. * nofs.eachDir('dir/path', { iter: (curr) => curr }) * .then((tree) => * console.log(tree) * ); * * // Find all js files. * nofs.eachDir('dir/path', { * filter: '**\/*.js', * iter: ({ path }) => * console.log(paths) * }); * * // Find all js files. * nofs.eachDir('dir/path', { * filter: /\.js$/, * iter: ({ path }) => * console.log(paths) * }); * * // Custom filter. * nofs.eachDir('dir/path', { * filter: ({ path, stats }) => * path.slice(-1) != '/' && stats.size > 1000 * iter: (path) => * console.log(path) * }); * ``` */ eachDir: function(spath, opts) { var decideNext, execFn, handleFilter, handleSpath, raceResolver, readdir, resolve, stat; if (opts == null) { opts = {}; } _.defaults(opts, { isAutoPmatch: true, all: true, filter: function() { return true; }, searchFilter: function() { return true; }, handleNames: function(names) { return names; }, cwd: '', isIterFileOnly: false, isIncludeRoot: true, isFollowLink: true, isReverse: false }); stat = opts.isFollowLink ? fs.stat : fs.lstat; handleSpath = function() { var pm; spath = npath.normalize(spath); if (opts.isAutoPmatch && (pm = nofs.pmatch.isPmatch(spath))) { if (nofs.pmatch.isNotPlain(pm)) { // keep the user defined filter opts._filter = opts.filter; opts.filter = pm; } return spath = nofs.pmatch.getPlainPath(pm); } }; handleFilter = function() { var pm, reg; if (_.isRegExp(opts.filter)) { reg = opts.filter; opts.filter = function(fileInfo) { return reg.test(fileInfo.path); }; return; } pm = null; if (_.isString(opts.filter)) { pm = new nofs.pmatch.Minimatch(opts.filter); } if (opts.filter instanceof nofs.pmatch.Minimatch) { pm = opts.filter; } if (pm) { opts.filter = function(fileInfo) { // Hot fix for minimatch, it should match '**' to '.'. if (fileInfo.path === '.') { return pm.match(''); } return pm.match(fileInfo.path) && (_.isFunction(opts._filter) ? opts._filter(fileInfo) : true); }; return opts.searchFilter = function(fileInfo) { // Hot fix for minimatch, it should match '**' to '.'. if (fileInfo.path === '.') { return true; } return pm.match(fileInfo.path, true) && (_.isFunction(opts._searchFilter) ? opts._searchFilter(fileInfo) : true); }; } }; resolve = function(path) { return npath.join(opts.cwd, path); }; execFn = function(fileInfo) { if (!opts.all && fileInfo.name[0] === '.') { return; } if (opts.isIterFileOnly && fileInfo.isDir) { return; } if ((opts.iter != null) && opts.filter(fileInfo)) { return opts.iter(fileInfo); } }; // TODO: Race Condition // It's possible that the file has already gone. // Here we silently ignore it, since you normally don't // want to iterate a non-exists path. raceResolver = function(err) { if (err.code !== 'ENOENT') { return Promise.reject(err); } }; decideNext = function(dir, name) { var path; path = npath.join(dir, name); return stat(resolve(path))["catch"](raceResolver).then(function(stats) { var fileInfo, isDir, p; if (!stats) { return; } isDir = stats.isDirectory(); if (opts.baseDir === void 0) { opts.baseDir = isDir ? spath : npath.dirname(spath); } fileInfo = { path: path, name: name, baseDir: opts.baseDir, isDir: isDir, stats: stats }; if (isDir) { if (!opts.searchFilter(fileInfo)) { return; } if (opts.isReverse) { return readdir(path).then(function(children) { fileInfo.children = children; return execFn(fileInfo); }); } else { p = execFn(fileInfo); if (!p || !p.then) { p = Promise.resolve(p); } return p.then(function(val) { return readdir(path).then(function(children) { fileInfo.children = children; fileInfo.val = val; return fileInfo; }); }); } } else { return execFn(fileInfo); } }); }; readdir = function(dir) { return fs.readdir(resolve(dir))["catch"](raceResolver).then(function(names) { if (!names) { return; } return Promise.all(opts.handleNames(names).map(function(name) { return decideNext(dir, name); })); }); }; handleSpath(); handleFilter(); if (opts.isIncludeRoot) { return decideNext(npath.dirname(spath), npath.basename(spath)); } else { return readdir(spath); } }, eachDirSync: function(spath, opts) { var decideNext, execFn, handleFilter, handleSpath, raceResolver, readdir, resolve, stat; if (opts == null) { opts = {}; } _.defaults(opts, { isAutoPmatch: true, all: true, filter: function() { return true; }, searchFilter: function() { return true; }, handleNames: function(names) { return names; }, cwd: '', isIterFileOnly: false, isIncludeRoot: true, isFollowLink: true, isReverse: false }); stat = opts.isFollowLink ? fs.statSync : fs.lstatSync; handleSpath = function() { var pm; spath = npath.normalize(spath); if (opts.isAutoPmatch && (pm = nofs.pmatch.isPmatch(spath))) { if (nofs.pmatch.isNotPlain(pm)) { opts._filter = opts.filter; opts.filter = pm; } return spath = nofs.pmatch.getPlainPath(pm); } }; handleFilter = function() { var pm, reg; if (_.isRegExp(opts.filter)) { reg = opts.filter; opts.filter = function(fileInfo) { return reg.test(fileInfo.path); }; return; } pm = null; if (_.isString(opts.filter)) { pm = new nofs.pmatch.Minimatch(opts.filter); } if (opts.filter instanceof nofs.pmatch.Minimatch) { pm = opts.filter; } if (pm) { opts.filter = function(fileInfo) { if (fileInfo.path === '.') { return pm.match(''); } return pm.match(fileInfo.path) && (_.isFunction(opts._filter) ? opts._filter(fileInfo) : true); }; return opts.searchFilter = function(fileInfo) { if (fileInfo.path === '.') { return true; } return pm.match(fileInfo.path, true) && (_.isFunction(opts._searchFilter) ? opts._searchFilter(fileInfo) : true); }; } }; resolve = function(path) { return npath.join(opts.cwd, path); }; execFn = function(fileInfo) { if (!opts.all && fileInfo.name[0] === '.') { return; } if (opts.isIterFileOnly && fileInfo.isDir) { return; } if ((opts.iter != null) && opts.filter(fileInfo)) { return opts.iter(fileInfo); } }; raceResolver = function(err) { if (err.code !== 'ENOENT') { throw err; } }; decideNext = function(dir, name) { var children, err, error, fileInfo, isDir, path, stats, val; path = npath.join(dir, name); try { stats = stat(resolve(path)); isDir = stats.isDirectory(); } catch (error) { err = error; raceResolver(err); return; } if (opts.baseDir === void 0) { opts.baseDir = isDir ? spath : npath.dirname(spath); } fileInfo = { path: path, name: name, baseDir: opts.baseDir, isDir: isDir, stats: stats }; if (isDir) { if (!opts.searchFilter(fileInfo)) { return; } if (opts.isReverse) { children = readdir(path); fileInfo.children = children; return execFn(fileInfo); } else { val = execFn(fileInfo); children = readdir(path); fileInfo.children = children; fileInfo.val = val; return fileInfo; } } else { return execFn(fileInfo); } }; readdir = function(dir) { var err, error, names; try { names = fs.readdirSync(resolve(dir)); } catch (error) { err = error; raceResolver(err); return; } return opts.handleNames(names).map(function(name) { return decideNext(dir, name); }); }; handleSpath(); handleFilter(); if (opts.isIncludeRoot) { return decideNext(npath.dirname(spath), npath.basename(spath)); } else { return readdir(spath); } }, /** * Ensures that the file exists. * Change file access and modification times. * If the file does not exist, it is created. * If the file exists, it is NOT MODIFIED. * @param {String} path * @param {Object} opts * @return {Promise} */ ensureFile: function(path, opts) { if (opts == null) { opts = {}; } return nofs.fileExists(path).then(function(exists) { if (exists) { return Promise.resolve(); } else { return nofs.outputFile(path, Buffer.from(''), opts); } }); }, ensureFileSync: function(path, opts) { if (opts == null) { opts = {}; } if (!nofs.fileExistsSync(path)) { return nofs.outputFileSync(path, Buffer.from(''), opts); } }, /** * Check if a path exists, and if it is a file. * @param {String} path * @return {Promise} Resolves a boolean value. */ fileExists: function(path) { return fs.stat(path).then(function(stats) { return stats.isFile(); })["catch"](function() { return false; }); }, fileExistsSync: function(path) { if (fs.existsSync(path)) { return fs.statSync(path).isFile(); } else { return false; } }, /** * Get files by patterns. * @param {String | Array} pattern The minimatch pattern. * Patterns that starts with '!' in the array will be used * to exclude paths. * @param {Object} opts Extends the options of [eachDir](#eachDir-opts). * **The `filter` property is fixed with the pattern, use `iter` instead**. * Defaults: * ```js * { * all: false, * * // The minimatch option object. * pmatch: {}, * * // It will be called after each match. It can also return * // a promise. * iter: (fileInfo, list) => list.push(fileInfo.path) * } * ``` * @return {Promise} Resolves the list array. * @example * ```js * // Get all js files. * nofs.glob(['**\/*.js', '**\/*.css']).then((paths) => * console.log(paths) * ); * * // Exclude some files. "a.js" will be ignored. * nofs.glob(['**\/*.js', '!**\/a.js']).then((paths) => * console.log(paths) * ); * * // Custom the iterator. Append '/' to each directory path. * nofs.glob('**\/*.js', { * iter: (info, list) => * list.push(info.isDir ? (info.path + '/') : info.path * }).then((paths) => * console.log(paths) * ); * ``` */ glob: function(patterns, opts) { var glob, iter, list, negateMath, pmatches, ref; if (opts == null) { opts = {}; } _.defaults(opts, { pmatch: {}, all: false, iter: function(fileInfo, list) { return list.push(fileInfo.path); } }); opts.pmatch.dot = opts.all; if (_.isString(patterns)) { patterns = [patterns]; } patterns = patterns.map(npath.normalize); list = []; ref = nofs.pmatch.matchMultiple(patterns, opts.pmatch), pmatches = ref.pmatches, negateMath = ref.negateMath; iter = opts.iter; opts.iter = function(fileInfo) { return iter(fileInfo, list); }; glob = function(pm) { var newOpts; newOpts = _.defaults({ filter: function(fileInfo) { if (negateMath(fileInfo.path)) { return; } if (fileInfo.path === '.') { return pm.match(''); } return pm.match(fileInfo.path); }, searchFilter: function(fileInfo) { if (fileInfo.path === '.') { return true; } return pm.match(fileInfo.path, true); } }, opts); return nofs.eachDir(nofs.pmatch.getPlainPath(pm), newOpts); }; return pmatches.reduce(function(p, pm) { return p.then(function() { return glob(pm); }); }, Promise.resolve()).then(function() { return list; }); }, globSync: function(patterns, opts) { var glob, i, iter, len, list, negateMath, pm, pmatches, ref; if (opts == null) { opts = {}; } _.defaults(opts, { pmatch: {}, all: false, iter: function(fileInfo, list) { return list.push(fileInfo.path); } }); opts.pmatch.dot = opts.all; if (_.isString(patterns)) { patterns = [patterns]; } patterns = patterns.map(npath.normalize); list = []; ref = nofs.pmatch.matchMultiple(patterns, opts.pmatch), pmatches = ref.pmatches, negateMath = ref.negateMath; iter = opts.iter; opts.iter = function(fileInfo) { return iter(fileInfo, list); }; glob = function(pm) { var newOpts; newOpts = _.defaults({ filter: function(fileInfo) { if (negateMath(fileInfo.path)) { return; } if (fileInfo.path === '.') { return pm.match(''); } return pm.match(fileInfo.path); }, searchFilter: function(fileInfo) { if (fileInfo.path === '.') { return true; } return pm.match(fileInfo.path, true); } }, opts); return nofs.eachDirSync(nofs.pmatch.getPlainPath(pm), newOpts); }; for (i = 0, len = pmatches.length; i < len; i++) { pm = pmatches[i]; glob(pm); } return list; }, /** * Map file from a directory to another recursively with a * callback. * @param {String} from The root directory to start with. * @param {String} to This directory can be a non-exists path. * @param {Object} opts Extends the options of [eachDir](#eachDir-opts). But `cwd` is * fixed with the same as the `from` parameter. Defaults: * ```js * { * // It will be called with each path. The callback can return * // a `Promise` to keep the async sequence go on. * iter: (src, dest, fileInfo) => Promise | Any, * * // When isMapContent is true, and the current is a file. * iter: (content, src, dest, fileInfo) => Promise | Any, * * // When isMapContent is true, and the current is a folder. * iter: (mode, src, dest, fileInfo) => Promise | Any, * * isMapContent: false, * * isIterFileOnly: true * } * ``` * @return {Promise} Resolves a tree object. * @example * ```js * nofs.mapDir('from', 'to', { * iter: (src, dest, info) => * console.log(src, dest, info) * }); * ``` * @example * ```js * // Copy and add license header for each files * // from a folder to another. * nofs.mapDir('from', 'to', { * ismMapContent: true, * iter: (content) => * 'License MIT\n' + content * }); * ``` */ mapDir: function(from, to, opts) { var iter, pm; if (opts == null) { opts = {}; } _.defaults(opts, { iter: _.id, isIterFileOnly: true, isMapContent: false }); if (pm = nofs.pmatch.isPmatch(from)) { from = nofs.pmatch.getPlainPath(pm); pm = npath.relative(from, pm.pattern); opts.filter = pm; } opts.cwd = from; iter = opts.iter; opts.iter = function(fileInfo) { var dest, src; src = npath.join(from, fileInfo.path); dest = npath.join(to, fileInfo.path); if (opts.isMapContent) { if (fileInfo.isDir) { return iter(fileInfo.stats.mode, src, dest, fileInfo) .then(function (mode) { return nofs.mkdirs(dest, mode); }); } else { return fs.readFile(src).then(function (content) { return iter(content, src, dest, fileInfo); }).then(function (content) { return nofs.outputFile(dest, content, { mode: fileInfo.mode }); }); } } else { return iter(src, dest, fileInfo); } }; return nofs.eachDir('', opts); }, mapDirSync: function(from, to, opts) { var iter, pm; if (opts == null) { opts = {}; } _.defaults(opts, { iter: _.id, isIterFileOnly: true, isMapContent: false }); if (pm = nofs.pmatch.isPmatch(from)) { from = nofs.pmatch.getPlainPath(pm); pm = npath.relative(from, pm.pattern); opts.filter = pm; } opts.cwd = from; iter = opts.iter; opts.iter = function(fileInfo) { var dest, src; src = npath.join(from, fileInfo.path); dest = npath.join(to, fileInfo.path); if (opts.isMapContent) { if (fileInfo.isDir) { var mode = iter(fileInfo.stats.mode, src, dest, fileInfo); nofs.mkdirsSync(dest, mode); } else { var content = fs.readFileSync(src); content = iter(content, src, dest, fileInfo); nofs.outputFileSync(dest, content, { mode: fileInfo.mode }); } } else { return iter(src, dest, fileInfo); } }; return nofs.eachDirSync('', opts); }, /** * Recursively create directory path, like `mkdir -p`. * @param {String} path * @param {String} mode Defaults: `0o777 & ~process.umask()` * @return {Promise} */ mkdirs: function(path, mode) { var makedir; if (mode == null) { mode = 0x1ff & ~process.umask(); } makedir = function(path) { // ys TODO: // Sometimes I think this async operation is // useless, since during the next process tick, the // dir may be created. // We may use dirExistsSync to avoid this bug, but // for the sake of pure async, I leave it still. return nofs.dirExists(path).then(function(exists) { var parentPath; if (exists) { return Promise.resolve(); } else { parentPath = npath.dirname(path); return makedir(parentPath).then(function() { return fs.mkdir(path, mode)["catch"](function(err) { if (err.code !== 'EEXIST') { return Promise.reject(err); } }); }); } }); }; return makedir(path); }, mkdirsSync: function(path, mode) { var makedir; if (mode == null) { mode = 0x1ff & ~process.umask(); } makedir = function(path) { var parentPath; if (!nofs.dirExistsSync(path)) { parentPath = npath.dirname(path); makedir(parentPath); return fs.mkdirSync(path, mode); } }; return makedir(path); }, /** * Moves a file or directory. Also works between partitions. * Behaves like the Unix `mv`. * @param {String} from Source path. * @param {String} to Destination path. * @param {Object} opts Defaults: * ```js * { * isForce: false, * isFollowLink: false * } * ``` * @return {Promise} It will resolve a boolean value which indicates * whether this action is taken between two partitions. */ move: function(from, to, opts) { var moveFile; if (opts == null) { opts = {}; } _.defaults(opts, { isForce: false, isFollowLink: false }); moveFile = function(src, dest) { if (opts.isForce) { return fs.rename(src, dest); } else { return fs.link(src, dest).then(function() { return fs.unlink(src); }); } }; return fs.stat(from).then(function(stats) { return nofs.dirExists(to).then(function(exists) { if (exists) { nofs.mkdirs(to); return to = npath.join(to, npath.basename(from)); } else { return nofs.mkdirs(npath.dirname(to)); } }).then(function() { if (stats.isDirectory()) { return fs.rename(from, to); } else { return moveFile(from, to); } }); })["catch"](function(err) { if (err.code === 'EXDEV') { return nofs.copy(from, to, opts).then(function() { return nofs.remove(from); }); } else { return Promise.reject(err); } }); }, moveSync: function(from, to, opts) { var err, error, moveFile, stats; if (opts == null) { opts = {}; } _.defaults(opts, { isForce: false }); moveFile = function(src, dest) { if (opts.isForce) { fs.renameSync(src, dest); } else { fs.linkSync(src, dest) fs.unlinkSync(src); } }; try { if (nofs.dirExistsSync(to)) { nofs.mkdirsSync(to); to = npath.join(to, npath.basename(from)); } else { nofs.mkdirsSync(npath.dirname(to)); } stats = fs.statSync(from); if (stats.isDirectory()) { return fs.renameSync(from, to); } else { return moveFile(from, to); } } catch (error) { err = error; if (err.code === 'EXDEV') { nofs.copySync(from, to, opts); return nofs.removeSync(from); } else { throw err; } } }, /** * Almost the same as `writeFile`, except that if its parent * directories do not exist, they will be created. * @param {String} path * @param {String | Buffer} data * @param {String | Object} opts * Same with the [writeFile](#writeFile-opts). * @return {Promise} */ outputFile: function(path, data, opts) { if (opts == null) { opts = {}; } return nofs.fileExists(path).then(function(exists) { var dir; if (exists) { return nofs.writeFile(path, data, opts); } else { dir = npath.dirname(path); return nofs.mkdirs(dir, opts.mode).then(function() { return nofs.writeFile(path, data, opts); }); } }); }, outputFileSync: function(path, data, opts) { var dir; if (opts == null) { opts = {}; } if (nofs.fileExistsSync(path)) { return nofs.writeFileSync(path, data, opts); } else { dir = npath.dirname(path); nofs.mkdirsSync(dir, opts.mode); return nofs.writeFileSync(path, data, opts); } }, /** * Write a object to a file, if its parent directory doesn't * exists, it will be created. * @param {String} path * @param {Any} obj The data object to save. * @param {Object | String} opts Extends the options of [outputFile](#outputFile-opts). * Defaults: * ```js * { * replacer: null, * space: null * } * ``` * @return {Promise} */ outputJson: function(path, obj, opts) { var err, error, str; if (opts == null) { opts = {}; } if (_.isString(opts)) { opts = { encoding: opts }; } try { str = JSON.stringify(obj, opts.replacer, opts.space); str += '\n'; } catch (error) { err = error; return Promise.reject(err); } return nofs.outputFile(path, str, opts); }, outputJsonSync: function(path, obj, opts) { var str; if (opts == null) { opts = {}; } if (_.isString(opts)) { opts = { encoding: opts }; } str = JSON.stringify(obj, opts.replacer, opts.space); str += '\n'; return nofs.outputFileSync(path, str, opts); }, /** * The path module nofs is using. * It's the native [io.js](iojs.org) path lib. * nofs will force all the path separators to `/`, * such as `C:\a\b` will be transformed to `C:/a/b`. * @type {Object} */ path: npath, /** * The `minimatch` lib. It has two extra methods: * - `isPmatch(String | Object) -> Pmatch | undefined` * It helps to detect if a string or an object is a minimatch. * * - `getPlainPath(Pmatch) -> String` * Helps to get the plain root path of a pattern. Such as `src/js/*.js` * will get `src/js` * * [Documentation](https://github.com/isaacs/minimatch) * * [Offline Documentation](?gotoDoc=minimatch/readme.md) * @example * ```js * nofs.pmatch('a/b/c.js', '**\/*.js'); * // output => true * nofs.pmatch.isPmatch('test*'); * // output => true * nofs.pmatch.isPmatch('test/b'); * // output => false * ``` */ pmatch: require('./pmatch'), /** * What promise this lib is using. * @type {Promise} */ Promise: Promise, /** * Same as the [`yaku/lib/utils`](https://github.com/ysmood/yaku#utils). * @type {Object} */ PromiseUtils: _.PromiseUtils, /** * Read A Json file and parse it to a object. * @param {String} path * @param {Object | String} opts Same with the native `nofs.readFile`. * @return {Promise} Resolves a parsed object. * @example * ```js * nofs.readJson('a.json').then((obj) => * console.log(obj.name, obj.age) * ); * ``` */ readJson: function(path, opts) { if (opts == null) { opts = {}; } return fs.readFile(path, opts).then(function(data) { var err, error; try { return JSON.parse(data + ''); } catch (error) { err = error; return Promise.reject(err); } }); }, readJsonSync: function(path, opts) { var data; if (opts == null) { opts = {}; } data = fs.readFileSync(path, opts); return JSON.parse(data + ''); }, /** * Walk through directory recursively with a iterator. * @param {String} path * @param {Object} opts Extends the options of [eachDir](#eachDir-opts), * with some extra options: * ```js * { * iter: (prev, path, isDir, stats) -> Promise | Any, * * // The init value of the walk. * init: undefined, * * isIterFileOnly: true * } * ``` * @return {Promise} Final resolved value. * @example * ```js * // Concat all files. * nofs.reduceDir('dir/path', { * init: '', * iter: (val, { path }) => * nofs.readFile(path).then((str) => * val += str + '\n' * ) * }).then((ret) => * console.log(ret) * ); * ``` */ reduceDir: function(path, opts) { var iter, prev; if (opts == null) { opts = {}; } _.defaults(opts, { isIterFileOnly: true }); prev = Promise.resolve(opts.init); iter = opts.iter; opts.iter = function(fileInfo) { return prev = prev.then(function(val) { val = iter(val, fileInfo); if (!val || !val.then) { return Promise.resolve(val); } }); }; return nofs.eachDir(path, opts).then(function() { return prev; }); }, reduceDirSync: function(path, opts) { var iter, prev; if (opts == null) { opts = {}; } _.defaults(opts, { isIterFileOnly: true }); prev = opts.init; iter = opts.iter; opts.iter = function(fileInfo) { return prev = iter(prev, fileInfo); }; nofs.eachDirSync(path, opts); return prev; }, /** * Remove a file or directory peacefully, same with the `rm -rf`. * @param {String} path * @param {Object} opts Extends the options of [eachDir](#eachDir-opts). But * the `isReverse` is fixed with `true`. Defaults: * ```js * { isFollowLink: false } * ``` * @return {Promise} */ remove: function(path, opts) { var removeOpts; if (opts == null) { opts = {}; } _.defaults(opts, { isFollowLink: false }); opts.isReverse = true; removeOpts = _.extend({ iter: function(arg) { var isDir, path; path = arg.path, isDir = arg.isDir; if (isDir) { return fs.rmdir(path); } else { return fs.unlink(path); } } }, opts, { isAutoPmatch: false }); opts.iter = function(arg) { var isDir, path; path = arg.path, isDir = arg.isDir; if (isDir) { return fs.rmdir(path)["catch"](function(err) { if (err.code === 'ENOTEMPTY') { return nofs.eachDir(path, removeOpts); } return Promise.reject(err); }); } else { return fs.unlink(path); } }; return nofs.eachDir(path, opts); }, removeSync: function(path, opts) { var removeOpts; if (opts == null) { opts = {}; } _.defaults(opts, { isFollowLink: false }); opts.isReverse = true; removeOpts = _.extend({ iter: function(arg) { var isDir, path; path = arg.path, isDir = arg.isDir; if (isDir) { return fs.rmdirSync(path); } else { return fs.unlinkSync(path); } } }, opts, { isAutoPmatch: false }); opts.iter = function(arg) { var err, error, isDir, path; path = arg.path, isDir = arg.isDir; if (isDir) { try { return fs.rmdirSync(path); } catch (error) { err = error; if (err.code === 'ENOTEMPTY') { return nofs.eachDirSync(path, removeOpts); } return Promise.reject(err); } } else { return fs.unlinkSync(path); } }; return nofs.eachDirSync(path, opts); }, /** * Change file access and modification times. * If the file does not exist, it is created. * @param {String} path * @param {Object} opts Default: * ```js * { * atime: Date.now(), * mtime: Date.now(), * mode: undefined * } * ``` * @return {Promise} If new file created, resolves true. */ touch: function(path, opts) { var now; if (opts == null) { opts = {}; } now = new Date; _.defaults(opts, { atime: now, mtime: now }); return nofs.fileExists(path).then(function(exists) { var p; if (exists) { p = Promise.resolve(); } else { p = nofs.outputFile(path, Buffer.from(''), opts); } return p.then(function () { return fs.utimes(path, opts.atime, opts.mtime); }).then(function () { return !exists; }); }); }, touchSync: function(path, opts) { var exists, now; if (opts == null) { opts = {}; } now = new Date; _.defaults(opts, { atime: now, mtime: now }); exists = nofs.fileExistsSync(path); if (!exists) { nofs.outputFileSync(path, Buffer.from(''), opts); } fs.utimesSync(path, opts.atime, opts.mtime); return !exists; }, /** * * Watch a file. If the file changes, the handler will be invoked. * You can change the polling interval by using `process.env.pollingWatch`. * Use `process.env.watchPersistent = 'off'` to disable the persistent. * Why not use `nofs.watch`? Because `nofs.watch` is unstable on some file * systems, such as Samba or OSX. * @param {String} path The file path * @param {Object} opts Defaults: * ```js * { * handler: (path, curr, prev, isDeletion) => {}, * * // Auto unwatch the file while file deletion. * autoUnwatch: true, * * persistent: process.env.watchPersistent != 'off', * interval: +process.env.pollingWatch || 300 * } * ``` * @return {Promise} It resolves the `StatWatcher` object: * ```js * { * path, * handler * } * ``` * @example * ```js * process.env.watchPersistent = 'off' * nofs.watchPath('a.js', { * handler: (path, curr, prev, isDeletion) => { * if (curr.mtime !== prev.mtime) * console.log(path); * } * }).then((watcher) => * nofs.unwatchFile(watcher.path, watcher.handler) * ); * ``` */ watchPath: function(path, opts) { var handler, watcher; if (opts == null) { opts = {}; } _.defaults(opts, { autoUnwatch: true, persistent: process.env.watchPersistent !== 'off', interval: +process.env.pollingWatch || 300 }); handler = function(curr, prev) { var isDeletion; isDeletion = curr.mtime.getTime() === 0; opts.handler(path, curr, prev, isDeletion); if (opts.autoUnwatch && isDeletion) { return fs.unwatchFile(path, handler); } }; watcher = fs.watchFile(path, opts, handler); return Promise.resolve(_.extend(watcher, { path: path, handler: handler })); }, /** * Watch files, when file changes, the handler will be invoked. * It is build on the top of `nofs.watchPath`. * @param {Array} patterns String array with minimatch syntax. * Such as `['*\/**.css', 'lib\/**\/*.js']`. * @param {Object} opts Same as the `nofs.watchPath`. * @return {Promise} It contains the wrapped watch listeners. * @example * ```js * nofs.watchFiles('*.js', handler: (path, curr, prev, isDeletion) => * console.log (path) * ); * ``` */ watchFiles: function(patterns, opts) { if (opts == null) { opts = {}; } return nofs.glob(patterns).then(function(paths) { return Promise.all(paths.map(function(path) { return nofs.watchPath(path, opts); })); }); }, /** * Watch directory and all the files in it. * It supports three types of change: create, modify, move, delete. * By default, `move` event is disabled. * It is build on the top of `nofs.watchPath`. * @param {String} root * @param {Object} opts Defaults: * ```js * { * handler: (type, path, oldPath, stats, oldStats) => {}, * * patterns: '**', // minimatch, string or array * * // Whether to watch POSIX hidden file. * all: false, * * // The minimatch options. * pmatch: {}, * * isEnableMoveEvent: false * } * ``` * @return {Promise} Resolves a object that keys are paths, * values are listeners. * @example * ```js * // Only current folder, and only watch js and css file. * nofs.watchDir('lib', { * pattern: '*.+(js|css)', * handler: (type, path, oldPath, stats, oldStats) => * console.log(type, path, stats.isDirectory(), oldStats.isDirectory()) * }); * ``` */ watchDir: function(root, opts) { var dirHandler, fileHandler, isSameFile, match, negateMath, ref, watchedList; if (opts == null) { opts = {}; } _.defaults(opts, { patterns: '**', pmatch: {}, all: false, error: function(err) { try { return console.error(err); } catch (err) {} } }); opts.pmatch.dot = opts.all; if (_.isString(opts.patterns)) { opts.patterns = [opts.patterns]; } opts.patterns = opts.patterns.map(function(p) { if (p[0] === '!') { return '!' + npath.join(root, p.slice(1)); } else { return npath.join(root, p); } }); ref = nofs.pmatch.matchMultiple(opts.patterns, opts.pmatch), match = ref.match, negateMath = ref.negateMath; watchedList = {}; // TODO: move event isSameFile = function(statsA, statsB) { // On Unix just "ino" will do the trick, but on Windows // "ino" is always zero. if (statsA.ctime.ino !== 0 && statsA.ctime.ino === statsB.ctime.ino) { return true; } // Since "size" for Windows is always zero, and the unit of "time" // is second, the code below is not reliable. return statsA.mtime.getTime() === statsB.mtime.getTime() && statsA.ctime.getTime() === statsB.ctime.getTime() && statsA.size === statsB.size; }; fileHandler = function(path, curr, prev, isDelete) { if (isDelete) { opts.handler('delete', path, null, curr, prev); return delete watchedList[path]; } else { return opts.handler('modify', path, null, curr, prev); } }; dirHandler = function(dir, curr, prev, isDelete) { // Possible Event Order // 1. modify event: file modify. // 2. delete event: file delete -> parent modify. // 3. create event: parent modify -> file create. // 4. move event: file delete -> parent modify -> file create. if (isDelete) { opts.handler('delete', dir, null, curr, prev); delete watchedList[dir]; return; } // Prevent high frequency concurrent fs changes, // we should to use Sync function here. But for // now if we don't need `move` event, everything is OK. return nofs.eachDir(dir, { all: opts.all, iter: function(fileInfo) { var path; path = fileInfo.path; if (watchedList[path]) { return; } if (fileInfo.isDir) { if (curr) { opts.handler('create', path, null, fileInfo.stats); } return nofs.watchPath(path, { handler: dirHandler }).then(function(listener) { if (listener) { return watchedList[path] = listener; } }); } else if (!negateMath(path) && match(path)) { if (curr) { opts.handler('create', path, null, fileInfo.stats); } return nofs.watchPath(path, { handler: fileHandler }).then(function(listener) { if (listener) { return watchedList[path] = listener; } }); } } }); }; return dirHandler(root).then(function() { return watchedList; }); }, /** * A `writeFile` shim for `< Node v0.10`. * @param {String} path * @param {String | Buffer} data * @param {String | Object} opts * @return {Promise} */ writeFile: function(path, data, opts) { var encoding, flag, mode; if (opts == null) { opts = {}; } switch (typeof opts) { case 'string': encoding = opts; break; case 'object': encoding = opts.encoding, flag = opts.flag, mode = opts.mode; break; default: return Promise.reject(new TypeError('Bad arguments')); } if (flag == null) { flag = 'w'; } if (mode == null) { mode = 0x1b6; } return fs.open(path, flag, mode).then(function(fd) { var buf, pos; buf = _.encodeNonBuffer(data, encoding); pos = flag.indexOf('a') > -1 ? null : 0; return fs.write(fd, buf, 0, buf.length, pos).then(function() { return fs.close(fd); }); }); }, writeFileSync: function(path, data, opts) { var buf, encoding, fd, flag, mode, pos; if (opts == null) { opts = {}; } switch (typeof opts) { case 'string': encoding = opts; break; case 'object': encoding = opts.encoding, flag = opts.flag, mode = opts.mode; break; default: throw new TypeError('Bad arguments'); } if (flag == null) { flag = 'w'; } if (mode == null) { mode = 0x1b6; } fd = fs.openSync(path, flag, mode); buf = _.encodeNonBuffer(data, encoding); pos = flag.indexOf('a') > -1 ? null : 0; fs.writeSync(fd, buf, 0, buf.length, pos); return fs.closeSync(fd); } }); (function() { var k, name, results; results = []; for (k in nofs) { if (k.slice(-4) === 'Sync') { name = k.slice(0, -4); fs[name] = _.PromiseUtils.callbackify(nofs[name]); } results.push(fs[k] = nofs[k]); } return results; })(); require('./alias')(fs); module.exports = fs;