UNPKG

8.63 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 removeRecursive = require('rimraf');
24// @ts-ignore
25const ProxyAgent = require('https-proxy-agent');
26// @ts-ignore
27const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
28
29const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
30const downloadURLs = {
31 linux: '%s/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',
32 mac: '%s/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',
33 win32: '%s/chromium-browser-snapshots/Win/%d/chrome-win32.zip',
34 win64: '%s/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',
35};
36
37// Project root will be different for node6-transpiled code.
38const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
39
40class Downloader {
41 /**
42 * @param {string} downloadsFolder
43 */
44 constructor(downloadsFolder) {
45 this._downloadsFolder = downloadsFolder;
46 this._downloadHost = DEFAULT_DOWNLOAD_HOST;
47 }
48
49 /**
50 * @return {string}
51 */
52 static defaultRevision() {
53 return require(path.join(PROJECT_ROOT, 'package.json')).puppeteer.chromium_revision;
54 }
55
56 /**
57 * @return {!Downloader}
58 */
59 static createDefault() {
60 const downloadsFolder = path.join(PROJECT_ROOT, '.local-chromium');
61 return new Downloader(downloadsFolder);
62 }
63
64 /**
65 * @param {string} downloadHost
66 */
67 setDownloadHost(downloadHost) {
68 this._downloadHost = downloadHost.replace(/\/+$/, '');
69 }
70
71 /**
72 * @return {!Array<string>}
73 */
74 supportedPlatforms() {
75 return Object.keys(downloadURLs);
76 }
77
78 /**
79 * @return {string}
80 */
81 currentPlatform() {
82 const platform = os.platform();
83 if (platform === 'darwin')
84 return 'mac';
85 if (platform === 'linux')
86 return 'linux';
87 if (platform === 'win32')
88 return os.arch() === 'x64' ? 'win64' : 'win32';
89 return '';
90 }
91
92 /**
93 * @param {string} platform
94 * @param {string} revision
95 * @return {!Promise<boolean>}
96 */
97 canDownloadRevision(platform, revision) {
98 console.assert(downloadURLs[platform], 'Unknown platform: ' + platform);
99
100 const url = util.format(downloadURLs[platform], this._downloadHost, revision);
101
102 let resolve;
103 const promise = new Promise(x => resolve = x);
104 const request = httpRequest(url, 'HEAD', response => {
105 resolve(response.statusCode === 200);
106 });
107 request.on('error', error => {
108 console.error(error);
109 resolve(false);
110 });
111 return promise;
112 }
113
114 /**
115 * @param {string} platform
116 * @param {string} revision
117 * @param {?function(number, number)} progressCallback
118 * @return {!Promise}
119 */
120 downloadRevision(platform, revision, progressCallback) {
121 let url = downloadURLs[platform];
122 console.assert(url, `Unsupported platform: ${platform}`);
123 url = util.format(url, this._downloadHost, revision);
124 const zipPath = path.join(this._downloadsFolder, `download-${platform}-${revision}.zip`);
125 const folderPath = this._getFolderPath(platform, revision);
126 if (fs.existsSync(folderPath))
127 return;
128 if (!fs.existsSync(this._downloadsFolder))
129 fs.mkdirSync(this._downloadsFolder);
130 return downloadFile(url, zipPath, progressCallback)
131 .then(() => extractZip(zipPath, folderPath))
132 .catch(err => err)
133 .then(err => {
134 if (fs.existsSync(zipPath))
135 fs.unlinkSync(zipPath);
136 if (err)
137 throw err;
138 });
139 }
140
141 /**
142 * @return {!Array<!{platform:string, revision: string}>}
143 */
144 downloadedRevisions() {
145 if (!fs.existsSync(this._downloadsFolder))
146 return [];
147 const fileNames = fs.readdirSync(this._downloadsFolder);
148 return fileNames.map(fileName => parseFolderPath(fileName)).filter(revision => !!revision);
149 }
150
151 /**
152 * @param {string} platform
153 * @param {string} revision
154 * @return {!Promise}
155 */
156 removeRevision(platform, revision) {
157 console.assert(downloadURLs[platform], `Unsupported platform: ${platform}`);
158 const folderPath = this._getFolderPath(platform, revision);
159 console.assert(fs.existsSync(folderPath));
160 return new Promise(fulfill => removeRecursive(folderPath, fulfill));
161 }
162
163 /**
164 * @param {string} platform
165 * @param {string} revision
166 * @return {!{revision: string, folderPath: string, executablePath: string, downloaded: boolean}}
167 */
168 revisionInfo(platform, revision) {
169 console.assert(downloadURLs[platform], `Unsupported platform: ${platform}`);
170 const folderPath = this._getFolderPath(platform, revision);
171 let executablePath = '';
172 if (platform === 'mac')
173 executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
174 else if (platform === 'linux')
175 executablePath = path.join(folderPath, 'chrome-linux', 'chrome');
176 else if (platform === 'win32' || platform === 'win64')
177 executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
178 else
179 throw 'Unsupported platform: ' + platform;
180 return {
181 revision,
182 executablePath,
183 folderPath,
184 downloaded: fs.existsSync(folderPath)
185 };
186 }
187
188 /**
189 * @param {string} platform
190 * @param {string} revision
191 * @return {string}
192 */
193 _getFolderPath(platform, revision) {
194 return path.join(this._downloadsFolder, platform + '-' + revision);
195 }
196}
197
198module.exports = Downloader;
199
200/**
201 * @param {string} folderPath
202 * @return {?{platform: string, revision: string}}
203 */
204function parseFolderPath(folderPath) {
205 const name = path.basename(folderPath);
206 const splits = name.split('-');
207 if (splits.length !== 2)
208 return null;
209 const [platform, revision] = splits;
210 if (!downloadURLs[platform])
211 return null;
212 return {platform, revision};
213}
214
215/**
216 * @param {string} url
217 * @param {string} destinationPath
218 * @param {?function(number, number)} progressCallback
219 * @return {!Promise}
220 */
221function downloadFile(url, destinationPath, progressCallback) {
222 let fulfill, reject;
223
224 const promise = new Promise((x, y) => { fulfill = x; reject = y; });
225
226 const request = httpRequest(url, 'GET', response => {
227 if (response.statusCode !== 200) {
228 const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
229 // consume response data to free up memory
230 response.resume();
231 reject(error);
232 return;
233 }
234 const file = fs.createWriteStream(destinationPath);
235 file.on('finish', () => fulfill());
236 file.on('error', error => reject(error));
237 response.pipe(file);
238 const totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
239 if (progressCallback)
240 response.on('data', onData.bind(null, totalBytes));
241 });
242 request.on('error', error => reject(error));
243 return promise;
244
245 function onData(totalBytes, chunk) {
246 progressCallback(totalBytes, chunk.length);
247 }
248}
249
250/**
251 * @param {string} zipPath
252 * @param {string} folderPath
253 * @return {!Promise<?Error>}
254 */
255function extractZip(zipPath, folderPath) {
256 return new Promise(fulfill => extract(zipPath, {dir: folderPath}, fulfill));
257}
258
259function httpRequest(url, method, response) {
260 /** @type {Object} */
261 const options = URL.parse(url);
262 options.method = method;
263
264 const proxyURL = getProxyForUrl(url);
265 if (proxyURL) {
266 /** @type {Object} */
267 const parsedProxyURL = URL.parse(proxyURL);
268 parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
269
270 options.agent = new ProxyAgent(parsedProxyURL);
271 options.rejectUnauthorized = false;
272 }
273
274 const driver = options.protocol === 'https:' ? 'https' : 'http';
275 const request = require(driver).request(options, res => {
276 if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
277 httpRequest(res.headers.location, method, response);
278 else
279 response(res);
280 });
281 request.end();
282 return request;
283}