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 |
|
32 | const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
|
33 | const 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 |
|
42 |
|
43 |
|
44 |
|
45 | function 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 |
|
52 | return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
|
53 | }
|
54 | return null;
|
55 | }
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | function downloadURL(platform, host, revision) {
|
64 | return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision));
|
65 | }
|
66 |
|
67 | const readdirAsync = helper.promisify(fs.readdir.bind(fs));
|
68 | const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
|
69 | const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
|
70 | const chmodAsync = helper.promisify(fs.chmod.bind(fs));
|
71 |
|
72 | function 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 |
|
79 | class BrowserFetcher {
|
80 | |
81 |
|
82 |
|
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 |
|
103 |
|
104 | platform() {
|
105 | return this._platform;
|
106 | }
|
107 |
|
108 | |
109 |
|
110 |
|
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 |
|
128 |
|
129 |
|
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 |
|
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 |
|
164 |
|
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 |
|
174 |
|
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 |
|
194 |
|
195 |
|
196 | _getFolderPath(revision) {
|
197 | return path.join(this._downloadsFolder, this._platform + '-' + revision);
|
198 | }
|
199 | }
|
200 |
|
201 | module.exports = BrowserFetcher;
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | function 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 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | function 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 |
|
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( (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 |
|
258 |
|
259 |
|
260 |
|
261 | function 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 |
|
270 | function httpRequest(url, method, response) {
|
271 |
|
272 | const options = URL.parse(url);
|
273 | options.method = method;
|
274 |
|
275 | const proxyURL = getProxyForUrl(url);
|
276 | if (proxyURL) {
|
277 |
|
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 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|