Files
cnpmjs.org/controllers/registry/package/list.js

483 lines
13 KiB
JavaScript

'use strict';
var debug = require('debug')('cnpmjs.org:controllers:registry:package:list');
var utility = require('utility');
var packageService = require('../../../services/package');
var blocklistService = require('../../../services/blocklist');
var common = require('../../../lib/common');
var SyncModuleWorker = require('../../sync_module_worker');
var config = require('../../../config');
const cache = require('../../../common/cache');
const logger = require('../../../common/logger');
// https://forum.nginx.org/read.php?2,240120,240120#msg-240120
// should set weak etag avoid nginx remove it
function etag(objs) {
return 'W/"' + utility.md5(JSON.stringify(objs)) + '"';
}
function filterBlockVerions(rows, blocks) {
if (!blocks) {
return rows;
}
return rows.filter(row => !blocks[row.version]);
}
/**
* list all version of a module
* GET /:name
*/
module.exports = function* list() {
const name = this.params.name || this.params[0];
// sync request will contain this query params
let noCache = this.query.cache === '0';
if (!noCache) {
const ua = this.headers['user-agent'] || '';
// old sync client will request with these user-agent
if (ua.indexOf('npm_service.cnpmjs.org/') !== -1) {
noCache = true;
}
}
const isJSONPRequest = this.query.callback;
let cacheKey;
let needAbbreviatedMeta = false;
let abbreviatedMetaType = 'application/vnd.npm.install-v1+json';
if (config.enableAbbreviatedMetadata && this.accepts([ 'json', abbreviatedMetaType ]) === abbreviatedMetaType) {
needAbbreviatedMeta = true;
if (cache && !isJSONPRequest) {
cacheKey = `list-${name}-v1`;
}
}
if (cacheKey && !noCache) {
const values = yield cache.hmget(cacheKey, 'etag', 'body');
if (values && values[0] && values[1]) {
this.body = values[1];
this.type = 'json';
this.etag = values[0];
this.set('x-hit-cache', cacheKey);
debug('hmget %s success, etag:%j', cacheKey, values[0]);
if (config.registryCacheControlHeader) {
this.set('cache-control', config.registryCacheControlHeader);
}
if (config.registryVaryHeader) {
this.set('vary', config.registryVaryHeader);
}
return;
}
debug('hmget %s missing, %j', cacheKey, values);
}
var rs = yield [
packageService.getModuleLastModified(name),
packageService.listModuleTags(name),
blocklistService.findBlockPackageVersions(name),
];
var modifiedTime = rs[0];
var tags = rs[1];
var blocks = rs[2];
if (blocks && blocks['*']) {
this.status = 451;
const error = `[block] package was blocked, reason: ${blocks['*'].reason}`;
this.jsonp = {
name,
error,
reason: error,
};
return;
}
debug('show %s, last modified: %s, tags: %j', name, modifiedTime, tags);
if (modifiedTime) {
// find out the latest modfied time
// because update tags only modfied tag, wont change module gmt_modified
for (var i = 0; i < tags.length; i++) {
var tag = tags[i];
if (tag.gmt_modified > modifiedTime) {
modifiedTime = tag.gmt_modified;
}
}
// must set status first
this.status = 200;
if (this.fresh) {
debug('%s not change at %s, 304 return', name, modifiedTime);
this.status = 304;
return;
}
}
if (needAbbreviatedMeta) {
var rows = yield packageService.listModuleAbbreviatedsByName(name);
rows = filterBlockVerions(rows, blocks);
if (rows.length > 0) {
yield handleAbbreviatedMetaRequest(this, name, modifiedTime, tags, rows, cacheKey);
return;
}
var fullRows = yield packageService.listModulesByName(name);
fullRows = filterBlockVerions(fullRows, blocks);
if (fullRows.length > 0) {
// no abbreviated meta rows, use the full meta convert to abbreviated meta
yield handleAbbreviatedMetaRequestWithFullMeta(this, name, modifiedTime, tags, fullRows);
return;
}
}
var r = yield [
packageService.listModulesByName(name),
packageService.listStarUserNames(name),
packageService.listMaintainers(name),
];
var rows = filterBlockVerions(r[0], blocks);
var starUsers = r[1];
var maintainers = r[2];
debug('show %s got %d rows, %d tags, %d star users, maintainers: %j',
name, rows.length, tags.length, starUsers.length, maintainers);
var starUserMap = {};
for (var i = 0; i < starUsers.length; i++) {
var starUser = starUsers[i];
if (starUser[0] !== '"' && starUser[0] !== "'") {
starUserMap[starUser] = true;
}
}
starUsers = starUserMap;
if (rows.length === 0) {
// check if unpublished
var unpublishedInfo = yield packageService.getUnpublishedModule(name);
debug('show unpublished %j', unpublishedInfo);
if (unpublishedInfo) {
this.status = 404;
this.jsonp = {
_id: name,
name: name,
time: {
modified: unpublishedInfo.package.time,
unpublished: unpublishedInfo.package,
},
_attachments: {},
};
return;
}
}
// if module not exist in this registry,
// sync the module backend and return package info from official registry
if (rows.length === 0) {
if (!this.allowSync) {
this.status = 404;
const error = '[not_found] document not found';
this.jsonp = {
error,
reason: error,
};
return;
}
// start sync
var logId = yield SyncModuleWorker.sync(name, 'sync-by-install');
debug('start sync %s, get log id %s', name, logId);
return this.redirect(config.officialNpmRegistry + this.url);
}
var latestMod = null;
var readme = null;
// set tags
var distTags = {};
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
distTags[t.tag] = t.version;
}
// set versions and times
var versions = {};
var allVersionString = '';
var times = {};
var attachments = {};
var createdTime = null;
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var pkg = row.package;
// pkg is string ... ignore it
if (typeof pkg === 'string') {
continue;
}
common.setDownloadURL(pkg, this);
pkg._cnpm_publish_time = row.publish_time;
pkg.publish_time = pkg.publish_time || row.publish_time;
versions[pkg.version] = pkg;
allVersionString += pkg.version + ',';
var t = times[pkg.version] = row.publish_time ? new Date(row.publish_time) : row.gmt_modified;
if ((!distTags.latest && !latestMod) || distTags.latest === pkg.version) {
latestMod = row;
readme = pkg.readme;
}
delete pkg.readme;
if (maintainers.length > 0) {
pkg.maintainers = maintainers;
}
if (!createdTime || t < createdTime) {
createdTime = t;
}
}
if (modifiedTime && createdTime) {
var ts = {
modified: modifiedTime,
created: createdTime,
};
for (var t in times) {
ts[t] = times[t];
}
times = ts;
}
if (!latestMod) {
latestMod = rows[0];
}
var rev = String(latestMod.id);
var pkg = latestMod.package;
if (tags.length === 0) {
// some sync error reason, will cause tags missing
// set latest tag at least
distTags.latest = pkg.version;
}
if (!readme && config.enableAbbreviatedMetadata) {
var packageReadme = yield packageService.getPackageReadme(name);
if (packageReadme) {
readme = packageReadme.readme;
}
}
var info = {
_id: name,
_rev: rev,
name: name,
description: pkg.description,
'dist-tags': distTags,
maintainers: pkg.maintainers,
time: times,
users: starUsers,
author: pkg.author,
repository: pkg.repository,
versions: versions,
readme: readme,
_attachments: attachments,
};
info.readmeFilename = pkg.readmeFilename;
info.homepage = pkg.homepage;
info.bugs = pkg.bugs;
info.license = pkg.license;
if (typeof config.formatCustomFullPackageInfoAndVersions === 'function') {
info = config.formatCustomFullPackageInfoAndVersions(this, info);
}
debug('show module %s: %s, latest: %s', name, rev, latestMod.version);
this.jsonp = info;
// use faster etag
this.etag = etag([
modifiedTime,
distTags,
pkg.maintainers,
allVersionString,
]);
if (config.registryCacheControlHeader) {
this.set('cache-control', config.registryCacheControlHeader);
}
if (config.registryVaryHeader) {
this.set('vary', config.registryVaryHeader);
}
};
function* handleAbbreviatedMetaRequest(ctx, name, modifiedTime, tags, rows, cacheKey) {
debug('show %s got %d rows, %d tags, modifiedTime: %s', name, rows.length, tags.length, modifiedTime);
const isJSONPRequest = ctx.query.callback;
var latestMod = null;
// set tags
var distTags = {};
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
distTags[t.tag] = t.version;
}
// set versions and times
var versions = {};
var allVersionString = '';
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var pkg = row.package;
common.setDownloadURL(pkg, ctx);
pkg._publish_on_cnpm = undefined;
pkg.publish_time = pkg.publish_time || row.publish_time;
versions[pkg.version] = pkg;
allVersionString += pkg.version + ',';
if ((!distTags.latest && !latestMod) || distTags.latest === pkg.version) {
latestMod = row;
}
// abbreviatedMeta row maybe update by syncer on missing attributes add
if (!modifiedTime || row.gmt_modified > modifiedTime) {
modifiedTime = row.gmt_modified;
}
}
if (!latestMod) {
latestMod = rows[0];
}
if (tags.length === 0) {
// some sync error reason, will cause tags missing
// set latest tag at least
distTags.latest = latestMod.package.version;
}
var info = {
name: name,
modified: modifiedTime,
'dist-tags': distTags,
versions: versions,
};
debug('show %j', info);
// use faster etag
const resultEtag = etag([
modifiedTime,
distTags,
allVersionString,
]);
if (isJSONPRequest) {
ctx.jsonp = info;
} else {
ctx.body = JSON.stringify(info);
ctx.type = 'json';
// set cache
if (cacheKey) {
// set cache async, dont block the response
cache.pipeline()
.hmset(cacheKey, 'etag', resultEtag, 'body', ctx.body)
// cache 120s
.expire(cacheKey, 120)
.exec()
.catch(err => {
logger.error(err);
});
}
}
ctx.etag = resultEtag;
if (config.registryCacheControlHeader) {
ctx.set('cache-control', config.registryCacheControlHeader);
}
if (config.registryVaryHeader) {
ctx.set('vary', config.registryVaryHeader);
}
}
function* handleAbbreviatedMetaRequestWithFullMeta(ctx, name, modifiedTime, tags, rows) {
debug('show %s got %d rows, %d tags',
name, rows.length, tags.length);
var latestMod = null;
// set tags
var distTags = {};
for (var i = 0; i < tags.length; i++) {
var t = tags[i];
distTags[t.tag] = t.version;
}
// set versions and times
var versions = {};
var allVersionString = '';
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
// pkg is string ... ignore it
if (typeof row.package === 'string') {
continue;
}
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
var hasInstallScript;
if (row.package.scripts) {
// https://www.npmjs.com/package/fix-has-install-script
if (row.package.scripts.install || row.package.scripts.preinstall || row.package.scripts.postinstall) {
hasInstallScript = true;
}
}
var pkg = {
name: row.package.name,
version: row.package.version,
deprecated: row.package.deprecated,
dependencies: row.package.dependencies,
optionalDependencies: row.package.optionalDependencies,
devDependencies: row.package.devDependencies,
bundleDependencies: row.package.bundleDependencies,
peerDependencies: row.package.peerDependencies,
peerDependenciesMeta: row.package.peerDependenciesMeta,
bin: row.package.bin,
os: row.package.os,
cpu: row.package.cpu,
directories: row.package.directories,
dist: row.package.dist,
engines: row.package.engines,
workspaces: row.package.workspaces,
_hasShrinkwrap: row.package._hasShrinkwrap,
hasInstallScript: hasInstallScript,
publish_time: row.package.publish_time || row.publish_time,
};
common.setDownloadURL(pkg, ctx);
versions[pkg.version] = pkg;
allVersionString += pkg.version + ',';
if ((!distTags.latest && !latestMod) || distTags.latest === pkg.version) {
latestMod = row;
}
}
if (!latestMod) {
latestMod = rows[0];
}
if (tags.length === 0) {
// some sync error reason, will cause tags missing
// set latest tag at least
distTags.latest = latestMod.package.version;
}
var info = {
name: name,
modified: modifiedTime,
'dist-tags': distTags,
versions: versions,
};
debug('show %j', info);
ctx.jsonp = info;
// use faster etag
ctx.etag = etag([
modifiedTime,
distTags,
allVersionString,
]);
if (config.registryCacheControlHeader) {
ctx.set('cache-control', config.registryCacheControlHeader);
}
if (config.registryVaryHeader) {
ctx.set('vary', config.registryVaryHeader);
}
}