index.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. // Licensed to the Software Freedom Conservancy (SFC) under one
  2. // or more contributor license agreements. See the NOTICE file
  3. // distributed with this work for additional information
  4. // regarding copyright ownership. The SFC licenses this file
  5. // to you under the Apache License, Version 2.0 (the
  6. // "License"); you may not use this file except in compliance
  7. // with the License. You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing,
  12. // software distributed under the License is distributed on an
  13. // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. // KIND, either express or implied. See the License for the
  15. // specific language governing permissions and limitations
  16. // under the License.
  17. 'use strict';
  18. var fs = require('fs'),
  19. path = require('path'),
  20. rimraf = require('rimraf'),
  21. tmp = require('tmp');
  22. /**
  23. * @param {!Function} fn .
  24. * @return {!Promise<T>} .
  25. * @template T
  26. */
  27. function checkedCall(fn) {
  28. return new Promise((resolve, reject) => {
  29. try {
  30. fn((err, value) => {
  31. if (err) {
  32. reject(err);
  33. } else {
  34. resolve(value);
  35. }
  36. });
  37. } catch (e) {
  38. reject(e);
  39. }
  40. });
  41. }
  42. // PUBLIC API
  43. /**
  44. * Recursively removes a directory and all of its contents. This is equivalent
  45. * to {@code rm -rf} on a POSIX system.
  46. * @param {string} dirPath Path to the directory to remove.
  47. * @return {!Promise} A promise to be resolved when the operation has
  48. * completed.
  49. */
  50. exports.rmDir = function(dirPath) {
  51. return new Promise(function(fulfill, reject) {
  52. var numAttempts = 0;
  53. attemptRm();
  54. function attemptRm() {
  55. numAttempts += 1;
  56. rimraf(dirPath, function(err) {
  57. if (err) {
  58. if (err.code && err.code === 'ENOTEMPTY' && numAttempts < 2) {
  59. attemptRm();
  60. return;
  61. }
  62. reject(err);
  63. } else {
  64. fulfill();
  65. }
  66. });
  67. }
  68. });
  69. };
  70. /**
  71. * Copies one file to another.
  72. * @param {string} src The source file.
  73. * @param {string} dst The destination file.
  74. * @return {!Promise<string>} A promise for the copied file's path.
  75. */
  76. exports.copy = function(src, dst) {
  77. return new Promise(function(fulfill, reject) {
  78. var rs = fs.createReadStream(src);
  79. rs.on('error', reject);
  80. rs.on('end', () => fulfill(dst));
  81. var ws = fs.createWriteStream(dst);
  82. ws.on('error', reject);
  83. rs.pipe(ws);
  84. });
  85. };
  86. /**
  87. * Recursively copies the contents of one directory to another.
  88. * @param {string} src The source directory to copy.
  89. * @param {string} dst The directory to copy into.
  90. * @param {(RegExp|function(string): boolean)=} opt_exclude An exclusion filter
  91. * as either a regex or predicate function. All files matching this filter
  92. * will not be copied.
  93. * @return {!Promise<string>} A promise for the destination
  94. * directory's path once all files have been copied.
  95. */
  96. exports.copyDir = function(src, dst, opt_exclude) {
  97. var predicate = opt_exclude;
  98. if (opt_exclude && typeof opt_exclude !== 'function') {
  99. predicate = function(p) {
  100. return !opt_exclude.test(p);
  101. };
  102. }
  103. // TODO(jleyba): Make this function completely async.
  104. if (!fs.existsSync(dst)) {
  105. fs.mkdirSync(dst);
  106. }
  107. var files = fs.readdirSync(src);
  108. files = files.map(function(file) {
  109. return path.join(src, file);
  110. });
  111. if (predicate) {
  112. files = files.filter(/** @type {function(string): boolean} */(predicate));
  113. }
  114. var results = [];
  115. files.forEach(function(file) {
  116. var stats = fs.statSync(file);
  117. var target = path.join(dst, path.basename(file));
  118. if (stats.isDirectory()) {
  119. if (!fs.existsSync(target)) {
  120. fs.mkdirSync(target, stats.mode);
  121. }
  122. results.push(exports.copyDir(file, target, predicate));
  123. } else {
  124. results.push(exports.copy(file, target));
  125. }
  126. });
  127. return Promise.all(results).then(() => dst);
  128. };
  129. /**
  130. * Tests if a file path exists.
  131. * @param {string} aPath The path to test.
  132. * @return {!Promise<boolean>} A promise for whether the file exists.
  133. */
  134. exports.exists = function(aPath) {
  135. return new Promise(function(fulfill, reject) {
  136. let type = typeof aPath;
  137. if (type !== 'string') {
  138. reject(TypeError(`expected string path, but got ${type}`));
  139. } else {
  140. fs.exists(aPath, fulfill);
  141. }
  142. });
  143. };
  144. /**
  145. * Calls `stat(2)`.
  146. * @param {string} aPath The path to stat.
  147. * @return {!Promise<!fs.Stats>} A promise for the file stats.
  148. */
  149. exports.stat = function stat(aPath) {
  150. return checkedCall(callback => fs.stat(aPath, callback));
  151. };
  152. /**
  153. * Deletes a name from the filesystem and possibly the file it refers to. Has
  154. * no effect if the file does not exist.
  155. * @param {string} aPath The path to remove.
  156. * @return {!Promise} A promise for when the file has been removed.
  157. */
  158. exports.unlink = function(aPath) {
  159. return new Promise(function(fulfill, reject) {
  160. fs.exists(aPath, function(exists) {
  161. if (exists) {
  162. fs.unlink(aPath, function(err) {
  163. err && reject(err) || fulfill();
  164. });
  165. } else {
  166. fulfill();
  167. }
  168. });
  169. });
  170. };
  171. /**
  172. * @return {!Promise<string>} A promise for the path to a temporary directory.
  173. * @see https://www.npmjs.org/package/tmp
  174. */
  175. exports.tmpDir = function() {
  176. return checkedCall(tmp.dir);
  177. };
  178. /**
  179. * @param {{postfix: string}=} opt_options Temporary file options.
  180. * @return {!Promise<string>} A promise for the path to a temporary file.
  181. * @see https://www.npmjs.org/package/tmp
  182. */
  183. exports.tmpFile = function(opt_options) {
  184. return checkedCall(callback => {
  185. // |tmp.file| checks arguments length to detect options rather than doing a
  186. // truthy check, so we must only pass options if there are some to pass.
  187. if (opt_options) {
  188. tmp.file(opt_options, callback);
  189. } else {
  190. tmp.file(callback);
  191. }
  192. });
  193. };
  194. /**
  195. * Searches the {@code PATH} environment variable for the given file.
  196. * @param {string} file The file to locate on the PATH.
  197. * @param {boolean=} opt_checkCwd Whether to always start with the search with
  198. * the current working directory, regardless of whether it is explicitly
  199. * listed on the PATH.
  200. * @return {?string} Path to the located file, or {@code null} if it could
  201. * not be found.
  202. */
  203. exports.findInPath = function(file, opt_checkCwd) {
  204. let dirs = [];
  205. if (opt_checkCwd) {
  206. dirs.push(process.cwd());
  207. }
  208. dirs.push.apply(dirs, process.env['PATH'].split(path.delimiter));
  209. let foundInDir = dirs.find(dir => {
  210. let tmp = path.join(dir, file);
  211. try {
  212. let stats = fs.statSync(tmp);
  213. return stats.isFile() && !stats.isDirectory();
  214. } catch (ex) {
  215. return false;
  216. }
  217. });
  218. return foundInDir ? path.join(foundInDir, file) : null;
  219. };
  220. /**
  221. * Reads the contents of the given file.
  222. *
  223. * @param {string} aPath Path to the file to read.
  224. * @return {!Promise<!Buffer>} A promise that will resolve with a buffer of the
  225. * file contents.
  226. */
  227. exports.read = function(aPath) {
  228. return checkedCall(callback => fs.readFile(aPath, callback));
  229. };
  230. /**
  231. * Writes to a file.
  232. *
  233. * @param {string} aPath Path to the file to write to.
  234. * @param {(string|!Buffer)} data The data to write.
  235. * @return {!Promise} A promise that will resolve when the operation has
  236. * completed.
  237. */
  238. exports.write = function(aPath, data) {
  239. return checkedCall(callback => fs.writeFile(aPath, data, callback));
  240. };
  241. /**
  242. * Creates a directory.
  243. *
  244. * @param {string} aPath The directory path.
  245. * @return {!Promise<string>} A promise that will resolve with the path of the
  246. * created directory.
  247. */
  248. exports.mkdir = function(aPath) {
  249. return checkedCall(callback => {
  250. fs.mkdir(aPath, undefined, err => {
  251. if (err && err.code !== 'EEXIST') {
  252. callback(err);
  253. } else {
  254. callback(null, aPath);
  255. }
  256. });
  257. });
  258. };
  259. /**
  260. * Recursively creates a directory and any ancestors that do not yet exist.
  261. *
  262. * @param {string} dir The directory path to create.
  263. * @return {!Promise<string>} A promise that will resolve with the path of the
  264. * created directory.
  265. */
  266. exports.mkdirp = function mkdirp(dir) {
  267. return checkedCall(callback => {
  268. fs.mkdir(dir, undefined, err => {
  269. if (!err) {
  270. callback(null, dir);
  271. return;
  272. }
  273. switch (err.code) {
  274. case 'EEXIST':
  275. callback(null, dir);
  276. return;
  277. case 'ENOENT':
  278. return mkdirp(path.dirname(dir))
  279. .then(() => mkdirp(dir))
  280. .then(() => callback(null, dir), err => callback(err));
  281. default:
  282. callback(err);
  283. return;
  284. }
  285. });
  286. });
  287. };
  288. /**
  289. * Recursively walks a directory, returning a promise that will resolve with
  290. * a list of all files/directories seen.
  291. *
  292. * @param {string} rootPath the directory to walk.
  293. * @return {!Promise<!Array<{path: string, dir: boolean}>>} a promise that will
  294. * resolve with a list of entries seen. For each entry, the recorded path
  295. * will be relative to `rootPath`.
  296. */
  297. exports.walkDir = function(rootPath) {
  298. let seen = [];
  299. return (function walk(dir) {
  300. return checkedCall(callback => fs.readdir(dir, callback))
  301. .then(files => Promise.all(files.map(file => {
  302. file = path.join(dir, file);
  303. return checkedCall(cb => fs.stat(file, cb)).then(stats => {
  304. seen.push({
  305. path: path.relative(rootPath, file),
  306. dir: stats.isDirectory()
  307. });
  308. return stats.isDirectory() && walk(file);
  309. });
  310. })));
  311. })(rootPath).then(() => seen);
  312. };