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 removeRecursive = require('rimraf');
|
24 |
|
25 | const ProxyAgent = require('https-proxy-agent');
|
26 |
|
27 | const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
|
28 |
|
29 | const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
|
30 | const 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 |
|
38 | const PROJECT_ROOT = fs.existsSync(path.join(__dirname, '..', 'package.json')) ? path.join(__dirname, '..') : path.join(__dirname, '..', '..');
|
39 |
|
40 | class Downloader {
|
41 | |
42 |
|
43 |
|
44 | constructor(downloadsFolder) {
|
45 | this._downloadsFolder = downloadsFolder;
|
46 | this._downloadHost = DEFAULT_DOWNLOAD_HOST;
|
47 | }
|
48 |
|
49 | |
50 |
|
51 |
|
52 | static defaultRevision() {
|
53 | return require(path.join(PROJECT_ROOT, 'package.json')).puppeteer.chromium_revision;
|
54 | }
|
55 |
|
56 | |
57 |
|
58 |
|
59 | static createDefault() {
|
60 | const downloadsFolder = path.join(PROJECT_ROOT, '.local-chromium');
|
61 | return new Downloader(downloadsFolder);
|
62 | }
|
63 |
|
64 | |
65 |
|
66 |
|
67 | setDownloadHost(downloadHost) {
|
68 | this._downloadHost = downloadHost.replace(/\/+$/, '');
|
69 | }
|
70 |
|
71 | |
72 |
|
73 |
|
74 | supportedPlatforms() {
|
75 | return Object.keys(downloadURLs);
|
76 | }
|
77 |
|
78 | |
79 |
|
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 |
|
94 |
|
95 |
|
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 |
|
116 |
|
117 |
|
118 |
|
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 |
|
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 |
|
153 |
|
154 |
|
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 |
|
165 |
|
166 |
|
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 |
|
190 |
|
191 |
|
192 |
|
193 | _getFolderPath(platform, revision) {
|
194 | return path.join(this._downloadsFolder, platform + '-' + revision);
|
195 | }
|
196 | }
|
197 |
|
198 | module.exports = Downloader;
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | function 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 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | function 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 |
|
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( (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 |
|
252 |
|
253 |
|
254 |
|
255 | function extractZip(zipPath, folderPath) {
|
256 | return new Promise(fulfill => extract(zipPath, {dir: folderPath}, fulfill));
|
257 | }
|
258 |
|
259 | function httpRequest(url, method, response) {
|
260 |
|
261 | const options = URL.parse(url);
|
262 | options.method = method;
|
263 |
|
264 | const proxyURL = getProxyForUrl(url);
|
265 | if (proxyURL) {
|
266 |
|
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 | }
|