123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- /**
- * Copyright 2017 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- const os = require('os');
- const fs = require('fs');
- const path = require('path');
- const util = require('util');
- const extract = require('extract-zip');
- const URL = require('url');
- const {helper, assert} = require('./helper');
- const removeRecursive = require('rimraf');
- // @ts-ignore
- const ProxyAgent = require('https-proxy-agent');
- // @ts-ignore
- const getProxyForUrl = require('proxy-from-env').getProxyForUrl;
- const DEFAULT_DOWNLOAD_HOST = 'https://storage.googleapis.com';
- const supportedPlatforms = ['mac', 'linux', 'win32', 'win64'];
- const downloadURLs = {
- linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
- mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip',
- win32: '%s/chromium-browser-snapshots/Win/%d/%s.zip',
- win64: '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip',
- };
- /**
- * @param {string} platform
- * @param {string} revision
- * @return {string}
- */
- function archiveName(platform, revision) {
- if (platform === 'linux')
- return 'chrome-linux';
- if (platform === 'mac')
- return 'chrome-mac';
- if (platform === 'win32' || platform === 'win64') {
- // Windows archive name changed at r591479.
- return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
- }
- return null;
- }
- /**
- * @param {string} platform
- * @param {string} host
- * @param {string} revision
- * @return {string}
- */
- function downloadURL(platform, host, revision) {
- return util.format(downloadURLs[platform], host, revision, archiveName(platform, revision));
- }
- const readdirAsync = helper.promisify(fs.readdir.bind(fs));
- const mkdirAsync = helper.promisify(fs.mkdir.bind(fs));
- const unlinkAsync = helper.promisify(fs.unlink.bind(fs));
- const chmodAsync = helper.promisify(fs.chmod.bind(fs));
- function existsAsync(filePath) {
- let fulfill = null;
- const promise = new Promise(x => fulfill = x);
- fs.access(filePath, err => fulfill(!err));
- return promise;
- }
- class BrowserFetcher {
- /**
- * @param {string} projectRoot
- * @param {!BrowserFetcher.Options=} options
- */
- constructor(projectRoot, options = {}) {
- this._downloadsFolder = options.path || path.join(projectRoot, '.local-chromium');
- this._downloadHost = options.host || DEFAULT_DOWNLOAD_HOST;
- this._platform = options.platform || '';
- if (!this._platform) {
- const platform = os.platform();
- if (platform === 'darwin')
- this._platform = 'mac';
- else if (platform === 'linux')
- this._platform = 'linux';
- else if (platform === 'win32')
- this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
- assert(this._platform, 'Unsupported platform: ' + os.platform());
- }
- assert(supportedPlatforms.includes(this._platform), 'Unsupported platform: ' + this._platform);
- }
- /**
- * @return {string}
- */
- platform() {
- return this._platform;
- }
- /**
- * @param {string} revision
- * @return {!Promise<boolean>}
- */
- canDownload(revision) {
- const url = downloadURL(this._platform, this._downloadHost, revision);
- let resolve;
- const promise = new Promise(x => resolve = x);
- const request = httpRequest(url, 'HEAD', response => {
- resolve(response.statusCode === 200);
- });
- request.on('error', error => {
- console.error(error);
- resolve(false);
- });
- return promise;
- }
- /**
- * @param {string} revision
- * @param {?function(number, number):void} progressCallback
- * @return {!Promise<!BrowserFetcher.RevisionInfo>}
- */
- async download(revision, progressCallback) {
- const url = downloadURL(this._platform, this._downloadHost, revision);
- const zipPath = path.join(this._downloadsFolder, `download-${this._platform}-${revision}.zip`);
- const folderPath = this._getFolderPath(revision);
- if (await existsAsync(folderPath))
- return this.revisionInfo(revision);
- if (!(await existsAsync(this._downloadsFolder)))
- await mkdirAsync(this._downloadsFolder);
- try {
- await downloadFile(url, zipPath, progressCallback);
- await extractZip(zipPath, folderPath);
- } finally {
- if (await existsAsync(zipPath))
- await unlinkAsync(zipPath);
- }
- const revisionInfo = this.revisionInfo(revision);
- if (revisionInfo)
- await chmodAsync(revisionInfo.executablePath, 0o755);
- return revisionInfo;
- }
- /**
- * @return {!Promise<!Array<string>>}
- */
- async localRevisions() {
- if (!await existsAsync(this._downloadsFolder))
- return [];
- const fileNames = await readdirAsync(this._downloadsFolder);
- return fileNames.map(fileName => parseFolderPath(fileName)).filter(entry => entry && entry.platform === this._platform).map(entry => entry.revision);
- }
- /**
- * @param {string} revision
- */
- async remove(revision) {
- const folderPath = this._getFolderPath(revision);
- assert(await existsAsync(folderPath), `Failed to remove: revision ${revision} is not downloaded`);
- await new Promise(fulfill => removeRecursive(folderPath, fulfill));
- }
- /**
- * @param {string} revision
- * @return {!BrowserFetcher.RevisionInfo}
- */
- revisionInfo(revision) {
- const folderPath = this._getFolderPath(revision);
- let executablePath = '';
- if (this._platform === 'mac')
- executablePath = path.join(folderPath, archiveName(this._platform, revision), 'Chromium.app', 'Contents', 'MacOS', 'Chromium');
- else if (this._platform === 'linux')
- executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome');
- else if (this._platform === 'win32' || this._platform === 'win64')
- executablePath = path.join(folderPath, archiveName(this._platform, revision), 'chrome.exe');
- else
- throw new Error('Unsupported platform: ' + this._platform);
- const url = downloadURL(this._platform, this._downloadHost, revision);
- const local = fs.existsSync(folderPath);
- return {revision, executablePath, folderPath, local, url};
- }
- /**
- * @param {string} revision
- * @return {string}
- */
- _getFolderPath(revision) {
- return path.join(this._downloadsFolder, this._platform + '-' + revision);
- }
- }
- module.exports = BrowserFetcher;
- /**
- * @param {string} folderPath
- * @return {?{platform: string, revision: string}}
- */
- function parseFolderPath(folderPath) {
- const name = path.basename(folderPath);
- const splits = name.split('-');
- if (splits.length !== 2)
- return null;
- const [platform, revision] = splits;
- if (!supportedPlatforms.includes(platform))
- return null;
- return {platform, revision};
- }
- /**
- * @param {string} url
- * @param {string} destinationPath
- * @param {?function(number, number):void} progressCallback
- * @return {!Promise}
- */
- function downloadFile(url, destinationPath, progressCallback) {
- let fulfill, reject;
- let downloadedBytes = 0;
- let totalBytes = 0;
- const promise = new Promise((x, y) => { fulfill = x; reject = y; });
- const request = httpRequest(url, 'GET', response => {
- if (response.statusCode !== 200) {
- const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
- // consume response data to free up memory
- response.resume();
- reject(error);
- return;
- }
- const file = fs.createWriteStream(destinationPath);
- file.on('finish', () => fulfill());
- file.on('error', error => reject(error));
- response.pipe(file);
- totalBytes = parseInt(/** @type {string} */ (response.headers['content-length']), 10);
- if (progressCallback)
- response.on('data', onData);
- });
- request.on('error', error => reject(error));
- return promise;
- function onData(chunk) {
- downloadedBytes += chunk.length;
- progressCallback(downloadedBytes, totalBytes);
- }
- }
- /**
- * @param {string} zipPath
- * @param {string} folderPath
- * @return {!Promise<?Error>}
- */
- function extractZip(zipPath, folderPath) {
- return new Promise((fulfill, reject) => extract(zipPath, {dir: folderPath}, err => {
- if (err)
- reject(err);
- else
- fulfill();
- }));
- }
- function httpRequest(url, method, response) {
- /** @type {Object} */
- let options = URL.parse(url);
- options.method = method;
- const proxyURL = getProxyForUrl(url);
- if (proxyURL) {
- if (url.startsWith('http:')) {
- const proxy = URL.parse(proxyURL);
- options = {
- path: options.href,
- host: proxy.hostname,
- port: proxy.port,
- };
- } else {
- /** @type {Object} */
- const parsedProxyURL = URL.parse(proxyURL);
- parsedProxyURL.secureProxy = parsedProxyURL.protocol === 'https:';
- options.agent = new ProxyAgent(parsedProxyURL);
- options.rejectUnauthorized = false;
- }
- }
- const requestCallback = res => {
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location)
- httpRequest(res.headers.location, method, response);
- else
- response(res);
- };
- const request = options.protocol === 'https:' ?
- require('https').request(options, requestCallback) :
- require('http').request(options, requestCallback);
- request.end();
- return request;
- }
- /**
- * @typedef {Object} BrowserFetcher.Options
- * @property {string=} platform
- * @property {string=} path
- * @property {string=} host
- */
- /**
- * @typedef {Object} BrowserFetcher.RevisionInfo
- * @property {string} folderPath
- * @property {string} executablePath
- * @property {string} url
- * @property {boolean} local
- * @property {string} revision
- */
|