1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const os = require('os');
|
18 | const fs = require('fs');
|
19 | const path = require('path');
|
20 | const extract = require('extract-zip');
|
21 | const util = require('util');
|
22 | const URL = require('url');
|
23 | const {helper, assert} = require('./helper');
|
24 | const removeRecursive = require('rimraf');
|
25 |
|
26 | const ProxyAgent = require('https-proxy-agent');
|
27 |
|
28 | const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
|
29 |
|
30 | const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
|
31 | const downloadURLs = {
|
32 | linux: '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
|
33 | mac: '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
|
34 | win32: '%s/chromium-browser-snapshots/Win/%d/chrome-win32.zip',
|
35 | win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',
|
36 | };
|
37 |
|
38 | const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
39 | const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
40 | const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
41 | const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
42 |
|
43 | function existsAsync(filePath) {
|
44 | let fulfill = null;
|
45 | const promise = new Promise(x => fulfill = x);
|
46 | fs.access(filePath, err => fulfill(!err));
|
47 | return promise;
|
48 | }
|
49 |
|
50 | class BrowserFetcher {
|
51 | |
52 |
|
53 |
|
54 | constructor(options = {}) {
|
55 | this._downloadsFolder = options.path || path.join(helper.projectRoot(), '.local-chromium');
|
56 | this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
|
57 | this._platform = options.platform || '';
|
58 | if (!this._platform) {
|
59 | const platform = os.platform();
|
60 | if (platform === 'darwin')
|
61 | this._platform = 'mac';
|
62 | else if (platform === 'linux')
|
63 | this._platform = 'linux';
|
64 | else if (platform === 'win32')
|
65 | this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
|
66 | assert(this._platform, 'Unsupported platform: ' + os.platform());
|
67 | }
|
68 | const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
|
69 | assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
|
70 | }
|
71 |
|
72 | |
73 |
|
74 |
|
75 | platform() {
|
76 | return this._platform;
|
77 | }
|
78 |
|
79 | |
80 |
|
81 |
|
82 |
|
83 | canDownload(revision) {
|
84 | const url = util.format(downloadURLs[this._platform], this._downloadHost, revision);
|
85 |
|
86 | let resolve;
|
87 | const promise = new Promise(x => resolve = x);
|
88 | const request = httpRequest(url, 'HEAD', response => {
|
89 | resolve(response.statusCode === 200);
|
90 | });
|
91 | request.on('error', error => {
|
92 | console.error(error);
|
93 | resolve(false);
|
94 | });
|
95 | return promise;
|
96 | }
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
103 | download(revision, progressCallback) {return (fn => {
|
104 | const gen = fn.call(this);
|
105 | return new Promise((resolve, reject) => {
|
106 | function step(key, arg) {
|
107 | let info, value;
|
108 | try {
|
109 | info = gen[key](arg);
|
110 | value = info.value;
|
111 | } catch (error) {
|
112 | reject(error);
|
113 | return;
|
114 | }
|
115 | if (info.done) {
|
116 | resolve(value);
|
117 | } else {
|
118 | return Promise.resolve(value).then(
|
119 | value => {
|
120 | step('next', value);
|
121 | },
|
122 | err => {
|
123 | step('throw', err);
|
124 | });
|
125 | }
|
126 | }
|
127 | return step('next');
|
128 | });
|
129 | })(function*(){
|
130 | let url = downloadURLs[this._platform];
|
131 | url = util.format(url, this._downloadHost, revision);
|
132 | const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
|
133 | const folderPath = this._getFolderPath(revision);
|
134 | if ((yield existsAsync(folderPath)))
|
135 | return this.revisionInfo(revision);
|
136 | if (!((yield existsAsync(this._downloadsFolder))))
|
137 | (yield mkdirAsync(this._downloadsFolder));
|
138 | try {
|
139 | (yield downloadFile(url, zipPath, progressCallback));
|
140 | (yield extractZip(zipPath, folderPath));
|
141 | } finally {
|
142 | if ((yield existsAsync(zipPath)))
|
143 | (yield unlinkAsync(zipPath));
|
144 | }
|
145 | const revisionInfo = this.revisionInfo(revision);
|
146 | if (revisionInfo)
|
147 | (yield chmodAsync(revisionInfo.executablePath, 0o755));
|
148 | return revisionInfo;
|
149 | });}
|
150 |
|
151 | |
152 |
|
153 |
|
154 | localRevisions() {return (fn => {
|
155 | const gen = fn.call(this);
|
156 | return new Promise((resolve, reject) => {
|
157 | function step(key, arg) {
|
158 | let info, value;
|
159 | try {
|
160 | info = gen[key](arg);
|
161 | value = info.value;
|
162 | } catch (error) {
|
163 | reject(error);
|
164 | return;
|
165 | }
|
166 | if (info.done) {
|
167 | resolve(value);
|
168 | } else {
|
169 | return Promise.resolve(value).then(
|
170 | value => {
|
171 | step('next', value);
|
172 | },
|
173 | err => {
|
174 | step('throw', err);
|
175 | });
|
176 | }
|
177 | }
|
178 | return step('next');
|
179 | });
|
180 | })(function*(){
|
181 | if (!(yield existsAsync(this._downloadsFolder)))
|
182 | return [];
|
183 | const fileNames = (yield readdirAsync(this._downloadsFolder));
|
184 | return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
|
185 | });}
|
186 |
|
187 | |
188 |
|
189 |
|
190 |
|
191 | remove(revision) {return (fn => {
|
192 | const gen = fn.call(this);
|
193 | return new Promise((resolve, reject) => {
|
194 | function step(key, arg) {
|
195 | let info, value;
|
196 | try {
|
197 | info = gen[key](arg);
|
198 | value = info.value;
|
199 | } catch (error) {
|
200 | reject(error);
|
201 | return;
|
202 | }
|
203 | if (info.done) {
|
204 | resolve(value);
|
205 | } else {
|
206 | return Promise.resolve(value).then(
|
207 | value => {
|
208 | step('next', value);
|
209 | },
|
210 | err => {
|
211 | step('throw', err);
|
212 | });
|
213 | }
|
214 | }
|
215 | return step('next');
|
216 | });
|
217 | })(function*(){
|
218 | const folderPath = this._getFolderPath(revision);
|
219 | assert((yield existsAsync(folderPath)), `Failed to remove: revision ${revision} is not downloaded`);
|
220 | (yield new Promise(fulfill => removeRecursive(folderPath, fulfill)));
|
221 | });}
|
222 |
|
223 | |
224 |
|
225 |
|
226 |
|
227 | revisionInfo(revision) {
|
228 | const folderPath = this._getFolderPath(revision);
|
229 | let executablePath = '';
|
230 | if (this._platform === 'mac')
|
231 | executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
|
232 | else if (this._platform === 'linux')
|
233 | executablePath = path.join(folderPath, 'chrome-linux', 'chrome');
|
234 | else if (this._platform === 'win32' || this._platform === 'win64')
|
235 | executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
|
236 | else
|
237 | throw new Error('Unsupported platform: ' + this._platform);
|
238 | let url = downloadURLs[this._platform];
|
239 | url = util.format(url, this._downloadHost, revision);
|
240 | const local = fs.existsSync(folderPath);
|
241 | return {revision, executablePath, folderPath, local, url};
|
242 | }
|
243 |
|
244 | |
245 |
|
246 |
|
247 |
|
248 | _getFolderPath(revision) {
|
249 | return path.join(this._downloadsFolder, this._platform + '-' + revision);
|
250 | }
|
251 | }
|
252 |
|
253 | module.exports = BrowserFetcher;
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 | function parseFolderPath(folderPath) {
|
260 | const name = path.basename(folderPath);
|
261 | const splits = name.split('-');
|
262 | if (splits.length !== 2)
|
263 | return null;
|
264 | const [platform, revision] = splits;
|
265 | if (!downloadURLs[platform])
|
266 | return null;
|
267 | return {platform, revision};
|
268 | }
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 | function downloadFile(url, destinationPath, progressCallback) {
|
277 | let fulfill, reject;
|
278 | let downloadedBytes = 0;
|
279 | let totalBytes = 0;
|
280 |
|
281 | const promise = new Promise((x, y) => { fulfill = x; reject = y; });
|
282 |
|
283 | const request = httpRequest(url, 'GET', response => {
|
284 | if (response.statusCode !== 200) {
|
285 | const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
|
286 |
|
287 | response.resume();
|
288 | reject(error);
|
289 | return;
|
290 | }
|
291 | const file = fs.createWriteStream(destinationPath);
|
292 | file.on('finish', () => fulfill());
|
293 | file.on('error', error => reject(error));
|
294 | response.pipe(file);
|
295 | totalBytes = parseInt( (response.headers['content-length']), 10);
|
296 | if (progressCallback)
|
297 | response.on('data', onData);
|
298 | });
|
299 | request.on('error', error => reject(error));
|
300 | return promise;
|
301 |
|
302 | function onData(chunk) {
|
303 | downloadedBytes += chunk.length;
|
304 | progressCallback(downloadedBytes, totalBytes);
|
305 | }
|
306 | }
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 | function extractZip(zipPath, folderPath) {
|
314 | return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
|
315 | if (err)
|
316 | reject(err);
|
317 | else
|
318 | fulfill();
|
319 | }));
|
320 | }
|
321 |
|
322 | function httpRequest(url, method, response) {
|
323 |
|
324 | const options = URL.parse(url);
|
325 | options.method = method;
|
326 |
|
327 | const proxyURL = getProxyForUrl(url);
|
328 | if (proxyURL) {
|
329 |
|
330 | const parsedProxyURL = URL.parse(proxyURL);
|
331 | parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
332 |
|
333 | options.agent = new ProxyAgent(parsedProxyURL);
|
334 | options.rejectUnauthorized = false;
|
335 | }
|
336 |
|
337 | const driver = options.protocol === 'https:' ? 'https' : 'http';
|
338 | const request = require(driver).request(options, res => {
|
339 | if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
|
340 | httpRequest(res.headers.location, method, response);
|
341 | else
|
342 | response(res);
|
343 | });
|
344 | request.end();
|
345 | return request;
|
346 | }
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|