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 |
|
19 |
|
20 | const fs = require('fs')
|
21 | const path = require('path')
|
22 | const rimraf = require('rimraf')
|
23 | const tmp = require('tmp')
|
24 |
|
25 | /**
|
26 | * @param {!Function} fn .
|
27 | * @return {!Promise<T>} .
|
28 | * @template T
|
29 | */
|
30 | function 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 | */
|
55 | exports.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 | */
|
82 | exports.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 | */
|
105 | exports.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 | */
|
150 | exports.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 | */
|
167 | exports.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 | */
|
177 | exports.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 | */
|
196 | exports.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 | */
|
205 | exports.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 | */
|
225 | exports.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 | */
|
252 | exports.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 | */
|
264 | exports.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 | */
|
275 | exports.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 | */
|
294 | exports.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 | */
|
330 | exports.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 | }
|