npminstall
Version:
Make npm install fast and handy.
860 lines (806 loc) • 27.6 kB
JavaScript
const debug = require('node:util').debuglog('npminstall:download:npm');
const { randomUUID } = require('node:crypto');
const { createWriteStream, createReadStream, rmSync } = require('node:fs');
const fs = require('node:fs/promises');
const path = require('node:path');
const crypto = require('node:crypto');
const { resolve: urlresolve } = require('node:url');
const zlib = require('node:zlib');
const os = require('node:os');
const bytes = require('bytes');
const tar = require('tar');
const destroy = require('destroy');
const chalk = require('chalk');
const moment = require('moment');
const semver = require('semver');
const { family: getLibcFamily } = require('detect-libc');
const get = require('../get');
const utils = require('../utils');
const config = require('../cnpm_config');
module.exports = async (pkg, options) => {
const realPkg = await resolve(pkg.subSpec || pkg, options);
// download tarball and unzip
const info = await download(realPkg, options);
info.package = realPkg;
return info;
};
module.exports.resolve = resolve;
async function resolve(pkg, options) {
const dependenciesTree = options.cache.dependenciesTree;
// check cache first
if (dependenciesTree[pkg.raw]) {
debug('resolve hit dependencies cache: %s', pkg.raw);
return dependenciesTree[pkg.raw];
}
const packageMetaKey = `npm:resolve:package:${pkg.name}`;
let packageMeta = options.cache[packageMetaKey];
if (!packageMeta) {
// add a lock to make sure fetch full meta once
packageMeta = { done: false };
options.cache[packageMetaKey] = packageMeta;
let err;
try {
const fullMeta = await getFullPackageMeta(pkg.name, options);
Object.assign(packageMeta, fullMeta);
packageMeta.done = true;
packageMeta.allVersions = Object.keys(packageMeta.versions);
} catch (e) {
err = e;
// clean cache
options.cache[packageMetaKey] = null;
}
if (err) {
options.events.emit(packageMetaKey, err);
throw err;
} else {
options.events.emit(packageMetaKey);
}
} else if (!packageMeta.done) {
debug('await %s', packageMetaKey);
const err = await options.events.await(packageMetaKey);
if (err) throw err;
}
let spec = pkg.fetchSpec;
if (spec === '*') {
spec = 'latest';
}
const distTags = packageMeta['dist-tags'];
let realPkgVersion = utils.findMaxSatisfyingVersion(spec, distTags, packageMeta.allVersions);
let fixDependencies;
let fixScripts;
if (!realPkgVersion) {
// remove disk cache
await removeCacheInfo(pkg.name, options);
throw new Error(`[${pkg.displayName}] Can\'t find package ${pkg.name}@${pkg.rawSpec}`);
}
if (options.autoFixVersion) {
const fixVersion = options.autoFixVersion(pkg.name, realPkgVersion);
if (fixVersion) {
if (fixVersion.version && fixVersion.version !== realPkgVersion) {
options.console.warn(`[${pkg.name}@${realPkgVersion}] use ${pkg.name}@${chalk.green(fixVersion.version)} instead, reason: ${chalk.yellow(fixVersion.reason)}`);
realPkgVersion = fixVersion.version;
}
if (fixVersion.dependencies) {
options.console.warn(`[${pkg.name}@${realPkgVersion}] use dependencies: ${chalk.green(JSON.stringify(fixVersion.dependencies))} instead, reason: ${chalk.yellow(fixVersion.reason)}`);
fixDependencies = fixVersion.dependencies;
}
// https://github.com/npm/rfcs/pull/488/files
// merge custom scripts
// {
// "foo": {
// "1.0.0": {
// "scripts": { "postinstall": "" },
// "reason": "some description message"
// }
// }
// }
if (fixVersion.scripts) {
options.console.warn(`[${pkg.name}@${realPkgVersion}] use scripts: ${chalk.green(JSON.stringify(fixVersion.scripts))} instead, reason: ${chalk.yellow(fixVersion.reason)}`);
fixScripts = fixVersion.scripts;
}
}
}
const realPkg = packageMeta.versions[realPkgVersion];
if (!realPkg) {
// remove disk cache
await removeCacheInfo(pkg.name, options);
throw new Error(`[${pkg.displayName}] Can\'t find package ${pkg.name}\'s version: ${realPkgVersion}`);
}
if (fixDependencies) {
realPkg.__fixDependencies = fixDependencies;
}
if (fixScripts) {
realPkg.__fixScripts = fixScripts;
}
debug('[%s@%s] spec: %s, real version: %s, dist-tags: %j',
pkg.name, pkg.rawSpec, pkg.fetchSpec, realPkg.version, distTags);
// cache resolve result
dependenciesTree[pkg.raw] = realPkg;
return realPkg;
}
function _getScope(name) {
if (name[0] === '@') return name.slice(0, name.indexOf('/'));
}
async function _getCacheInfo(fullname, globalOptions) {
// check name has scope
let registry = globalOptions.registry;
const scope = _getScope(fullname);
if (scope) {
registry = config.get(scope + ':registry') || globalOptions.registry;
}
const info = {
pkgUrl: utils.formatPackageUrl(registry, fullname),
cacheFile: '',
cache: null,
};
if (!globalOptions.cacheDir) {
return info;
}
const hash = crypto.createHash('md5').update(info.pkgUrl).digest('hex');
const parentDir = path.join(globalOptions.cacheDir, 'manifests', hash[0], hash[1], hash[2]);
// { etag, age, headers, manifests }
info.cacheFile = path.join(parentDir, `${hash}.json`);
const exists = await utils.exists(info.cacheFile);
// cache not exists
if (!exists) {
await utils.mkdirp(parentDir);
return info;
}
// cache exists
const cacheContent = await fs.readFile(info.cacheFile);
try {
info.cache = JSON.parse(cacheContent);
} catch (_) {
globalOptions.console.warn('[npminstall:download:npm] Ignore invalid cache file %s', info.cacheFile);
}
return info;
}
async function removeCacheInfo(fullname, globalOptions) {
const info = await _getCacheInfo(fullname, globalOptions);
if (info.cache) {
await fs.rm(info.cacheFile, { force: true });
}
}
async function getFullPackageMeta(fullname, globalOptions) {
const info = await _getCacheInfo(fullname, globalOptions);
if (globalOptions.offline && !info.cache) {
throw new Error(`Can\'t find package ${fullname} manifests on offline mode`);
}
if (!info.cacheFile) {
const result = await _fetchFullPackageMeta(info.pkgUrl, globalOptions);
return result.data;
}
// cache file not exists
if (!info.cache) {
return await _fetchFullPackageMetaWithCache(info.pkgUrl, globalOptions, info.cacheFile);
}
// check is expired or not
// offline should force to use cache manifests
if (globalOptions.offline || info.cache.expired > Date.now()) {
globalOptions.totalCacheJSONCount += 1;
return info.cache.manifests;
}
// use etag to request
return await _fetchFullPackageMetaWithCache(info.pkgUrl, globalOptions, info.cacheFile, info.cache);
}
async function _fetchFullPackageMetaWithCache(pkgUrl, globalOptions, cacheFile, cache) {
const etag = cache && cache.etag;
let result;
try {
result = await _fetchFullPackageMeta(pkgUrl, globalOptions, etag, !!cache);
} catch (err) {
if (cache) {
globalOptions.console.warn('[npminstall:download:npm] Request %s error, use cache instead', pkgUrl);
return cache.manifests;
}
throw err;
}
// < etag: "14A082C35C551D6A94A29E064E32BDA5"
// < cache-control: public, max-age=300
const headers = result.headers;
const cacheControl = headers['cache-control'];
let maxAge = 120;
if (cacheControl) {
const cacheControlMatch = /max-age=(\d{1,100})/.exec(cacheControl);
if (cacheControlMatch) {
maxAge = parseInt(cacheControlMatch[1]);
// >= 1min & <= 30mins
if (maxAge > 1800) {
maxAge = 1800;
} else if (maxAge < 60) {
maxAge = 60;
}
}
}
debug('GET %s with etag: %j, status: %s, maxAge: %s',
pkgUrl, etag, result.status, maxAge);
const expired = Date.now() + maxAge * 1000;
// etag match
if (result.status === 304) {
cache.expired = expired;
cache.headers = headers;
await fs.writeFile(cacheFile, JSON.stringify(cache));
globalOptions.totalEtagHitCount += 1;
globalOptions.totalCacheJSONCount += 1;
return cache.manifests;
}
// 200 status
await fs.writeFile(cacheFile, JSON.stringify({
etag: headers.etag,
expired,
headers,
manifests: result.data,
}));
if (etag) {
globalOptions.totalEtagMissCount += 1;
}
return result.data;
}
async function _fetchFullPackageMeta(pkgUrl, globalOptions, etag, hasCache = false) {
const headers = {
accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
};
if (etag) {
headers['if-none-match'] = etag;
}
const result = await get(pkgUrl, {
headers,
timeout: globalOptions.timeout,
followRedirect: true,
gzip: true,
dataType: 'json',
}, globalOptions, hasCache);
if (result.status === 200) {
globalOptions.totalJSONSize += result.res.size;
globalOptions.totalJSONCount += 1;
}
return result;
}
async function download(pkg, options) {
// don't download pkg in not matched os
if (!utils.matchPlatform(process.platform, pkg.os)) {
const errMsg = `[${pkg.name}@${pkg.version}] skip download for reason ${pkg.os.join(', ')} dont includes your platform ${process.platform}`;
const err = new Error(errMsg);
err.name = 'UnSupportedPlatformError';
throw err;
}
// don't download pkg in not matched cpu
if (!utils.matchPlatform(process.arch, pkg.cpu)) {
const errMsg = `[${pkg.name}@${pkg.version}] skip download for reason ${pkg.cpu.join(', ')} dont includes your arch ${process.arch}`;
const err = new Error(errMsg);
err.name = 'UnSupportedPlatformError';
throw err;
}
// don't download pkg in not matched libc
if (Array.isArray(pkg.libc)) {
const currentLibc = await getLibcFamily();
if (currentLibc && !utils.matchPlatform(currentLibc, pkg.libc)) {
const errMsg = `[${pkg.name}@${pkg.version}] skip download for reason ${pkg.libc.join(', ')} dont includes your libc ${currentLibc}`;
const err = new Error(errMsg);
err.name = 'UnSupportedPlatformError';
throw err;
}
}
const ungzipDir = options.ungzipDir || utils.getPackageStorePath(options.storeDir, pkg, options);
// make sure only one download for a version
const key = `download:${pkg.name}@${pkg.version}`;
if (options.cache[key]) {
// wait download finish
if (!options.cache[key].done) {
const err = await options.events.await(key);
if (err) {
throw err;
}
}
return {
exists: true,
dir: ungzipDir,
};
}
options.cache[key] = {
done: false,
};
if (await utils.isInstallDone(ungzipDir)) {
options.cache[key].done = true;
options.events.emit(key);
// debug('[%s@%s] Exists', pkg.name, pkg.version);
return {
exists: true,
dir: ungzipDir,
};
}
await utils.mkdirp(ungzipDir);
// download tar and unzip
let lastErr;
let count = 0;
const tarballUrls = utils.parseTarballUrls(pkg.dist.tarball);
let tarballUrlIndex = 0;
let tarballUrl;
while (count < 3) {
tarballUrl = tarballUrls[tarballUrlIndex++];
if (!tarballUrl) {
tarballUrlIndex = 0;
tarballUrl = tarballUrls[tarballUrlIndex];
}
try {
const stream = await getTarballStream(tarballUrl, pkg, options);
let useTarFormat = false;
if (count === 1 && lastErr && lastErr.code === 'Z_DATA_ERROR') {
options.console.warn(`[${pkg.name}@${pkg.version}] format ungzip error, try to use tar format`);
useTarFormat = true;
}
await checkShasumAndUngzip(ungzipDir, stream, pkg, useTarFormat, options);
lastErr = null;
break;
} catch (err) {
lastErr = err;
count++;
options.console.warn(`[${pkg.name}@${pkg.version}] download %s: %s, fail count: %s`,
err.name, err.message, count);
// retry download on any error
// sleep for a while to wait for server become normal
if (count < 3) {
await utils.sleep(count * 500);
}
}
}
if (lastErr) {
options.events.emit(key, lastErr);
throw lastErr;
}
// read package.json to merge into realPkg
const fullMeta = await utils.readPackageJSON(ungzipDir);
Object.assign(pkg, fullMeta);
if (pkg.__fixDependencies) {
pkg.dependencies = Object.assign({}, pkg.dependencies, pkg.__fixDependencies);
}
if (pkg.__fixScripts) {
pkg.scripts = Object.assign({}, pkg.scripts, pkg.__fixScripts);
}
await utils.setInstallDone(ungzipDir);
const pkgMeta = {
_from: `${pkg.name}@${pkg.version}`,
_resolved: pkg.dist.tarball,
};
const binaryMirror = options.binaryMirrors[pkg.name];
if (binaryMirror) {
// node-pre-gyp
if (pkg.scripts && pkg.scripts.install && !binaryMirror.replaceHostFiles) {
// leveldown and sqlite3
// nodegit
if (/prebuild --install/.test(pkg.scripts.install) ||
/prebuild --download/.test(pkg.scripts.install) ||
/node-pre-gyp install/.test(pkg.scripts.install) ||
// utf-8-validate
/prebuild-install || node-gyp rebuild/.test(pkg.scripts.install) ||
pkg.name === 'nodegit' ||
pkg.name === 'fsevents') {
const newBinary = pkg.binary || {};
for (const key in binaryMirror) {
newBinary[key] = binaryMirror[key];
}
pkgMeta.binary = newBinary;
// ignore https protocol check on: node_modules/node-pre-gyp/lib/util/versioning.js
if (/node-pre-gyp install/.test(pkg.scripts.install)) {
const versioningFile = path.join(ungzipDir, 'node_modules/node-pre-gyp/lib/util/versioning.js');
if (await utils.exists(versioningFile)) {
let content = await fs.readFile(versioningFile, 'utf-8');
content = content.replace('if (protocol === \'http:\') {',
'if (false && protocol === \'http:\') { // hack by npminstall');
await fs.writeFile(versioningFile, content);
}
}
options.console.info('%s download from binary mirror: %j',
chalk.gray(`${pkg.name}@${pkg.version}`), newBinary);
}
} else if ((binaryMirror.replaceHost && binaryMirror.host) || binaryMirror.replaceHostMap || binaryMirror.replaceHostRegExpMap) {
// use mirror url instead
// e.g.: pngquant-bin
// https://github.com/lovell/sharp/blob/master/install/libvips.js#L19
const replaceHostFiles = binaryMirror.replaceHostFiles || [
'lib/index.js',
'lib/install.js',
];
for (const replaceHostFile of replaceHostFiles) {
const replaceHostFilePath = path.join(ungzipDir, replaceHostFile);
await replaceHostInFile(pkg, replaceHostFilePath, binaryMirror, options);
}
}
// replace cypress download url
// https://github.com/cypress-io/cypress/blob/master/cli/lib/tasks/download.js#L30
if (pkg.name === 'cypress') {
const defaultPlatforms = {
darwin: 'osx64',
linux: 'linux64',
win32: 'win64',
};
let platforms = binaryMirror.platforms || defaultPlatforms;
// version >= 3.3.0 should use binaryMirror.newPlatforms by default, other use defaultPlatforms
if (binaryMirror.newPlatforms && semver.gte(pkg.version, '3.3.0')) {
platforms = binaryMirror.newPlatforms;
}
const targetPlatform = platforms[os.platform()];
if (targetPlatform) {
options.console.info('%s download from binary mirror: %j, targetPlatform: %s',
chalk.gray(`${pkg.name}@${pkg.version}`), binaryMirror, targetPlatform);
const downloadFile = path.join(ungzipDir, 'lib/tasks/download.js');
if (await utils.exists(downloadFile)) {
let content = await fs.readFile(downloadFile, 'utf-8');
// return version ? prepend('desktop/' + version) : prepend('desktop');
const afterContent = 'return "' + binaryMirror.host + '/" + version + "/' + targetPlatform + '/cypress.zip"; // hack by npminstall\n';
content = content
.replace('return version ? prepend(\`desktop/${version}\`) : prepend(\'desktop\')', afterContent)
.replace('return version ? prepend(\'desktop/\' + version) : prepend(\'desktop\');', afterContent);
await fs.writeFile(downloadFile, content);
}
}
} else if (pkg.name === 'vscode') {
// https://github.com/Microsoft/vscode-extension-vscode/blob/master/bin/install#L64
const indexFilepath = path.join(ungzipDir, 'bin/install');
await replaceHostInFile(pkg, indexFilepath, binaryMirror, options);
}
}
if (pkg.name === 'node-pre-gyp') {
// ignore https protocol check on: lib/util/versioning.js
const versioningFile = path.join(ungzipDir, 'lib/util/versioning.js');
if (await utils.exists(versioningFile)) {
let content = await fs.readFile(versioningFile, 'utf-8');
content = content.replace('if (protocol === \'http:\') {',
'if (false && protocol === \'http:\') { // hack by npminstall');
await fs.writeFile(versioningFile, content);
}
}
await utils.addMetaToJSONFile(path.join(ungzipDir, 'package.json'), pkgMeta);
options.cache[key].done = true;
options.events.emit(key);
options.registryPackages++;
return {
exists: false,
dir: ungzipDir,
};
}
async function getTarballStream(tarballUrl, pkg, options) {
let formatRedirectUrl;
if (options.formatNpmTarballUrl) {
tarballUrl = options.formatNpmTarballUrl(tarballUrl);
formatRedirectUrl = (from, to) => {
return options.formatNpmTarballUrl(urlresolve(from, to));
};
}
if (!options.cacheDir || utils.isSudo()) {
// sudo don't touch the cacheDir
// production mode
debug('[%s@%s] GET streaming %j', pkg.name, pkg.version, tarballUrl);
const result = await get(tarballUrl, {
timeout: options.streamingTimeout || options.timeout,
followRedirect: true,
formatRedirectUrl,
streaming: true,
}, options);
if (result.status !== 200) {
try {
destroy(result.res);
} catch (err) {
options.console.warn('[npminstall:download:npm] ignore destroy response stream error: %s', err);
}
throw new Error(`Download ${tarballUrl} status: ${result.status} error, should be 200`);
}
// record size
result.res.on('data', chunk => {
options.totalTarballSize += chunk.length;
});
result.res.tarballUrl = tarballUrl;
return result.res;
}
// multi process problems
let paths = [ options.cacheDir ];
let name = pkg.name;
if (name[0] === '@') {
name = name.split('/')[1];
}
paths = paths.concat(name.toLowerCase().replace(/[-_.]/g, '').split('', 4));
paths.push(pkg.name);
const parentDir = path.join(...paths);
const tarballFile = path.join(parentDir, `${pkg.version}-${pkg.dist.shasum}.tgz`);
let exists = await utils.exists(tarballFile);
if (!exists) {
// try to remove expired tmp dirs
const dirs = [];
for (let i = 1; i <= 3; i++) {
// year
dirs.push(path.join(options.cacheDir, '.tmp', moment().subtract(i, 'years').format('YYYY')));
// month
dirs.push(path.join(options.cacheDir, '.tmp', moment().subtract(i, 'months').format('YYYY/MM')));
}
for (const dir of dirs) {
try {
await utils.rimraf(dir);
} catch (err) {
options.console.warn('Remove tmp dir %s error: %s, ignore it', dir, err);
}
}
const tmpDir = path.join(options.cacheDir, '.tmp', moment().format('YYYY/MM/DD'));
await utils.mkdirp(parentDir);
await utils.mkdirp(tmpDir);
const tmpFile = path.join(tmpDir, `${name}-${pkg.version}-${randomUUID()}.tgz`);
const result = await get(tarballUrl, {
timeout: options.streamingTimeout || options.timeout,
followRedirect: true,
formatRedirectUrl,
writeStream: createWriteStream(tmpFile),
}, options);
if (result.status !== 200) {
throw new Error(`Download ${tarballUrl} status: ${result.status} error, should be 200`);
}
// make sure tarball file is not exists again
exists = await utils.exists(tarballFile);
if (!exists) {
try {
await fs.rename(tmpFile, tarballFile);
} catch (err) {
if (err.code === 'EPERM') {
// Error: EPERM: operation not permitted, rename
exists = await utils.exists(tarballFile);
if (exists) {
// parallel execution case same file exists, ignore rename error
// clean tmpFile
await fs.rm(tmpFile, { force: true });
} else {
// rename error
throw err;
}
} else {
// rename error
throw err;
}
}
} else {
// clean tmpFile
await fs.rm(tmpFile, { force: true });
}
const stat = await fs.stat(tarballFile);
debug('[%s@%s] saved %s %s => %s',
pkg.name, pkg.version, bytes(stat.size), tarballUrl, tarballFile);
options.totalTarballSize += stat.size;
}
const stream = createReadStream(tarballFile);
stream.tarballFile = tarballFile;
stream.tarballUrl = tarballUrl;
return stream;
}
function toMap(arr) {
const m = {};
for (const a of arr) {
m[a] = true;
}
return m;
}
// https://github.com/tj/node-prune/blob/master/prune.go#L17
const defaultFiles = toMap([
'Makefile',
'Gruntfile.js',
'.DS_Store',
'.tern-project',
'.gitattributes',
'.editorconfig',
'.eslintrc',
'eslint',
'.eslintrc.js',
'.eslintrc.json',
'.eslintrc.yml',
'.eslintignore',
'.stylelintrc',
'stylelint.config.js',
'.stylelintrc.json',
'.stylelintrc.yaml',
'.stylelintrc.yml',
'.stylelintrc.js',
'.htmllintrc',
'htmllint.js',
'.lint',
'.npmignore',
'.jshintrc',
'.flowconfig',
'.documentup.json',
'.yarn-metadata.json',
'.travis.yml',
'appveyor.yml',
'.gitlab-ci.yml',
'circle.yml',
'.coveralls.yml',
'CHANGES',
'changelog',
'ChangeLog',
'TODO',
'LICENSE.txt',
'LICENSE',
'LICENSE-MIT',
'LICENSE.BSD',
'license',
'License',
'PATENTS',
'AUTHORS',
'CONTRIBUTORS',
'.mailmap',
'.yarn-integrity',
'.yarnclean',
'_config.yml',
'.babelrc',
'.yo-rc.json',
'jest.config.js',
'karma.conf.js',
'.appveyor.yml',
'tsconfig.json',
'.zuul.yml',
'.jscs.json',
'.jscsrc',
'.autod.conf.js',
'.jshintignore',
'.nvmrc',
]);
const defaultDirectories = toMap([
'__tests__',
'test',
'tests',
'powered-test',
'docs',
'doc',
'.idea',
'.vscode',
'website',
'images',
'example',
'examples',
'coverage',
'.nyc_output',
'.circleci',
'.github',
]);
const defaultExtensions = toMap([
'.markdown',
'.md',
'.mkd',
'.jst',
'.coffee',
'.tgz',
'.swp',
]);
function checkShasumAndUngzip(ungzipDir, readstream, pkg, useTarFormat, options) {
return new Promise((resolve, reject) => {
const shasum = pkg.dist.shasum;
const integrity = pkg.dist.integrity;
const algorithmType = pkg.dist.checkSSRI ? 'sha512' : 'sha1';
const hash = crypto.createHash(algorithmType);
let tarballSize = 0;
const opts = {
cwd: ungzipDir,
strip: 1,
onentry(entry) {
if (entry.type.toLowerCase() === 'file') {
/* eslint-disable no-bitwise */
entry.mode = (entry.mode || 0) | 0o644;
}
if (entry.type.toLowerCase() === 'directory') {
/* eslint-disable no-bitwise */
entry.mode = (entry.mode || 0) | 0o755;
}
},
};
if (options.prune) {
opts.filter = function(filepath, entry) {
options.rawCount++;
options.rawSize += entry.size;
const filename = path.basename(filepath);
if (defaultFiles[filename]) {
options.pruneCount++;
options.pruneSize += entry.size;
return false;
}
const extname = path.extname(filename);
if (defaultExtensions[extname]) {
options.pruneCount++;
options.pruneSize += entry.size;
return false;
}
// package/foo/bar/zoo
const dirname = filepath.split('/', 3)[1];
if (defaultDirectories[dirname]) {
options.pruneCount++;
options.pruneSize += entry.size;
return false;
}
return true;
};
}
const extracter = tar.extract(opts);
let isEnd = false;
function handleCallback(err) {
if (isEnd) return;
isEnd = true;
if (err) {
// make sure readstream will be destroy
destroy(readstream);
err.message += ` (${pkg.name}@${pkg.version})`;
if (readstream.tarballFile && utils.existsSync(readstream.tarballFile)) {
err.message += ` (${readstream.tarballFile})`;
debug('[%s@%s] remove tarball file: %s, because %s',
pkg.name, pkg.version, readstream.tarballFile, err);
// remove tarball cache file
rmSync(readstream.tarballFile, { force: true });
}
return reject(err);
}
resolve();
}
readstream.on('data', buf => {
tarballSize += buf.length;
hash.update(buf);
});
readstream.on('end', () => {
// this will be fire before extracter `env` event fire.
let hashResult = '';
let hashString = '';
if (pkg.dist.checkSSRI) {
hashResult = algorithmType + '-' + hash.digest('base64');
hashString = integrity;
} else {
hashResult = hash.digest('hex');
hashString = shasum;
}
if (hashResult !== hashString) {
const err = new Error(`real ${algorithmType}:${hashResult} not equal to remote:${hashString}, download url ${readstream.tarballUrl || ''}, download size ${tarballSize}`);
err.name = 'ShasumNotMatchError';
handleCallback(err);
}
});
extracter.on('end', handleCallback);
readstream.on('error', handleCallback);
extracter.on('error', handleCallback);
if (useTarFormat) {
readstream.pipe(extracter);
} else {
const gunzip = zlib.createGunzip();
gunzip.on('error', handleCallback);
readstream.pipe(gunzip).pipe(extracter);
}
});
}
async function replaceHostInFile(pkg, filepath, binaryMirror, globalOptions) {
const exists = await utils.exists(filepath);
if (!exists) {
return;
}
let content = await fs.readFile(filepath, 'utf8');
let replaceHostMap;
// support RegExp string
if (binaryMirror.replaceHostRegExpMap) {
replaceHostMap = binaryMirror.replaceHostRegExpMap;
for (const replaceHost in replaceHostMap) {
const replaceAllRE = new RegExp(replaceHost, 'g');
const targetHost = replaceHostMap[replaceHost];
debug('replace %j(%s) => %s', replaceHost, replaceAllRE, targetHost);
content = content.replace(replaceAllRE, targetHost);
}
} else {
replaceHostMap = binaryMirror.replaceHostMap;
if (!replaceHostMap) {
let replaceHosts = binaryMirror.replaceHost;
if (!Array.isArray(replaceHosts)) {
replaceHosts = [ replaceHosts ];
}
replaceHostMap = {};
for (const replaceHost of replaceHosts) {
replaceHostMap[replaceHost] = binaryMirror.host;
}
}
for (const replaceHost in replaceHostMap) {
content = content.replace(replaceHost, replaceHostMap[replaceHost]);
}
}
debug('%s: \n%s', filepath, content);
await fs.writeFile(filepath, content);
globalOptions.console.info('%s download from mirrors: %j, changed file: %s',
chalk.gray(`${pkg.name}@${pkg.version}`),
replaceHostMap,
filepath);
}