UNPKG

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