UNPKG

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