123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- // Licensed to the Software Freedom Conservancy (SFC) under one
- // or more contributor license agreements. See the NOTICE file
- // distributed with this work for additional information
- // regarding copyright ownership. The SFC licenses this file
- // to you under the Apache License, Version 2.0 (the
- // "License"); you may not use this file except in compliance
- // with the License. You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing,
- // software distributed under the License is distributed on an
- // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- // KIND, either express or implied. See the License for the
- // specific language governing permissions and limitations
- // under the License.
- 'use strict';
- var fs = require('fs'),
- path = require('path'),
- rimraf = require('rimraf'),
- tmp = require('tmp');
- /**
- * @param {!Function} fn .
- * @return {!Promise<T>} .
- * @template T
- */
- function checkedCall(fn) {
- return new Promise((resolve, reject) => {
- try {
- fn((err, value) => {
- if (err) {
- reject(err);
- } else {
- resolve(value);
- }
- });
- } catch (e) {
- reject(e);
- }
- });
- }
- // PUBLIC API
- /**
- * Recursively removes a directory and all of its contents. This is equivalent
- * to {@code rm -rf} on a POSIX system.
- * @param {string} dirPath Path to the directory to remove.
- * @return {!Promise} A promise to be resolved when the operation has
- * completed.
- */
- exports.rmDir = function(dirPath) {
- return new Promise(function(fulfill, reject) {
- var numAttempts = 0;
- attemptRm();
- function attemptRm() {
- numAttempts += 1;
- rimraf(dirPath, function(err) {
- if (err) {
- if (err.code && err.code === 'ENOTEMPTY' && numAttempts < 2) {
- attemptRm();
- return;
- }
- reject(err);
- } else {
- fulfill();
- }
- });
- }
- });
- };
- /**
- * Copies one file to another.
- * @param {string} src The source file.
- * @param {string} dst The destination file.
- * @return {!Promise<string>} A promise for the copied file's path.
- */
- exports.copy = function(src, dst) {
- return new Promise(function(fulfill, reject) {
- var rs = fs.createReadStream(src);
- rs.on('error', reject);
- rs.on('end', () => fulfill(dst));
- var ws = fs.createWriteStream(dst);
- ws.on('error', reject);
- rs.pipe(ws);
- });
- };
- /**
- * Recursively copies the contents of one directory to another.
- * @param {string} src The source directory to copy.
- * @param {string} dst The directory to copy into.
- * @param {(RegExp|function(string): boolean)=} opt_exclude An exclusion filter
- * as either a regex or predicate function. All files matching this filter
- * will not be copied.
- * @return {!Promise<string>} A promise for the destination
- * directory's path once all files have been copied.
- */
- exports.copyDir = function(src, dst, opt_exclude) {
- var predicate = opt_exclude;
- if (opt_exclude && typeof opt_exclude !== 'function') {
- predicate = function(p) {
- return !opt_exclude.test(p);
- };
- }
- // TODO(jleyba): Make this function completely async.
- if (!fs.existsSync(dst)) {
- fs.mkdirSync(dst);
- }
- var files = fs.readdirSync(src);
- files = files.map(function(file) {
- return path.join(src, file);
- });
- if (predicate) {
- files = files.filter(/** @type {function(string): boolean} */(predicate));
- }
- var results = [];
- files.forEach(function(file) {
- var stats = fs.statSync(file);
- var target = path.join(dst, path.basename(file));
- if (stats.isDirectory()) {
- if (!fs.existsSync(target)) {
- fs.mkdirSync(target, stats.mode);
- }
- results.push(exports.copyDir(file, target, predicate));
- } else {
- results.push(exports.copy(file, target));
- }
- });
- return Promise.all(results).then(() => dst);
- };
- /**
- * Tests if a file path exists.
- * @param {string} aPath The path to test.
- * @return {!Promise<boolean>} A promise for whether the file exists.
- */
- exports.exists = function(aPath) {
- return new Promise(function(fulfill, reject) {
- let type = typeof aPath;
- if (type !== 'string') {
- reject(TypeError(`expected string path, but got ${type}`));
- } else {
- fs.exists(aPath, fulfill);
- }
- });
- };
- /**
- * Calls `stat(2)`.
- * @param {string} aPath The path to stat.
- * @return {!Promise<!fs.Stats>} A promise for the file stats.
- */
- exports.stat = function stat(aPath) {
- return checkedCall(callback => fs.stat(aPath, callback));
- };
- /**
- * Deletes a name from the filesystem and possibly the file it refers to. Has
- * no effect if the file does not exist.
- * @param {string} aPath The path to remove.
- * @return {!Promise} A promise for when the file has been removed.
- */
- exports.unlink = function(aPath) {
- return new Promise(function(fulfill, reject) {
- fs.exists(aPath, function(exists) {
- if (exists) {
- fs.unlink(aPath, function(err) {
- err && reject(err) || fulfill();
- });
- } else {
- fulfill();
- }
- });
- });
- };
- /**
- * @return {!Promise<string>} A promise for the path to a temporary directory.
- * @see https://www.npmjs.org/package/tmp
- */
- exports.tmpDir = function() {
- return checkedCall(tmp.dir);
- };
- /**
- * @param {{postfix: string}=} opt_options Temporary file options.
- * @return {!Promise<string>} A promise for the path to a temporary file.
- * @see https://www.npmjs.org/package/tmp
- */
- exports.tmpFile = function(opt_options) {
- return checkedCall(callback => {
- // |tmp.file| checks arguments length to detect options rather than doing a
- // truthy check, so we must only pass options if there are some to pass.
- if (opt_options) {
- tmp.file(opt_options, callback);
- } else {
- tmp.file(callback);
- }
- });
- };
- /**
- * Searches the {@code PATH} environment variable for the given file.
- * @param {string} file The file to locate on the PATH.
- * @param {boolean=} opt_checkCwd Whether to always start with the search with
- * the current working directory, regardless of whether it is explicitly
- * listed on the PATH.
- * @return {?string} Path to the located file, or {@code null} if it could
- * not be found.
- */
- exports.findInPath = function(file, opt_checkCwd) {
- let dirs = [];
- if (opt_checkCwd) {
- dirs.push(process.cwd());
- }
- dirs.push.apply(dirs, process.env['PATH'].split(path.delimiter));
- let foundInDir = dirs.find(dir => {
- let tmp = path.join(dir, file);
- try {
- let stats = fs.statSync(tmp);
- return stats.isFile() && !stats.isDirectory();
- } catch (ex) {
- return false;
- }
- });
- return foundInDir ? path.join(foundInDir, file) : null;
- };
- /**
- * Reads the contents of the given file.
- *
- * @param {string} aPath Path to the file to read.
- * @return {!Promise<!Buffer>} A promise that will resolve with a buffer of the
- * file contents.
- */
- exports.read = function(aPath) {
- return checkedCall(callback => fs.readFile(aPath, callback));
- };
- /**
- * Writes to a file.
- *
- * @param {string} aPath Path to the file to write to.
- * @param {(string|!Buffer)} data The data to write.
- * @return {!Promise} A promise that will resolve when the operation has
- * completed.
- */
- exports.write = function(aPath, data) {
- return checkedCall(callback => fs.writeFile(aPath, data, callback));
- };
- /**
- * Creates a directory.
- *
- * @param {string} aPath The directory path.
- * @return {!Promise<string>} A promise that will resolve with the path of the
- * created directory.
- */
- exports.mkdir = function(aPath) {
- return checkedCall(callback => {
- fs.mkdir(aPath, undefined, err => {
- if (err && err.code !== 'EEXIST') {
- callback(err);
- } else {
- callback(null, aPath);
- }
- });
- });
- };
- /**
- * Recursively creates a directory and any ancestors that do not yet exist.
- *
- * @param {string} dir The directory path to create.
- * @return {!Promise<string>} A promise that will resolve with the path of the
- * created directory.
- */
- exports.mkdirp = function mkdirp(dir) {
- return checkedCall(callback => {
- fs.mkdir(dir, undefined, err => {
- if (!err) {
- callback(null, dir);
- return;
- }
- switch (err.code) {
- case 'EEXIST':
- callback(null, dir);
- return;
- case 'ENOENT':
- return mkdirp(path.dirname(dir))
- .then(() => mkdirp(dir))
- .then(() => callback(null, dir), err => callback(err));
- default:
- callback(err);
- return;
- }
- });
- });
- };
- /**
- * Recursively walks a directory, returning a promise that will resolve with
- * a list of all files/directories seen.
- *
- * @param {string} rootPath the directory to walk.
- * @return {!Promise<!Array<{path: string, dir: boolean}>>} a promise that will
- * resolve with a list of entries seen. For each entry, the recorded path
- * will be relative to `rootPath`.
- */
- exports.walkDir = function(rootPath) {
- let seen = [];
- return (function walk(dir) {
- return checkedCall(callback => fs.readdir(dir, callback))
- .then(files => Promise.all(files.map(file => {
- file = path.join(dir, file);
- return checkedCall(cb => fs.stat(file, cb)).then(stats => {
- seen.push({
- path: path.relative(rootPath, file),
- dir: stats.isDirectory()
- });
- return stats.isDirectory() && walk(file);
- });
- })));
- })(rootPath).then(() => seen);
- };
|