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} = 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 | console.assert(this._platform, 'Unsupported platform: ' + os.platform());
|
67 | }
|
68 | const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
|
69 | console.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 | async download(revision, progressCallback) {
|
104 | let url = downloadURLs[this._platform];
|
105 | url = util.format(url, this._downloadHost, revision);
|
106 | const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
|
107 | const folderPath = this._getFolderPath(revision);
|
108 | if (await existsAsync(folderPath))
|
109 | return this.revisionInfo(revision);
|
110 | if (!(await existsAsync(this._downloadsFolder)))
|
111 | await mkdirAsync(this._downloadsFolder);
|
112 | try {
|
113 | await downloadFile(url, zipPath, progressCallback);
|
114 | await extractZip(zipPath, folderPath);
|
115 | } finally {
|
116 | if (await existsAsync(zipPath))
|
117 | await unlinkAsync(zipPath);
|
118 | }
|
119 | const revisionInfo = this.revisionInfo(revision);
|
120 | if (revisionInfo)
|
121 | await chmodAsync(revisionInfo.executablePath, 0o755);
|
122 | return revisionInfo;
|
123 | }
|
124 |
|
125 | |
126 |
|
127 |
|
128 | async localRevisions() {
|
129 | if (!await existsAsync(this._downloadsFolder))
|
130 | return [];
|
131 | const fileNames = await readdirAsync(this._downloadsFolder);
|
132 | return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
|
133 | }
|
134 |
|
135 | |
136 |
|
137 |
|
138 |
|
139 | async remove(revision) {
|
140 | const folderPath = this._getFolderPath(revision);
|
141 | console.assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
|
142 | await new Promise(fulfill => removeRecursive(folderPath, fulfill));
|
143 | }
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 | revisionInfo(revision) {
|
150 | const folderPath = this._getFolderPath(revision);
|
151 | let executablePath = '';
|
152 | if (this._platform === 'mac')
|
153 | executablePath = path.join(folderPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
|
154 | else if (this._platform === 'linux')
|
155 | executablePath = path.join(folderPath, 'chrome-linux', 'chrome');
|
156 | else if (this._platform === 'win32' || this._platform === 'win64')
|
157 | executablePath = path.join(folderPath, 'chrome-win32', 'chrome.exe');
|
158 | else
|
159 | throw 'Unsupported platform: ' + this._platform;
|
160 | let url = downloadURLs[this._platform];
|
161 | url = util.format(url, this._downloadHost, revision);
|
162 | const local = fs.existsSync(folderPath);
|
163 | return {revision, executablePath, folderPath, local, url};
|
164 | }
|
165 |
|
166 | |
167 |
|
168 |
|
169 |
|
170 | _getFolderPath(revision) {
|
171 | return path.join(this._downloadsFolder, this._platform + '-' + revision);
|
172 | }
|
173 | }
|
174 |
|
175 | module.exports = BrowserFetcher;
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 | function parseFolderPath(folderPath) {
|
182 | const name = path.basename(folderPath);
|
183 | const splits = name.split('-');
|
184 | if (splits.length !== 2)
|
185 | return null;
|
186 | const [platform, revision] = splits;
|
187 | if (!downloadURLs[platform])
|
188 | return null;
|
189 | return {platform, revision};
|
190 | }
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | function downloadFile(url, destinationPath, progressCallback) {
|
199 | let fulfill, reject;
|
200 | let downloadedBytes = 0;
|
201 | let totalBytes = 0;
|
202 |
|
203 | const promise = new Promise((x, y) => { fulfill = x; reject = y; });
|
204 |
|
205 | const request = httpRequest(url, 'GET', response => {
|
206 | if (response.statusCode !== 200) {
|
207 | const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
|
208 |
|
209 | response.resume();
|
210 | reject(error);
|
211 | return;
|
212 | }
|
213 | const file = fs.createWriteStream(destinationPath);
|
214 | file.on('finish', () => fulfill());
|
215 | file.on('error', error => reject(error));
|
216 | response.pipe(file);
|
217 | totalBytes = parseInt( (response.headers['content-length']), 10);
|
218 | if (progressCallback)
|
219 | response.on('data', onData);
|
220 | });
|
221 | request.on('error', error => reject(error));
|
222 | return promise;
|
223 |
|
224 | function onData(chunk) {
|
225 | downloadedBytes += chunk.length;
|
226 | progressCallback(downloadedBytes, totalBytes);
|
227 | }
|
228 | }
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 | function extractZip(zipPath, folderPath) {
|
236 | return new Promise(fulfill => extract(zipPath, {dir: folderPath}, fulfill));
|
237 | }
|
238 |
|
239 | function httpRequest(url, method, response) {
|
240 |
|
241 | const options = URL.parse(url);
|
242 | options.method = method;
|
243 |
|
244 | const proxyURL = getProxyForUrl(url);
|
245 | if (proxyURL) {
|
246 |
|
247 | const parsedProxyURL = URL.parse(proxyURL);
|
248 | parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
|
249 |
|
250 | options.agent = new ProxyAgent(parsedProxyURL);
|
251 | options.rejectUnauthorized = false;
|
252 | }
|
253 |
|
254 | const driver = options.protocol === 'https:' ? 'https' : 'http';
|
255 | const request = require(driver).request(options, res => {
|
256 | if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
|
257 | httpRequest(res.headers.location, method, response);
|
258 | else
|
259 | response(res);
|
260 | });
|
261 | request.end();
|
262 | return request;
|
263 | }
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|