diff --git a/lib/index.js b/lib/index.js index 8e9c8ecc8..c669fc01c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,10 +11,10 @@ var Storage = require('./storage') module.exports = function(config_hash) { Logger.setup(config_hash.logs) - var config = Config(config_hash) - var storage = Storage(config) - var auth = Auth(config) - var app = express() + var config = Config(config_hash); + var storage = new Storage(config); + var auth = Auth(config); + var app = express(); // run in production mode by default, just in case // it shouldn't make any difference anyway diff --git a/lib/local-storage.js b/lib/local-storage.js index 3c688c8df..a3e7a7f3a 100644 --- a/lib/local-storage.js +++ b/lib/local-storage.js @@ -1,30 +1,19 @@ "use strict"; -var assert = require('assert') -var async = require('async') -var Crypto = require('crypto') -var fs = require('fs') -var Error = require('http-errors') -var Path = require('path') -var Stream = require('readable-stream') -var URL = require('url') -var fs_storage = require('./local-fs') -var Logger = require('./logger') -var Search = require('./search') -var MyStreams = require('./streams') -var Utils = require('./utils') -var info_file = 'package.json' - -// -// Implements Storage interface -// (same for storage.js, local-storage.js, up-storage.js) -// -class Storage { - constructor(config) { - this.config = config - this.logger = Logger.logger.child({ sub: 'fs' }) - } -} +const assert = require('assert'); +const async = require('async'); +const Crypto = require('crypto'); +const fs = require('fs'); +const Error = require('http-errors'); +const Path = require('path'); +const Stream = require('readable-stream'); +const URL = require('url'); +const fs_storage = require('./local-fs'); +const Logger = require('./logger'); +const Search = require('./search'); +const MyStreams = require('./streams'); +const Utils = require('./utils'); +const info_file = 'package.json'; // returns the minimal package file function get_boilerplate(name) { @@ -39,626 +28,739 @@ function get_boilerplate(name) { '_attachments': {}, '_uplinks': {}, } -} +}; -Storage.prototype._internal_error = function(err, file, message) { - this.logger.error( { err: err, file: file } - , message + ' @{file}: @{!err.message}' ) - return Error[500]() -} +// +// Implements Storage interface +// (same for storage.js, local-storage.js, up-storage.js) +// +class Storage { + constructor(config) { + this.config = config + this.logger = Logger.logger.child({ sub: 'fs' }) + } -Storage.prototype.add_package = function(name, info, callback) { - var storage = this.storage(name) - if (!storage) return callback( Error[404]('this package cannot be added') ) + /** + * + * @param {*} err + * @param {*} file + * @param {*} message + */ + _internal_error(err, file, message) { + this.logger.error( { err: err, file: file } + , message + ' @{file}: @{!err.message}' ) + return Error[500]() + } - storage.create_json(info_file, get_boilerplate(name), function(err) { - if (err && err.code === 'EEXISTS') { - return callback( Error[409]('this package is already present') ) - } + /** + * + * @param {*} name + * @param {*} info + * @param {*} callback + */ + add_package(name, info, callback) { + var storage = this.storage(name) + if (!storage) return callback( Error[404]('this package cannot be added') ) - var latest = info['dist-tags'].latest - if (latest && info.versions[latest]) { - Search.add(info.versions[latest]) - } - callback() - }) -} - -Storage.prototype.remove_package = function(name, callback) { - this.logger.info( { name: name } - , 'unpublishing @{name} (all)') - - var storage = this.storage(name) - if (!storage) return callback( Error[404]('no such package available') ) - - storage.read_json(info_file, (err, data) => { - if (err) { - if (err.code === 'ENOENT') { - return callback( Error[404]('no such package available') ) - } else { - return callback(err) - } - } - this._normalize_package(data) - - storage.unlink(info_file, function(err) { - if (err) return callback(err) - - var files = Object.keys(data._attachments) - - function unlinkNext(cb) { - if (files.length === 0) return cb() - - var file = files.shift() - storage.unlink(file, function() { - unlinkNext(cb) - }) + storage.create_json(info_file, get_boilerplate(name), function(err) { + if (err && err.code === 'EEXISTS') { + return callback( Error[409]('this package is already present') ) } - unlinkNext(function() { - // try to unlink the directory, but ignore errors because it can fail - storage.rmdir('.', function(err) { - callback(err) + var latest = info['dist-tags'].latest + if (latest && info.versions[latest]) { + Search.add(info.versions[latest]) + } + callback() + }) + } + + /** + * + * @param {*} name + * @param {*} callback + */ + remove_package(name, callback) { + this.logger.info( { name: name } + , 'unpublishing @{name} (all)') + + var storage = this.storage(name) + if (!storage) return callback( Error[404]('no such package available') ) + + storage.read_json(info_file, (err, data) => { + if (err) { + if (err.code === 'ENOENT') { + return callback( Error[404]('no such package available') ) + } else { + return callback(err) + } + } + this._normalize_package(data) + + storage.unlink(info_file, function(err) { + if (err) return callback(err) + + var files = Object.keys(data._attachments) + + function unlinkNext(cb) { + if (files.length === 0) return cb() + + var file = files.shift() + storage.unlink(file, function() { + unlinkNext(cb) + }) + } + + unlinkNext(function() { + // try to unlink the directory, but ignore errors because it can fail + storage.rmdir('.', function(err) { + callback(err) + }) }) }) }) - }) - Search.remove(name) - this.config.localList.remove(name) -} - -Storage.prototype._read_create_package = function(name, callback) { - var storage = this.storage(name) - if (!storage) { - var data = get_boilerplate(name) - this._normalize_package(data) - return callback(null, data) + Search.remove(name) + this.config.localList.remove(name) } - storage.read_json(info_file, (err, data) => { - // TODO: race condition - if (err) { - if (err.code === 'ENOENT') { - // if package doesn't exist, we create it here - data = get_boilerplate(name) - } else { - return callback(this._internal_error(err, info_file, 'error reading')) - } + + /** + * + * @param {*} name + * @param {*} callback + */ + _read_create_package(name, callback) { + var storage = this.storage(name) + if (!storage) { + var data = get_boilerplate(name) + this._normalize_package(data) + return callback(null, data) } - this._normalize_package(data) - callback(null, data) - }) -} + storage.read_json(info_file, (err, data) => { + // TODO: race condition + if (err) { + if (err.code === 'ENOENT') { + // if package doesn't exist, we create it here + data = get_boilerplate(name) + } else { + return callback(this._internal_error(err, info_file, 'error reading')) + } + } + this._normalize_package(data) + callback(null, data) + }) + } -// synchronize remote package info with the local one -// TODO: readfile called twice -Storage.prototype.update_versions = function(name, newdata, callback) { - this._read_create_package(name, (err, data) => { - if (err) return callback(err) + /** + * Synchronize remote package info with the local one + * @param {*} name + * @param {*} newdata + * @param {*} callback + */ + update_versions(name, newdata, callback) { + this._read_create_package(name, (err, data) => { + if (err) return callback(err) - var change = false - for (var ver in newdata.versions) { - if (data.versions[ver] == null) { - var verdata = newdata.versions[ver] + var change = false + for (var ver in newdata.versions) { + if (data.versions[ver] == null) { + var verdata = newdata.versions[ver] - // we don't keep readmes for package versions, - // only one readme per package - delete verdata.readme + // we don't keep readmes for package versions, + // only one readme per package + delete verdata.readme - change = true - data.versions[ver] = verdata + change = true + data.versions[ver] = verdata - if (verdata.dist && verdata.dist.tarball) { - var filename = URL.parse(verdata.dist.tarball).pathname.replace(/^.*\//, '') - // we do NOT overwrite any existing records - if (data._distfiles[filename] == null) { - var hash = data._distfiles[filename] = { - url: verdata.dist.tarball, - sha: verdata.dist.shasum, - } - - if (verdata._verdaccio_uplink) { - // if we got this information from a known registry, - // use the same protocol for the tarball - // - // see https://github.com/rlidwka/sinopia/issues/166 - var tarball_url = URL.parse(hash.url) - var uplink_url = URL.parse(this.config.uplinks[verdata._verdaccio_uplink].url) - if (uplink_url.host === tarball_url.host) { - tarball_url.protocol = uplink_url.protocol - hash.registry = verdata._verdaccio_uplink - hash.url = URL.format(tarball_url) + if (verdata.dist && verdata.dist.tarball) { + var filename = URL.parse(verdata.dist.tarball).pathname.replace(/^.*\//, '') + // we do NOT overwrite any existing records + if (data._distfiles[filename] == null) { + var hash = data._distfiles[filename] = { + url: verdata.dist.tarball, + sha: verdata.dist.shasum, + } + // if (verdata[Symbol('_verdaccio_uplink')]) { + if (verdata._verdaccio_uplink) { + // if we got this information from a known registry, + // use the same protocol for the tarball + // + // see https://github.com/rlidwka/sinopia/issues/166 + var tarball_url = URL.parse(hash.url) + var uplink_url = URL.parse(this.config.uplinks[verdata._verdaccio_uplink].url) + if (uplink_url.host === tarball_url.host) { + tarball_url.protocol = uplink_url.protocol + hash.registry = verdata._verdaccio_uplink + hash.url = URL.format(tarball_url) + } } } } } } - } - for (var tag in newdata['dist-tags']) { - if (!data['dist-tags'][tag] || data['dist-tags'][tag] !== newdata['dist-tags'][tag]) { - change = true - data['dist-tags'][tag] = newdata['dist-tags'][tag] - } - } - for (var up in newdata._uplinks) { - var need_change = !Utils.is_object(data._uplinks[up]) - || newdata._uplinks[up].etag !== data._uplinks[up].etag - || newdata._uplinks[up].fetched !== data._uplinks[up].fetched - - if (need_change) { - change = true - data._uplinks[up] = newdata._uplinks[up] - } - } - if (newdata.readme !== data.readme) { - data.readme = newdata.readme - change = true - } - - if (change) { - this.logger.debug('updating package info') - this._write_package(name, data, function(err) { - callback(err, data) - }) - } else { - callback(null, data) - } - }) -} - -Storage.prototype.add_version = function(name, version, metadata, tag, callback) { - this.update_package(name, (data, cb) => { - // keep only one readme per package - data.readme = metadata.readme - delete metadata.readme - - if (data.versions[version] != null) { - return cb( Error[409]('this version already present') ) - } - - // if uploaded tarball has a different shasum, it's very likely that we have some kind of error - if (Utils.is_object(metadata.dist) && typeof(metadata.dist.tarball) === 'string') { - var tarball = metadata.dist.tarball.replace(/.*\//, '') - if (Utils.is_object(data._attachments[tarball])) { - if (data._attachments[tarball].shasum != null && metadata.dist.shasum != null) { - if (data._attachments[tarball].shasum != metadata.dist.shasum) { - return cb( Error[400]('shasum error, ' - + data._attachments[tarball].shasum - + ' != ' + metadata.dist.shasum) ) - } + for (var tag in newdata['dist-tags']) { + if (!data['dist-tags'][tag] || data['dist-tags'][tag] !== newdata['dist-tags'][tag]) { + change = true + data['dist-tags'][tag] = newdata['dist-tags'][tag] } - - data._attachments[tarball].version = version } - } + for (var up in newdata._uplinks) { + var need_change = !Utils.is_object(data._uplinks[up]) + || newdata._uplinks[up].etag !== data._uplinks[up].etag + || newdata._uplinks[up].fetched !== data._uplinks[up].fetched - data.versions[version] = metadata - Utils.tag_version(data, version, tag) - this.config.localList.add(name) - cb() - }, callback) -} - -Storage.prototype.merge_tags = function(name, tags, callback) { - this.update_package(name, function updater(data, cb) { - for (let t in tags) { - if (tags[t] === null) { - delete data['dist-tags'][t] - continue + if (need_change) { + change = true + data._uplinks[up] = newdata._uplinks[up] + } + } + if (newdata.readme !== data.readme) { + data.readme = newdata.readme + change = true } - if (data.versions[tags[t]] == null) { - return cb( Error[404]("this version doesn't exist") ) + if (change) { + this.logger.debug('updating package info') + this._write_package(name, data, function(err) { + callback(err, data) + }) + } else { + callback(null, data) } - - Utils.tag_version(data, tags[t], t) - } - cb() - }, callback) -} - -Storage.prototype.replace_tags = function(name, tags, callback) { - this.update_package(name, function updater(data, cb) { - data['dist-tags'] = {} - - for (let t in tags) { - if (tags[t] === null) { - delete data['dist-tags'][t] - continue - } - - if (data.versions[tags[t]] == null) { - return cb( Error[404]("this version doesn't exist") ) - } - - Utils.tag_version(data, tags[t], t) - } - cb() - }, callback) -} - -// currently supports unpublishing only -Storage.prototype.change_package = function(name, metadata, revision, callback) { - - if (!Utils.is_object(metadata.versions) || !Utils.is_object(metadata['dist-tags'])) { - return callback( Error[422]('bad data') ) + }) } - this.update_package(name, (data, cb) => { - for (let ver in data.versions) { - if (metadata.versions[ver] == null) { - this.logger.info( { name: name, version: ver } - , 'unpublishing @{name}@@{version}') - delete data.versions[ver] + /** + * + * @param {*} name + * @param {*} version + * @param {*} metadata + * @param {*} tag + * @param {*} callback + */ + add_version(name, version, metadata, tag, callback) { + this.update_package(name, (data, cb) => { + // keep only one readme per package + data.readme = metadata.readme + delete metadata.readme - for (var file in data._attachments) { - if (data._attachments[file].version === ver) { - delete data._attachments[file].version + if (data.versions[version] != null) { + return cb( Error[409]('this version already present') ) + } + + // if uploaded tarball has a different shasum, it's very likely that we have some kind of error + if (Utils.is_object(metadata.dist) && typeof(metadata.dist.tarball) === 'string') { + var tarball = metadata.dist.tarball.replace(/.*\//, '') + if (Utils.is_object(data._attachments[tarball])) { + if (data._attachments[tarball].shasum != null && metadata.dist.shasum != null) { + if (data._attachments[tarball].shasum != metadata.dist.shasum) { + return cb( Error[400]('shasum error, ' + + data._attachments[tarball].shasum + + ' != ' + metadata.dist.shasum) ) + } } + data._attachments[tarball].version = version } } - } - data['dist-tags'] = metadata['dist-tags'] - cb() - }, function(err) { - if (err) return callback(err) - callback() - }) -} -Storage.prototype.remove_tarball = function(name, filename, revision, callback) { - assert(Utils.validate_name(filename)) - - this.update_package(name, (data, cb) => { - if (data._attachments[filename]) { - delete data._attachments[filename] + data.versions[version] = metadata + Utils.tag_version(data, version, tag) + this.config.localList.add(name) cb() - } else { - cb(Error[404]('no such file available')) - } - }, function(err) { - if (err) return callback(err) - var storage = this.storage(name) - if (storage) storage.unlink(filename, callback) - }) -} - -Storage.prototype.add_tarball = function(name, filename) { - assert(Utils.validate_name(filename)) - - var stream = MyStreams.UploadTarballStream() - var _transform = stream._transform - var length = 0 - var shasum = Crypto.createHash('sha1') - - stream.abort = stream.done = function(){} - - stream._transform = function(data) { - shasum.update(data) - length += data.length - _transform.apply(stream, arguments) + }, callback) } - if (name === info_file || name === '__proto__') { - process.nextTick(function() { - stream.emit('error', Error[403]("can't use this filename")) - }) - return stream - } - - var storage = this.storage(name) - if (!storage) { - process.nextTick(function() { - stream.emit('error', Error[404]("can't upload this package")) - }) - return stream - } - - var wstream = storage.write_stream(filename) - - wstream.on('error', (err) => { - if (err.code === 'EEXISTS') { - stream.emit('error', Error[409]('this tarball is already present')) - } else if (err.code === 'ENOENT') { - // check if package exists to throw an appropriate message - this.get_package(name, function(_err, res) { - if (_err) { - stream.emit('error', _err) - } else { - stream.emit('error', err) - } - }) - } else { - stream.emit('error', err) - } - }) - - wstream.on('open', function() { - // re-emitting open because it's handled in storage.js - stream.emit('open') - }) - wstream.on('success', () => { + /** + * + * @param {*} name + * @param {*} tags + * @param {*} callback + */ + merge_tags(name, tags, callback) { this.update_package(name, function updater(data, cb) { - data._attachments[filename] = { - shasum: shasum.digest('hex'), + for (let t in tags) { + if (tags[t] === null) { + delete data['dist-tags'][t] + continue + } + + if (data.versions[tags[t]] == null) { + return cb( Error[404]("this version doesn't exist") ) + } + + Utils.tag_version(data, tags[t], t) } cb() + }, callback) + } + + /** + * + * @param {*} name + * @param {*} tags + * @param {*} callback + */ + replace_tags(name, tags, callback) { + this.update_package(name, function updater(data, cb) { + data['dist-tags'] = {} + + for (let t in tags) { + if (tags[t] === null) { + delete data['dist-tags'][t] + continue + } + + if (data.versions[tags[t]] == null) { + return cb( Error[404]("this version doesn't exist") ) + } + + Utils.tag_version(data, tags[t], t) + } + cb() + }, callback) + } + + /** + * Currently supports unpublishing only + * @param {*} name + * @param {*} metadata + * @param {*} revision + * @param {*} callback + */ + change_package(name, metadata, revision, callback) { + + if (!Utils.is_object(metadata.versions) || !Utils.is_object(metadata['dist-tags'])) { + return callback( Error[422]('bad data') ) + } + + this.update_package(name, (data, cb) => { + for (let ver in data.versions) { + if (metadata.versions[ver] == null) { + this.logger.info( { name: name, version: ver } + , 'unpublishing @{name}@@{version}') + delete data.versions[ver] + + for (var file in data._attachments) { + if (data._attachments[file].version === ver) { + delete data._attachments[file].version + } + } + } + } + data['dist-tags'] = metadata['dist-tags'] + cb() }, function(err) { - if (err) { - stream.emit('error', err) + if (err) return callback(err) + callback() + }) + } + + /** + * + * @param {*} name + * @param {*} filename + * @param {*} revision + * @param {*} callback + */ + remove_tarball(name, filename, revision, callback) { + assert(Utils.validate_name(filename)) + + this.update_package(name, (data, cb) => { + if (data._attachments[filename]) { + delete data._attachments[filename] + cb() } else { - stream.emit('success') + cb(Error[404]('no such file available')) + } + }, function(err) { + if (err) return callback(err) + var storage = this.storage(name) + if (storage) storage.unlink(filename, callback) + }) + } + + /** + * + * @param {*} name + * @param {*} filename + */ + add_tarball(name, filename) { + assert(Utils.validate_name(filename)) + + var stream = MyStreams.UploadTarballStream() + var _transform = stream._transform + var length = 0 + var shasum = Crypto.createHash('sha1') + + stream.abort = stream.done = function(){} + + stream._transform = function(data) { + shasum.update(data) + length += data.length + _transform.apply(stream, arguments) + } + + if (name === info_file || name === '__proto__') { + process.nextTick(function() { + stream.emit('error', Error[403]("can't use this filename")) + }) + return stream + } + + var storage = this.storage(name) + if (!storage) { + process.nextTick(function() { + stream.emit('error', Error[404]("can't upload this package")) + }) + return stream + } + + var wstream = storage.write_stream(filename) + + wstream.on('error', (err) => { + if (err.code === 'EEXISTS') { + stream.emit('error', Error[409]('this tarball is already present')) + } else if (err.code === 'ENOENT') { + // check if package exists to throw an appropriate message + this.get_package(name, function(_err, res) { + if (_err) { + stream.emit('error', _err) + } else { + stream.emit('error', err) + } + }) + } else { + stream.emit('error', err) } }) - }) - stream.abort = function() { - wstream.abort() - } - stream.done = function() { - if (!length) { - stream.emit('error', Error[422]('refusing to accept zero-length file')) + + wstream.on('open', function() { + // re-emitting open because it's handled in storage.js + stream.emit('open') + }) + wstream.on('success', () => { + this.update_package(name, function updater(data, cb) { + data._attachments[filename] = { + shasum: shasum.digest('hex'), + } + cb() + }, function(err) { + if (err) { + stream.emit('error', err) + } else { + stream.emit('success') + } + }) + }) + stream.abort = function() { wstream.abort() - } else { - wstream.done() } - } - stream.pipe(wstream) + stream.done = function() { + if (!length) { + stream.emit('error', Error[422]('refusing to accept zero-length file')) + wstream.abort() + } else { + wstream.done() + } + } + stream.pipe(wstream) - return stream -} - -Storage.prototype.get_tarball = function(name, filename, callback) { - assert(Utils.validate_name(filename)) - var self = this - - var stream = MyStreams.ReadTarballStream() - stream.abort = function() { - if (rstream) rstream.abort() + return stream } - var storage = self.storage(name) - if (!storage) { - process.nextTick(function() { - stream.emit('error', Error[404]('no such file available')) + /** + * + * @param {*} name + * @param {*} filename + * @param {*} callback + */ + get_tarball(name, filename, callback) { + assert(Utils.validate_name(filename)) + var self = this + + var stream = MyStreams.ReadTarballStream() + stream.abort = function() { + if (rstream) rstream.abort() + } + + var storage = self.storage(name) + if (!storage) { + process.nextTick(function() { + stream.emit('error', Error[404]('no such file available')) + }) + return stream + } + + var rstream = storage.read_stream(filename) + rstream.on('error', function(err) { + if (err && err.code === 'ENOENT') { + stream.emit('error', Error(404, 'no such file available')) + } else { + stream.emit('error', err) + } + }) + rstream.on('content-length', function(v) { + stream.emit('content-length', v) + }) + rstream.on('open', function() { + // re-emitting open because it's handled in storage.js + stream.emit('open') + rstream.pipe(stream) }) return stream } - var rstream = storage.read_stream(filename) - rstream.on('error', function(err) { - if (err && err.code === 'ENOENT') { - stream.emit('error', Error(404, 'no such file available')) - } else { - stream.emit('error', err) + /** + * + * @param {*} name + * @param {*} options + * @param {*} callback + */ + get_package(name, options, callback) { + if (typeof(options) === 'function') { + callback = options, options = {}; } - }) - rstream.on('content-length', function(v) { - stream.emit('content-length', v) - }) - rstream.on('open', function() { - // re-emitting open because it's handled in storage.js - stream.emit('open') - rstream.pipe(stream) - }) - return stream -} -Storage.prototype.get_package = function(name, options, callback) { - if (typeof(options) === 'function') { - callback = options, options = {}; - } + var storage = this.storage(name) + if (!storage) return callback( Error[404]('no such package available') ) - var storage = this.storage(name) - if (!storage) return callback( Error[404]('no such package available') ) - - storage.read_json(info_file, (err, result) => { - if (err) { - if (err.code === 'ENOENT') { - return callback( Error[404]('no such package available') ) - } else { - return callback(this._internal_error(err, info_file, 'error reading')) - } - } - this._normalize_package(result) - callback(err, result) - }) -} - -// walks through each package and calls `on_package` on them -Storage.prototype._each_package = function (on_package, on_end) { - var storages = {} - - storages[this.config.storage] = true; - - if (this.config.packages) { - Object.keys(this.packages || {}).map( pkg => { - if (this.config.packages[pkg].storage) { - storages[this.config.packages[pkg].storage] = true - } - }) - } - - const base = Path.dirname(this.config.self_path); - - async.eachSeries(Object.keys(storages), function (storage, cb) { - fs.readdir(Path.resolve(base, storage), function (err, files) { - if (err) return cb(err) - - async.eachSeries(files, function (file, cb) { - if (file.match(/^@/)) { - // scoped - fs.readdir(Path.resolve(base, storage, file), function (err, files) { - if (err) return cb(err) - - async.eachSeries(files, function (file2, cb) { - if (Utils.validate_name(file2)) { - on_package({ - name: `${file}/${file2}`, - path: Path.resolve(base, storage, file, file2), - }, cb) - } else { - cb() - } - }, cb) - }) - } else if (Utils.validate_name(file)) { - on_package({ - name: file, - path: Path.resolve(base, storage, file) - }, cb) + storage.read_json(info_file, (err, result) => { + if (err) { + if (err.code === 'ENOENT') { + return callback( Error[404]('no such package available') ) } else { - cb() + return callback(this._internal_error(err, info_file, 'error reading')) } - }, cb) + } + this._normalize_package(result) + callback(err, result) }) - }, on_end) -} + } -// -// This function allows to update the package thread-safely -// -// Arguments: -// - name - package name -// - updateFn - function(package, cb) - update function -// - callback - callback that gets invoked after it's all updated -// -// Algorithm: -// 1. lock package.json for writing -// 2. read package.json -// 3. updateFn(pkg, cb), and wait for cb -// 4. write package.json.tmp -// 5. move package.json.tmp package.json -// 6. callback(err?) -// -Storage.prototype.update_package = function(name, updateFn, _callback) { - var self = this - var storage = self.storage(name) - if (!storage) return _callback( Error[404]('no such package available') ) - storage.lock_and_read_json(info_file, function(err, json) { - var locked = false + /** + * Walks through each package and calls `on_package` on them + * @param {*} on_package + * @param {*} on_end + */ + _each_package(on_package, on_end) { + var storages = {} - // callback that cleans up lock first - function callback(err) { - var _args = arguments - if (locked) { - storage.unlock_file(info_file, function () { - // ignore any error from the unlock - _callback.apply(err, _args) + storages[this.config.storage] = true; + + if (this.config.packages) { + Object.keys(this.packages || {}).map( pkg => { + if (this.config.packages[pkg].storage) { + storages[this.config.packages[pkg].storage] = true + } + }) + } + const base = Path.dirname(this.config.self_path); + + async.eachSeries(Object.keys(storages), function (storage, cb) { + fs.readdir(Path.resolve(base, storage), function (err, files) { + if (err) return cb(err) + + async.eachSeries(files, function (file, cb) { + if (file.match(/^@/)) { + // scoped + fs.readdir(Path.resolve(base, storage, file), function (err, files) { + if (err) return cb(err) + + async.eachSeries(files, function (file2, cb) { + if (Utils.validate_name(file2)) { + on_package({ + name: `${file}/${file2}`, + path: Path.resolve(base, storage, file, file2), + }, cb) + } else { + cb() + } + }, cb) + }) + } else if (Utils.validate_name(file)) { + on_package({ + name: file, + path: Path.resolve(base, storage, file) + }, cb) + } else { + cb() + } + }, cb) }) - } else { - _callback.apply(null, _args) - } + }, on_end); } - if (!err) { - locked = true - } + /** + * This function allows to update the package thread-safely + Algorithm: + 1. lock package.json for writing + 2. read package.json + 3. updateFn(pkg, cb), and wait for cb + 4. write package.json.tmp + 5. move package.json.tmp package.json + 6. callback(err?) + * @param {*} name package name + * @param {*} updateFn function(package, cb) - update function + * @param {*} _callback callback that gets invoked after it's all updated + */ + update_package(name, updateFn, _callback) { + var self = this + var storage = self.storage(name) + if (!storage) return _callback( Error[404]('no such package available') ) + storage.lock_and_read_json(info_file, function(err, json) { + var locked = false - if (err) { - if (err.code === 'EAGAIN') { - return callback( Error[503]('resource temporarily unavailable') ) - } else if (err.code === 'ENOENT') { - return callback( Error[404]('no such package available') ) - } else { - return callback(err) - } - } - - self._normalize_package(json) - updateFn(json, function(err) { - if (err) return callback(err) - - self._write_package(name, json, callback) - }) - }) -} - -Storage.prototype.search = function(startkey, options) { - var stream = new Stream.PassThrough({ objectMode: true }) - - this._each_package((item, cb) => { - fs.stat(item.path, (err, stats) => { - if (err) return cb(err) - - if (stats.mtime > startkey) { - this.get_package(item.name, options, function(err, data) { - if (err) return cb(err) - - var versions = Utils.semver_sort(Object.keys(data.versions)) - var latest = data['dist-tags'] && data['dist-tags'].latest ? data['dist-tags'].latest : versions.pop() - - if (data.versions[latest]) { - stream.push({ - name : data.versions[latest].name, - description : data.versions[latest].description, - 'dist-tags' : { latest: latest }, - maintainers : data.versions[latest].maintainers || - [ data.versions[latest]._npmUser ].filter(Boolean), - author : data.versions[latest].author, - repository : data.versions[latest].repository, - readmeFilename : data.versions[latest].readmeFilename || '', - homepage : data.versions[latest].homepage, - keywords : data.versions[latest].keywords, - bugs : data.versions[latest].bugs, - license : data.versions[latest].license, - time : { modified: item.time ? new Date(item.time).toISOString() : undefined }, - versions : {}, + // callback that cleans up lock first + function callback(err) { + var _args = arguments + if (locked) { + storage.unlock_file(info_file, function () { + // ignore any error from the unlock + _callback.apply(err, _args) }) + } else { + _callback.apply(null, _args) + } + } + + if (!err) { + locked = true + } + + if (err) { + if (err.code === 'EAGAIN') { + return callback( Error[503]('resource temporarily unavailable') ) + } else if (err.code === 'ENOENT') { + return callback( Error[404]('no such package available') ) + } else { + return callback(err) + } + } + + self._normalize_package(json) + updateFn(json, function(err) { + if (err) return callback(err) + + self._write_package(name, json, callback) + }) + }) + } + + /** + * + * @param {*} startkey + * @param {*} options + */ + search(startkey, options) { + const stream = new Stream.PassThrough({ objectMode: true }); + + this._each_package((item, cb) => { + fs.stat(item.path, (err, stats) => { + if (err) { + return cb(err); } - cb() + if (stats.mtime > startkey) { + this.get_package(item.name, options, function(err, data) { + if (err) { + return cb(err); + } + + var versions = Utils.semver_sort(Object.keys(data.versions)) + var latest = data['dist-tags'] && data['dist-tags'].latest ? data['dist-tags'].latest : versions.pop() + + if (data.versions[latest]) { + stream.push({ + name : data.versions[latest].name, + description : data.versions[latest].description, + 'dist-tags' : { latest: latest }, + maintainers : data.versions[latest].maintainers || + [ data.versions[latest]._npmUser ].filter(Boolean), + author : data.versions[latest].author, + repository : data.versions[latest].repository, + readmeFilename : data.versions[latest].readmeFilename || '', + homepage : data.versions[latest].homepage, + keywords : data.versions[latest].keywords, + bugs : data.versions[latest].bugs, + license : data.versions[latest].license, + time : { + modified: item.time ? new Date(item.time).toISOString() : undefined + }, + versions : {}, + }) + } + + cb() + }) + } else { + cb() + } }) - } else { - cb() + }, function on_end(err) { + if (err) return stream.emit('error', err) + stream.end() + }) + + return stream + } + + /** + * + * @param {*} pkg + */ + _normalize_package(pkg) { + ;['versions', 'dist-tags', '_distfiles', '_attachments', '_uplinks'].forEach(function(key) { + if (!Utils.is_object(pkg[key])) pkg[key] = {} + }) + if (typeof(pkg._rev) !== 'string') { + pkg._rev = '0-0000000000000000'; } - }) - }, function on_end(err) { - if (err) return stream.emit('error', err) - stream.end() - }) + // normalize dist-tags + Utils.normalize_dist_tags(pkg) + } - return stream -} + /** + * + * @param {*} name + * @param {*} json + * @param {*} callback + */ + _write_package(name, json, callback) { -Storage.prototype._normalize_package = function(pkg) { - ;['versions', 'dist-tags', '_distfiles', '_attachments', '_uplinks'].forEach(function(key) { - if (!Utils.is_object(pkg[key])) pkg[key] = {} - }) - if (typeof(pkg._rev) !== 'string') { - pkg._rev = '0-0000000000000000'; - } - // normalize dist-tags - Utils.normalize_dist_tags(pkg) -} + // calculate revision a la couchdb + if (typeof(json._rev) !== 'string') { + json._rev = '0-0000000000000000'; + } + var rev = json._rev.split('-') + json._rev = ((+rev[0] || 0) + 1) + '-' + Crypto.pseudoRandomBytes(8).toString('hex') -Storage.prototype._write_package = function(name, json, callback) { + var storage = this.storage(name) + if (!storage) return callback() + storage.write_json(info_file, json, callback) + } - // calculate revision a la couchdb - if (typeof(json._rev) !== 'string') { - json._rev = '0-0000000000000000'; - } - var rev = json._rev.split('-') - json._rev = ((+rev[0] || 0) + 1) + '-' + Crypto.pseudoRandomBytes(8).toString('hex') - - var storage = this.storage(name) - if (!storage) return callback() - storage.write_json(info_file, json, callback) -} - -Storage.prototype.storage = function(pkg) { - var path = this.config.get_package_spec(pkg).storage - if (path == null) path = this.config.storage - if (path == null || path === false) { - this.logger.debug( { name: pkg } - , 'this package has no storage defined: @{name}' ) - return null - } - return Path_Wrapper( - Path.join( - Path.resolve(Path.dirname(this.config.self_path || ''), path), - pkg - ) - ) + /** + * + * @param {*} pkg + */ + storage(pkg) { + let path = this.config.get_package_spec(pkg).storage + if (path == null) { + path = this.config.storage + } + if (path == null || path === false) { + this.logger.debug( { name: pkg } + , 'this package has no storage defined: @{name}' ) + return null + } + return Path_Wrapper( + Path.join( + Path.resolve(Path.dirname(this.config.self_path || ''), path), + pkg + ) + ) + } } var Path_Wrapper = (function() { diff --git a/lib/storage.js b/lib/storage.js index bab9fc159..5b0ac16e3 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -1,523 +1,555 @@ "use strict"; -var assert = require('assert') -var async = require('async') -var Error = require('http-errors') -var Stream = require('stream') -var Local = require('./local-storage') -var Logger = require('./logger') -var MyStreams = require('./streams') -var Proxy = require('./up-storage') -var Utils = require('./utils') - -module.exports = Storage +const assert = require('assert') +const async = require('async') +const Error = require('http-errors') +const Stream = require('stream') +const Local = require('./local-storage') +const Logger = require('./logger') +const MyStreams = require('./streams') +const Proxy = require('./up-storage') +const Utils = require('./utils') // // Implements Storage interface // (same for storage.js, local-storage.js, up-storage.js) // -function Storage(config) { - var self = Object.create(Storage.prototype) - self.config = config +class Storage { - // we support a number of uplinks, but only one local storage - // Proxy and Local classes should have similar API interfaces - self.uplinks = {} - for (var p in config.uplinks) { - self.uplinks[p] = Proxy(config.uplinks[p], config) - self.uplinks[p].upname = p + /** + * + * @param {*} config + */ + constructor(config) { + this.config = config + // we support a number of uplinks, but only one local storage + // Proxy and Local classes should have similar API interfaces + this.uplinks = {} + for (let p in config.uplinks) { + // instance for each up-link definition + this.uplinks[p] = new Proxy(config.uplinks[p], config) + this.uplinks[p].upname = p + } + // an instance for local storage + this.local = new Local(config) + this.logger = Logger.logger.child(); } - self.local = new Local(config) - self.logger = Logger.logger.child() - return self -} + /** + * 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 + * @param {*} name + * @param {*} metadata + * @param {*} callback + */ + add_package(name, metadata, callback) { + var self = this -// -// 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 -// -Storage.prototype.add_package = function(name, metadata, callback) { - var self = this + // NOTE: + // - when we checking package for existance, we ask ALL uplinks + // - when we publishing package, we only publish it to some of them + // so all requests are necessary - // NOTE: - // - when we checking package for existance, we ask ALL uplinks - // - when we publishing package, we only publish it to some of them - // so all requests are necessary - - check_package_local(function(err) { - if (err) return callback(err) - - check_package_remote(function(err) { + check_package_local(function(err) { if (err) return callback(err) - publish_package(function(err) { + check_package_remote(function(err) { if (err) return callback(err) - callback() + + publish_package(function(err) { + if (err) return callback(err) + callback() + }) }) }) - }) - function check_package_local(cb) { - self.local.get_package(name, {}, function(err, results) { - if (err && err.status !== 404) return cb(err) + function check_package_local(cb) { + self.local.get_package(name, {}, function(err, results) { + if (err && err.status !== 404) return cb(err) - if (results) return cb( Error[409]('this package is already present') ) + if (results) return cb( Error[409]('this package is already present') ) - cb() - }) - } + cb() + }) + } - function check_package_remote(cb) { - self._sync_package_with_uplinks(name, null, {}, function(err, results, err_results) { - // something weird - if (err && err.status !== 404) return cb(err) + function check_package_remote(cb) { + self._sync_package_with_uplinks(name, null, {}, function(err, results, err_results) { + // something weird + if (err && err.status !== 404) return cb(err) - // checking package - if (results) return cb( Error[409]('this package is already present') ) + // checking package + if (results) return cb( Error[409]('this package is already present') ) - for (var i=0; i= 500)) { - // report internal errors right away - return callback(err) + /** + Retrieve a package metadata for {name} package + Function invokes local.get_package 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 {*} name + * @param {*} options + * @param {*} callback + */ + get_package(name, options, callback) { + if (typeof(options) === 'function') { + callback = options, options = {}; } - self._sync_package_with_uplinks(name, data, options, function(err, result, uplink_errors) { - if (err) return callback(err) - var whitelist = [ '_rev', 'name', 'versions', 'dist-tags', 'readme' ] - for (var i in result) { - if (whitelist.indexOf(i) === -1) delete result[i] + this.local.get_package(name, options, (err, data) => { + if (err && (!err.status || err.status >= 500)) { + // report internal errors right away + return callback(err) } - Utils.normalize_dist_tags(result) + this._sync_package_with_uplinks(name, data, options, function(err, result, uplink_errors) { + if (err) return callback(err) + const whitelist = [ '_rev', 'name', 'versions', 'dist-tags', 'readme' ] + for (var i in result) { + if (whitelist.indexOf(i) === -1) delete result[i] + } - // npm can throw if this field doesn't exist - result._attachments = {} + Utils.normalize_dist_tags(result) - callback(null, result, uplink_errors) + // npm can throw if this field doesn't exist + result._attachments = {} + + callback(null, result, uplink_errors) + }) }) - }) -} + } -// -// 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) -// -Storage.prototype.search = function(startkey, options) { - var self = this + /** + 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 + */ + search(startkey, options) { + var self = this - var stream = new Stream.PassThrough({ objectMode: true }) + var stream = new Stream.PassThrough({ objectMode: true }) - async.eachSeries(Object.keys(self.uplinks), function(up_name, cb) { - // shortcut: if `local=1` is supplied, don't call uplinks - if (options.req.query.local !== undefined) return cb() + async.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() - var lstream = self.uplinks[up_name].search(startkey, options) - 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 () {} + var lstream = self.uplinks[up_name].search(startkey, options) + 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 () {} + } + }, function () { + var lstream = self.local.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() + }) }) - stream.abort = function () { - if (lstream.abort) lstream.abort() - cb(), cb = function () {} - } - }, function () { - var lstream = self.local.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; + } - return stream -} + /** + * + * @param {*} callback + */ + get_local(callback) { + var self = this + var locals = this.config.localList.get() + var packages = [] -Storage.prototype.get_local = function(callback) { - var self = this - var locals = this.config.localList.get() - var packages = [] + var getPackage = function(i) { + self.local.get_package(locals[i], function(err, info) { + if (!err) { + var latest = info['dist-tags'].latest + if (latest && info.versions[latest]) { + packages.push(info.versions[latest]) + } else { + self.logger.warn( { package: locals[i] } + , 'package @{package} does not have a "latest" tag?' ) + } + } - var getPackage = function(i) { - self.local.get_package(locals[i], function(err, info) { - if (!err) { - var latest = info['dist-tags'].latest - if (latest && info.versions[latest]) { - packages.push(info.versions[latest]) + if (i >= locals.length - 1) { + callback(null, packages) } else { - self.logger.warn( { package: locals[i] } - , 'package @{package} does not have a "latest" tag?' ) + getPackage(i + 1) + } + }) + } + + if (locals.length) { + getPackage(0) + } else { + callback(null, []) + } + } + + /** + * Function fetches package information 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) + * @param {*} name + * @param {*} pkginfo + * @param {*} options + * @param {*} callback + */ + _sync_package_with_uplinks(name, pkginfo, options, callback) { + var self = this + let exists = false; + if (!pkginfo) { + exists = false + + pkginfo = { + name : name, + versions : {}, + 'dist-tags' : {}, + _uplinks : {}, + } + } else { + exists = true + } + + var uplinks = [] + for (let i in self.uplinks) { + if (self.config.can_proxy_to(name, i)) { + uplinks.push(self.uplinks[i]) + } + } + + async.map(uplinks, function(up, cb) { + var _options = Object.assign({}, options) + if (Utils.is_object(pkginfo._uplinks[up.upname])) { + var fetched = pkginfo._uplinks[up.upname].fetched + if (fetched && fetched > (Date.now() - up.maxage)) { + return cb() + } + + _options.etag = pkginfo._uplinks[up.upname].etag + } + + up.get_package(name, _options, function(err, up_res, etag) { + if (err && err.status === 304) + pkginfo._uplinks[up.upname].fetched = Date.now() + + if (err || !up_res) return cb(null, [err || Error('no data')]) + + try { + Utils.validate_metadata(up_res, name) + } catch(err) { + self.logger.error({ + sub: 'out', + err: err, + }, 'package.json validating error @{!err.message}\n@{err.stack}') + return cb(null, [ err ]) + } + + pkginfo._uplinks[up.upname] = { + etag: etag, + fetched: Date.now() + } + for (let i in up_res.versions) { + // this won't be serialized to json, + // kinda like an ES6 Symbol + //FIXME: perhaps Symbol('_verdaccio_uplink') here? + Object.defineProperty(up_res.versions[i], '_verdaccio_uplink', { + value : up.upname, + enumerable : false, + configurable : false, + writable : true, + }); + } + + try { + Storage._merge_versions(pkginfo, up_res, self.config); + } 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() + }) + }, function(err, uplink_errors) { + assert(!err && Array.isArray(uplink_errors)) + + if (!exists) { + return callback( Error[404]('no such package available') + , null + , uplink_errors ) + } + + self.local.update_versions(name, pkginfo, function(err, pkginfo) { + if (err) return callback(err) + return callback(null, pkginfo, uplink_errors) + }) + }) + } + + /** + * Function gets a local info and an info from uplinks and tries to merge it + exported for unit tests only. + * @param {*} local + * @param {*} up + * @param {*} config + */ + static _merge_versions(local, up, config) { + // copy new versions to a cache + // NOTE: if a certain version was updated, we can't refresh it reliably + for (var i in up.versions) { + if (local.versions[i] == null) { + local.versions[i] = up.versions[i] + } + } + + // refresh dist-tags + for (var i in up['dist-tags']) { + if (local['dist-tags'][i] !== up['dist-tags'][i]) { + local['dist-tags'][i] = up['dist-tags'][i] + if (i === 'latest') { + // if remote has more fresh package, we should borrow its readme + local.readme = up.readme } } - - if (i >= locals.length - 1) { - callback(null, packages) - } else { - getPackage(i + 1) - } - }) + } } - if (locals.length) { - getPackage(0) - } else { - callback(null, []) - } } -// function fetches package information 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) -Storage.prototype._sync_package_with_uplinks = function(name, pkginfo, options, callback) { - var self = this - - if (!pkginfo) { - var exists = false - - pkginfo = { - name : name, - versions : {}, - 'dist-tags' : {}, - _uplinks : {}, - } - } else { - var exists = true - } - - var uplinks = [] - for (var i in self.uplinks) { - if (self.config.can_proxy_to(name, i)) { - uplinks.push(self.uplinks[i]) - } - } - - async.map(uplinks, function(up, cb) { - var _options = Object.assign({}, options) - if (Utils.is_object(pkginfo._uplinks[up.upname])) { - var fetched = pkginfo._uplinks[up.upname].fetched - if (fetched && fetched > (Date.now() - up.maxage)) { - return cb() - } - - _options.etag = pkginfo._uplinks[up.upname].etag - } - - up.get_package(name, _options, function(err, up_res, etag) { - if (err && err.status === 304) - pkginfo._uplinks[up.upname].fetched = Date.now() - - if (err || !up_res) return cb(null, [err || Error('no data')]) - - try { - Utils.validate_metadata(up_res, name) - } catch(err) { - self.logger.error({ - sub: 'out', - err: err, - }, 'package.json validating error @{!err.message}\n@{err.stack}') - return cb(null, [ err ]) - } - - pkginfo._uplinks[up.upname] = { - etag: etag, - fetched: Date.now() - } - - for (var i in up_res.versions) { - // this won't be serialized to json, - // kinda like an ES6 Symbol - Object.defineProperty(up_res.versions[i], '_verdaccio_uplink', { - value : up.upname, - enumerable : false, - configurable : false, - writable : true, - }) - } - - try { - Storage._merge_versions(pkginfo, up_res, self.config) - } 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() - }) - }, function(err, uplink_errors) { - assert(!err && Array.isArray(uplink_errors)) - - if (!exists) { - return callback( Error[404]('no such package available') - , null - , uplink_errors ) - } - - self.local.update_versions(name, pkginfo, function(err, pkginfo) { - if (err) return callback(err) - return callback(null, pkginfo, uplink_errors) - }) - }) -} - -// function gets a local info and an info from uplinks and tries to merge it -// exported for unit tests only -Storage._merge_versions = function(local, up, config) { - // copy new versions to a cache - // NOTE: if a certain version was updated, we can't refresh it reliably - for (var i in up.versions) { - if (local.versions[i] == null) { - local.versions[i] = up.versions[i] - } - } - - // refresh dist-tags - for (var i in up['dist-tags']) { - if (local['dist-tags'][i] !== up['dist-tags'][i]) { - local['dist-tags'][i] = up['dist-tags'][i] - if (i === 'latest') { - // if remote has more fresh package, we should borrow its readme - local.readme = up.readme - } - } - } -} +module.exports = Storage; diff --git a/lib/up-storage.js b/lib/up-storage.js index b94863165..65b2b384d 100644 --- a/lib/up-storage.js +++ b/lib/up-storage.js @@ -1,59 +1,19 @@ "use strict"; -var JSONStream = require('JSONStream') -var Error = require('http-errors') -var request = require('request') -var Stream = require('readable-stream') -var URL = require('url') -var parse_interval = require('./config').parse_interval -var Logger = require('./logger') -var MyStreams = require('./streams') -var Utils = require('./utils') -var encode = function(thing) { +const JSONStream = require('JSONStream') +const Error = require('http-errors') +const request = require('request') +const Stream = require('readable-stream') +const URL = require('url') +const parse_interval = require('./config').parse_interval +const Logger = require('./logger') +const MyStreams = require('./streams') +const Utils = require('./utils') +const encode = function(thing) { return encodeURIComponent(thing).replace(/^%40/, '@'); }; -module.exports = Storage - -// -// Implements Storage interface -// (same for storage.js, local-storage.js, up-storage.js) -// -function Storage(config, mainconfig) { - var self = Object.create(Storage.prototype) - self.config = config - self.failed_requests = 0 - self.userAgent = mainconfig.user_agent - self.ca = config.ca - self.logger = Logger.logger.child({sub: 'out'}) - self.server_id = mainconfig.server_id - - self.url = URL.parse(self.config.url) - - _setupProxy.call(self, self.url.hostname, config, mainconfig, self.url.protocol === 'https:') - - self.config.url = self.config.url.replace(/\/$/, '') - if (Number(self.config.timeout) >= 1000) { - self.logger.warn([ 'Too big timeout value: ' + self.config.timeout, - 'We changed time format to nginx-like one', - '(see http://wiki.nginx.org/ConfigNotation)', - 'so please update your config accordingly' ].join('\n')) - } - - // a bunch of different configurable timers - self.maxage = parse_interval(config_get('maxage' , '2m' )) - self.timeout = parse_interval(config_get('timeout' , '30s')) - self.max_fails = Number(config_get('max_fails' , 2 )) - self.fail_timeout = parse_interval(config_get('fail_timeout', '5m' )) - return self - - // just a helper (`config[key] || default` doesn't work because of zeroes) - function config_get(key, def) { - return config[key] != null ? config[key] : def - } -} - -function _setupProxy(hostname, config, mainconfig, isHTTPS) { +const _setupProxy = function(hostname, config, mainconfig, isHTTPS) { var no_proxy var proxy_key = isHTTPS ? 'https_proxy' : 'http_proxy' @@ -100,6 +60,52 @@ function _setupProxy(hostname, config, mainconfig, isHTTPS) { } } +// +// Implements Storage interface +// (same for storage.js, local-storage.js, up-storage.js) +// +class Storage { + + /** + * + * @param {*} config + * @param {*} mainconfig + */ + constructor(config, mainconfig) { + this.config = config + this.failed_requests = 0 + this.userAgent = mainconfig.user_agent + this.ca = config.ca + this.logger = Logger.logger.child({sub: 'out'}) + this.server_id = mainconfig.server_id + + this.url = URL.parse(this.config.url) + + _setupProxy.call(this, this.url.hostname, config, mainconfig, this.url.protocol === 'https:') + + this.config.url = this.config.url.replace(/\/$/, '') + if (Number(this.config.timeout) >= 1000) { + this.logger.warn([ 'Too big timeout value: ' + this.config.timeout, + 'We changed time format to nginx-like one', + '(see http://wiki.nginx.org/ConfigNotation)', + 'so please update your config accordingly' ].join('\n')) + } + + // a bunch of different configurable timers + this.maxage = parse_interval(config_get('maxage' , '2m' )) + this.timeout = parse_interval(config_get('timeout' , '30s')) + this.max_fails = Number(config_get('max_fails' , 2 )) + this.fail_timeout = parse_interval(config_get('fail_timeout', '5m' )) + return this + + // just a helper (`config[key] || default` doesn't work because of zeroes) + function config_get(key, def) { + return config[key] != null ? config[key] : def + } + } +} + + Storage.prototype.request = function(options, cb) { if (!this.status_check()) { var req = new Stream.Readable() @@ -396,3 +402,6 @@ Storage.prototype._add_proxy_headers = function(req, headers) { headers['Via'] += '1.1 ' + this.server_id + ' (Verdaccio)' } + + +module.exports = Storage;