UNPKG

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