1 | // Copyright 2013 Selenium committers |
2 | // Copyright 2013 Software Freedom Conservancy |
3 | // |
4 | // Licensed under the Apache License, Version 2.0 (the "License"); |
5 | // you may not use this file except in compliance with the License. |
6 | // You may obtain a copy of the License at |
7 | // |
8 | // http://www.apache.org/licenses/LICENSE-2.0 |
9 | // |
10 | // Unless required by applicable law or agreed to in writing, software |
11 | // distributed under the License is distributed on an "AS IS" BASIS, |
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | // See the License for the specific language governing permissions and |
14 | // limitations under the License. |
15 | |
16 | var fs = require('fs'), |
17 | path = require('path'), |
18 | rimraf = require('rimraf'), |
19 | tmp = require('tmp'); |
20 | |
21 | var promise = require('..').promise; |
22 | |
23 | |
24 | var PATH_SEPARATOR = process.platform === 'win32' ? ';' : ':'; |
25 | |
26 | |
27 | // PUBLIC API |
28 | |
29 | |
30 | |
31 | /** |
32 | * Recursively removes a directory and all of its contents. This is equivalent |
33 | * to {@code rm -rf} on a POSIX system. |
34 | * @param {string} path Path to the directory to remove. |
35 | * @return {!promise.Promise} A promise to be resolved when the operation has |
36 | * completed. |
37 | */ |
38 | exports.rmDir = function(path) { |
39 | return new promise.Promise(function(fulfill, reject) { |
40 | var numAttempts = 0; |
41 | attemptRm(); |
42 | function attemptRm() { |
43 | numAttempts += 1; |
44 | rimraf(path, function(err) { |
45 | if (err) { |
46 | if (err.code === 'ENOTEMPTY' && numAttempts < 2) { |
47 | attemptRm(); |
48 | return; |
49 | } |
50 | reject(err); |
51 | } else { |
52 | fulfill(); |
53 | } |
54 | }); |
55 | } |
56 | }); |
57 | }; |
58 | |
59 | |
60 | /** |
61 | * Copies one file to another. |
62 | * @param {string} src The source file. |
63 | * @param {string} dst The destination file. |
64 | * @return {!promise.Promise.<string>} A promise for the copied file's path. |
65 | */ |
66 | exports.copy = function(src, dst) { |
67 | var copied = promise.defer(); |
68 | |
69 | var rs = fs.createReadStream(src); |
70 | rs.on('error', copied.reject); |
71 | rs.on('end', function() { |
72 | copied.fulfill(dst); |
73 | }); |
74 | |
75 | var ws = fs.createWriteStream(dst); |
76 | ws.on('error', copied.reject); |
77 | |
78 | rs.pipe(ws); |
79 | |
80 | return copied.promise; |
81 | }; |
82 | |
83 | |
84 | /** |
85 | * Recursively copies the contents of one directory to another. |
86 | * @param {string} src The source directory to copy. |
87 | * @param {string} dst The directory to copy into. |
88 | * @param {(RegEx|function(string): boolean)=} opt_exclude An exclusion filter |
89 | * as either a regex or predicate function. All files matching this filter |
90 | * will not be copied. |
91 | * @return {!promise.Promise.<string>} A promise for the destination |
92 | * directory's path once all files have been copied. |
93 | */ |
94 | exports.copyDir = function(src, dst, opt_exclude) { |
95 | var predicate = opt_exclude; |
96 | if (opt_exclude && typeof opt_exclude !== 'function') { |
97 | predicate = function(p) { |
98 | return !opt_exclude.test(p); |
99 | }; |
100 | } |
101 | |
102 | // TODO(jleyba): Make this function completely async. |
103 | if (!fs.existsSync(dst)) { |
104 | fs.mkdirSync(dst); |
105 | } |
106 | |
107 | var files = fs.readdirSync(src); |
108 | files = files.map(function(file) { |
109 | return path.join(src, file); |
110 | }); |
111 | |
112 | if (predicate) { |
113 | files = files.filter(predicate); |
114 | } |
115 | |
116 | var results = []; |
117 | files.forEach(function(file) { |
118 | var stats = fs.statSync(file); |
119 | var target = path.join(dst, path.basename(file)); |
120 | |
121 | if (stats.isDirectory()) { |
122 | if (!fs.existsSync(target)) { |
123 | fs.mkdirSync(target, stats.mode); |
124 | } |
125 | results.push(exports.copyDir(file, target, predicate)); |
126 | } else { |
127 | results.push(exports.copy(file, target)); |
128 | } |
129 | }); |
130 | |
131 | return promise.all(results).then(function() { |
132 | return dst; |
133 | }); |
134 | }; |
135 | |
136 | |
137 | /** |
138 | * Tests if a file path exists. |
139 | * @param {string} path The path to test. |
140 | * @return {!promise.Promise.<boolean>} A promise for whether the file exists. |
141 | */ |
142 | exports.exists = function(path) { |
143 | var result = promise.defer(); |
144 | fs.exists(path, result.fulfill); |
145 | return result.promise; |
146 | }; |
147 | |
148 | |
149 | /** |
150 | * Deletes a name from the filesystem and possibly the file it refers to. Has |
151 | * no effect if the file does not exist. |
152 | * @param {string} path The path to remove. |
153 | * @return {!promise.Promise} A promise for when the file has been removed. |
154 | */ |
155 | exports.unlink = function(path) { |
156 | return new promise.Promise(function(fulfill, reject) { |
157 | fs.exists(path, function(exists) { |
158 | if (exists) { |
159 | fs.unlink(path, function(err) { |
160 | err && reject(err) || fulfill(); |
161 | }); |
162 | } else { |
163 | fulfill(); |
164 | } |
165 | }); |
166 | }); |
167 | }; |
168 | |
169 | |
170 | /** |
171 | * @return {!promise.Promise.<string>} A promise for the path to a temporary |
172 | * directory. |
173 | * @see https://www.npmjs.org/package/tmp |
174 | */ |
175 | exports.tmpDir = function() { |
176 | return promise.checkedNodeCall(tmp.dir); |
177 | }; |
178 | |
179 | |
180 | /** |
181 | * @param {{postfix: string}=} opt_options Temporary file options. |
182 | * @return {!promise.Promise.<string>} A promise for the path to a temporary |
183 | * file. |
184 | * @see https://www.npmjs.org/package/tmp |
185 | */ |
186 | exports.tmpFile = function(opt_options) { |
187 | // |tmp.file| checks arguments length to detect options rather than doing a |
188 | // truthy check, so we must only pass options if there are some to pass. |
189 | return opt_options ? |
190 | promise.checkedNodeCall(tmp.file, opt_options) : |
191 | promise.checkedNodeCall(tmp.file); |
192 | }; |
193 | |
194 | |
195 | /** |
196 | * Searches the {@code PATH} environment variable for the given file. |
197 | * @param {string} file The file to locate on the PATH. |
198 | * @param {boolean=} opt_checkCwd Whether to always start with the search with |
199 | * the current working directory, regardless of whether it is explicitly |
200 | * listed on the PATH. |
201 | * @return {?string} Path to the located file, or {@code null} if it could |
202 | * not be found. |
203 | */ |
204 | exports.findInPath = function(file, opt_checkCwd) { |
205 | if (opt_checkCwd) { |
206 | var tmp = path.join(process.cwd(), file); |
207 | if (fs.existsSync(tmp)) { |
208 | return tmp; |
209 | } |
210 | } |
211 | |
212 | var dirs = process.env['PATH'].split(PATH_SEPARATOR); |
213 | var found = null; |
214 | dirs.forEach(function(dir) { |
215 | var tmp = path.join(dir, file); |
216 | if (!found && fs.existsSync(tmp)) { |
217 | found = tmp; |
218 | } |
219 | }); |
220 | return found; |
221 | }; |