verdaccio
Version:
Private npm repository server
531 lines (446 loc) • 16.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
var _lodash = require('lodash');
var _lodash2 = _interopRequireDefault(_lodash);
var _assert = require('assert');
var _assert2 = _interopRequireDefault(_assert);
var _async = require('async');
var _async2 = _interopRequireDefault(_async);
var _stream = require('stream');
var _stream2 = _interopRequireDefault(_stream);
var _upStorage = require('./up-storage');
var _upStorage2 = _interopRequireDefault(_upStorage);
var _search = require('./search');
var _search2 = _interopRequireDefault(_search);
var _constants = require('./constants');
var _localStorage = require('./local-storage');
var _localStorage2 = _interopRequireDefault(_localStorage);
var _streams = require('@verdaccio/streams');
var _storageUtils = require('./storage-utils');
var _uplinkUtil = require('./uplink-util');
var _metadataUtils = require('./metadata-utils');
var _utils = require('./utils');
var _configUtils = require('./config-utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
const LoggerApi = require('../lib/logger');
class Storage {
constructor(config) {
this.config = config;
this.uplinks = (0, _uplinkUtil.setupUpLinks)(config);
this.logger = LoggerApi.logger.child();
}
init(config) {
this.localStorage = new _localStorage2.default(this.config, LoggerApi.logger);
return this.localStorage.getSecret(config);
}
/**
* Add a {name} package to a system
Function checks if package with the same name is available from uplinks.
If it isn't, we create package locally
Used storages: local (write) && uplinks
*/
addPackage(name, metadata, callback) {
var _this = this;
return _asyncToGenerator(function* () {
try {
yield (0, _storageUtils.checkPackageLocal)(name, _this.localStorage);
yield (0, _storageUtils.checkPackageRemote)(name, _this._isAllowPublishOffline(), _this._syncUplinksMetadata.bind(_this));
yield (0, _storageUtils.publishPackage)(name, metadata, _this.localStorage);
callback();
} catch (err) {
callback(err);
}
})();
}
_isAllowPublishOffline() {
return typeof this.config.publish !== 'undefined' && _lodash2.default.isBoolean(this.config.publish.allow_offline) && this.config.publish.allow_offline;
}
/**
* Add a new version of package {name} to a system
Used storages: local (write)
*/
addVersion(name, version, metadata, tag, callback) {
this.localStorage.addVersion(name, version, metadata, tag, callback);
}
/**
* Tags a package version with a provided tag
Used storages: local (write)
*/
mergeTags(name, tagHash, callback) {
this.localStorage.mergeTags(name, tagHash, callback);
}
/**
* Change an existing package (i.e. unpublish one version)
Function changes a package info from local storage and all uplinks with write access./
Used storages: local (write)
*/
changePackage(name, metadata, revision, callback) {
this.localStorage.changePackage(name, metadata, revision, callback);
}
/**
* Remove a package from a system
Function removes a package from local storage
Used storages: local (write)
*/
removePackage(name, callback) {
this.localStorage.removePackage(name, callback);
// update the indexer
_search2.default.remove(name);
}
/**
Remove a tarball from a system
Function removes a tarball from local storage.
Tarball in question should not be linked to in any existing
versions, i.e. package version should be unpublished first.
Used storage: local (write)
*/
removeTarball(name, filename, revision, callback) {
this.localStorage.removeTarball(name, filename, revision, callback);
}
/**
* Upload a tarball for {name} package
Function is syncronous and returns a WritableStream
Used storages: local (write)
*/
addTarball(name, filename) {
return this.localStorage.addTarball(name, filename);
}
/**
Get a tarball from a storage for {name} package
Function is syncronous and returns a ReadableStream
Function tries to read tarball locally, if it fails then it reads package
information in order to figure out where we can get this tarball from
Used storages: local || uplink (just one)
*/
getTarball(name, filename) {
let readStream = new _streams.ReadTarball();
readStream.abort = function () {};
let self = this;
// if someone requesting tarball, it means that we should already have some
// information about it, so fetching package info is unnecessary
// trying local first
// flow: should be IReadTarball
let localStream = self.localStorage.getTarball(name, filename);
let isOpen = false;
localStream.on('error', err => {
if (isOpen || err.status !== _constants.HTTP_STATUS.NOT_FOUND) {
return readStream.emit('error', err);
}
// local reported 404
let err404 = err;
localStream.abort();
// $FlowFixMe
localStream = null; // we force for garbage collector
self.localStorage.getPackageMetadata(name, (err, info) => {
if (_lodash2.default.isNil(err) && info._distfiles && _lodash2.default.isNil(info._distfiles[filename]) === false) {
// information about this file exists locally
serveFile(info._distfiles[filename]);
} else {
// we know nothing about this file, trying to get information elsewhere
self._syncUplinksMetadata(name, info, {}, (err, info) => {
if (_lodash2.default.isNil(err) === false) {
return readStream.emit('error', err);
}
if (_lodash2.default.isNil(info._distfiles) || _lodash2.default.isNil(info._distfiles[filename])) {
return readStream.emit('error', err404);
}
serveFile(info._distfiles[filename]);
});
}
});
});
localStream.on('content-length', function (v) {
readStream.emit('content-length', v);
});
localStream.on('open', function () {
isOpen = true;
localStream.pipe(readStream);
});
return readStream;
/**
* Fetch and cache local/remote packages.
* @param {Object} file define the package shape
*/
function serveFile(file) {
let uplink = null;
for (let uplinkId in self.uplinks) {
// $FlowFixMe
if (self.uplinks[uplinkId].isUplinkValid(file.url)) {
uplink = self.uplinks[uplinkId];
}
}
if (uplink == null) {
uplink = new _upStorage2.default({
url: file.url,
cache: true,
_autogenerated: true
}, self.config);
}
let savestream = null;
if (uplink.config.cache) {
savestream = self.localStorage.addTarball(name, filename);
}
let on_open = function () {
// prevent it from being called twice
on_open = function () {};
let rstream2 = uplink.fetchTarball(file.url);
rstream2.on('error', function (err) {
if (savestream) {
savestream.abort();
}
savestream = null;
readStream.emit('error', err);
});
rstream2.on('end', function () {
if (savestream) {
savestream.done();
}
});
rstream2.on('content-length', function (v) {
readStream.emit('content-length', v);
if (savestream) {
savestream.emit('content-length', v);
}
});
rstream2.pipe(readStream);
if (savestream) {
rstream2.pipe(savestream);
}
};
if (savestream) {
savestream.on('open', function () {
on_open();
});
savestream.on('error', function (err) {
self.logger.warn({ err: err, fileName: file }, 'error saving file @{fileName}: @{err.message}\n@{err.stack}');
if (savestream) {
savestream.abort();
}
savestream = null;
on_open();
});
} else {
on_open();
}
}
}
/**
Retrieve a package metadata for {name} package
Function invokes localStorage.getPackage and uplink.get_package for every
uplink with proxy_access rights against {name} and combines results
into one json object
Used storages: local && uplink (proxy_access)
* @param {object} options
* @property {string} options.name Package Name
* @property {object} options.req Express `req` object
* @property {boolean} options.keepUpLinkData keep up link info in package meta, last update, etc.
* @property {function} options.callback Callback for receive data
*/
getPackage(options) {
this.localStorage.getPackageMetadata(options.name, (err, data) => {
if (err && (!err.status || err.status >= _constants.HTTP_STATUS.INTERNAL_ERROR)) {
// report internal errors right away
return options.callback(err);
}
this._syncUplinksMetadata(options.name, data, { req: options.req }, function getPackageSynUpLinksCallback(err, result, uplinkErrors) {
if (err) {
return options.callback(err);
}
(0, _utils.normalizeDistTags)((0, _storageUtils.cleanUpLinksRef)(options.keepUpLinkData, result));
// npm can throw if this field doesn't exist
result._attachments = {};
options.callback(null, result, uplinkErrors);
});
});
}
/**
Retrieve remote and local packages more recent than {startkey}
Function streams all packages from all uplinks first, and then
local packages.
Note that local packages could override registry ones just because
they appear in JSON last. That's a trade-off we make to avoid
memory issues.
Used storages: local && uplink (proxy_access)
* @param {*} startkey
* @param {*} options
* @return {Stream}
*/
search(startkey, options) {
let self = this;
// stream to write a tarball
let stream = new _stream2.default.PassThrough({ objectMode: true });
_async2.default.eachSeries(Object.keys(this.uplinks), function (up_name, cb) {
// shortcut: if `local=1` is supplied, don't call uplinks
if (options.req.query.local !== undefined) {
return cb();
}
// search by keyword for each uplink
let lstream = self.uplinks[up_name].search(options);
// join streams
lstream.pipe(stream, { end: false });
lstream.on('error', function (err) {
self.logger.error({ err: err }, 'uplink error: @{err.message}');
cb(), cb = function () {};
});
lstream.on('end', function () {
cb(), cb = function () {};
});
stream.abort = function () {
if (lstream.abort) {
lstream.abort();
}
cb(), cb = function () {};
};
},
// executed after all series
function () {
// attach a local search results
let lstream = self.localStorage.search(startkey, options);
stream.abort = function () {
lstream.abort();
};
lstream.pipe(stream, { end: true });
lstream.on('error', function (err) {
self.logger.error({ err: err }, 'search error: @{err.message}');
stream.end();
});
});
return stream;
}
/**
* Retrieve only private local packages
* @param {*} callback
*/
getLocalDatabase(callback) {
let self = this;
this.localStorage.localData.get((err, locals) => {
if (err) {
callback(err);
}
let packages = [];
const getPackage = function (itemPkg) {
self.localStorage.getPackageMetadata(locals[itemPkg], function (err, info) {
if (_lodash2.default.isNil(err)) {
const latest = info[_utils.DIST_TAGS].latest;
if (latest && info.versions[latest]) {
const version = info.versions[latest];
const time = info.time[latest];
version.time = time;
packages.push(version);
} else {
self.logger.warn({ package: locals[itemPkg] }, 'package @{package} does not have a "latest" tag?');
}
}
if (itemPkg >= locals.length - 1) {
callback(null, packages);
} else {
getPackage(itemPkg + 1);
}
});
};
if (locals.length) {
getPackage(0);
} else {
callback(null, []);
}
});
}
/**
* Function fetches package metadata from uplinks and synchronizes it with local data
if package is available locally, it MUST be provided in pkginfo
returns callback(err, result, uplink_errors)
*/
_syncUplinksMetadata(name, packageInfo, options, callback) {
let exists = true;
const self = this;
const upLinks = [];
if (!packageInfo || packageInfo === null) {
exists = false;
packageInfo = (0, _storageUtils.generatePackageTemplate)(name);
}
for (let uplink in this.uplinks) {
if ((0, _configUtils.hasProxyTo)(name, uplink, this.config.packages)) {
upLinks.push(this.uplinks[uplink]);
}
}
_async2.default.map(upLinks, (upLink, cb) => {
const _options = Object.assign({}, options);
let upLinkMeta = packageInfo._uplinks[upLink.upname];
if ((0, _utils.isObject)(upLinkMeta)) {
const fetched = upLinkMeta.fetched;
if (fetched && Date.now() - fetched < upLink.maxage) {
return cb();
}
_options.etag = upLinkMeta.etag;
}
upLink.getRemoteMetadata(name, _options, (err, upLinkResponse, eTag) => {
if (err && err.remoteStatus === 304) {
upLinkMeta.fetched = Date.now();
}
if (err || !upLinkResponse) {
// $FlowFixMe
return cb(null, [err || _utils.ErrorCode.getInternalError('no data')]);
}
try {
(0, _utils.validate_metadata)(upLinkResponse, name);
} catch (err) {
self.logger.error({
sub: 'out',
err: err
}, 'package.json validating error @{!err.message}\n@{err.stack}');
return cb(null, [err]);
}
packageInfo._uplinks[upLink.upname] = {
etag: eTag,
fetched: Date.now()
};
packageInfo.time = (0, _storageUtils.mergeUplinkTimeIntoLocal)(packageInfo, upLinkResponse);
(0, _uplinkUtil.updateVersionsHiddenUpLink)(upLinkResponse.versions, upLink);
try {
(0, _metadataUtils.mergeVersions)(packageInfo, upLinkResponse);
} catch (err) {
self.logger.error({
sub: 'out',
err: err
}, 'package.json parsing error @{!err.message}\n@{err.stack}');
return cb(null, [err]);
}
// if we got to this point, assume that the correct package exists
// on the uplink
exists = true;
cb();
});
}, (err, upLinksErrors) => {
(0, _assert2.default)(!err && Array.isArray(upLinksErrors));
if (!exists) {
return callback(_utils.ErrorCode.getNotFound(_constants.API_ERROR.NO_PACKAGE), null, upLinksErrors);
}
self.localStorage.updateVersions(name, packageInfo, function (err, packageJsonLocal) {
if (err) {
return callback(err);
}
return callback(null, packageJsonLocal, upLinksErrors);
});
});
}
/**
* Set a hidden value for each version.
* @param {Array} versions list of version
* @param {String} upLink uplink name
* @private
*/
_updateVersionsHiddenUpLink(versions, upLink) {
for (let i in versions) {
if (Object.prototype.hasOwnProperty.call(versions, i)) {
const version = versions[i];
// holds a "hidden" value to be used by the package storage.
// $FlowFixMe
version[Symbol.for('__verdaccio_uplink')] = upLink.upname;
}
}
}
}
exports.default = Storage;
;