| module.exports = publish |
| |
| var url = require('url') |
| var semver = require('semver') |
| var crypto = require('crypto') |
| var Stream = require('stream').Stream |
| var assert = require('assert') |
| var fixer = require('normalize-package-data').fixer |
| var concat = require('concat-stream') |
| |
| function escaped (name) { |
| return name.replace('/', '%2f') |
| } |
| |
| function publish (uri, params, cb) { |
| assert(typeof uri === 'string', 'must pass registry URI to publish') |
| assert(params && typeof params === 'object', 'must pass params to publish') |
| assert(typeof cb === 'function', 'must pass callback to publish') |
| |
| var access = params.access |
| assert( |
| (!access) || ['public', 'restricted'].indexOf(access) !== -1, |
| "if present, access level must be either 'public' or 'restricted'" |
| ) |
| |
| var auth = params.auth |
| assert(auth && typeof auth === 'object', 'must pass auth to publish') |
| if (!(auth.token || |
| (auth.password && auth.username && auth.email))) { |
| var er = new Error('auth required for publishing') |
| er.code = 'ENEEDAUTH' |
| return cb(er) |
| } |
| |
| var metadata = params.metadata |
| assert( |
| metadata && typeof metadata === 'object', |
| 'must pass package metadata to publish' |
| ) |
| try { |
| fixer.fixNameField(metadata, {strict: true, allowLegacyCase: true}) |
| } catch (er) { |
| return cb(er) |
| } |
| var version = semver.clean(metadata.version) |
| if (!version) return cb(new Error('invalid semver: ' + metadata.version)) |
| metadata.version = version |
| |
| var body = params.body |
| assert(body, 'must pass package body to publish') |
| assert(body instanceof Stream, 'package body passed to publish must be a stream') |
| var client = this |
| var sink = concat(function (tarbuffer) { |
| putFirst.call(client, uri, metadata, tarbuffer, access, auth, cb) |
| }) |
| sink.on('error', cb) |
| body.pipe(sink) |
| } |
| |
| function putFirst (registry, data, tarbuffer, access, auth, cb) { |
| // optimistically try to PUT all in one single atomic thing. |
| // If 409, then GET and merge, try again. |
| // If other error, then fail. |
| |
| var root = { |
| _id: data.name, |
| name: data.name, |
| description: data.description, |
| 'dist-tags': {}, |
| versions: {}, |
| readme: data.readme || '' |
| } |
| |
| if (access) root.access = access |
| |
| if (!auth.token) { |
| root.maintainers = [{ name: auth.username, email: auth.email }] |
| data.maintainers = JSON.parse(JSON.stringify(root.maintainers)) |
| } |
| |
| root.versions[ data.version ] = data |
| var tag = data.tag || this.config.defaultTag |
| root['dist-tags'][tag] = data.version |
| |
| var tbName = data.name + '-' + data.version + '.tgz' |
| var tbURI = data.name + '/-/' + tbName |
| |
| data._id = data.name + '@' + data.version |
| data.dist = data.dist || {} |
| data.dist.shasum = crypto.createHash('sha1').update(tarbuffer).digest('hex') |
| data.dist.tarball = url.resolve(registry, tbURI) |
| .replace(/^https:\/\//, 'http://') |
| |
| root._attachments = {} |
| root._attachments[ tbName ] = { |
| 'content_type': 'application/octet-stream', |
| 'data': tarbuffer.toString('base64'), |
| 'length': tarbuffer.length |
| } |
| |
| var fixed = url.resolve(registry, escaped(data.name)) |
| var client = this |
| var options = { |
| method: 'PUT', |
| body: root, |
| auth: auth |
| } |
| this.request(fixed, options, function (er, parsed, json, res) { |
| var r409 = 'must supply latest _rev to update existing package' |
| var r409b = 'Document update conflict.' |
| var conflict = res && res.statusCode === 409 |
| if (parsed && (parsed.reason === r409 || parsed.reason === r409b)) { |
| conflict = true |
| } |
| |
| // a 409 is typical here. GET the data and merge in. |
| if (er && !conflict) { |
| client.log.error('publish', 'Failed PUT ' + (res && res.statusCode)) |
| return cb(er) |
| } |
| |
| if (!er && !conflict) return cb(er, parsed, json, res) |
| |
| // let's see what versions are already published. |
| client.request(fixed + '?write=true', { auth: auth }, function (er, current) { |
| if (er) return cb(er) |
| |
| putNext.call(client, registry, data.version, root, current, auth, cb) |
| }) |
| }) |
| } |
| |
| function putNext (registry, newVersion, root, current, auth, cb) { |
| // already have the tardata on the root object |
| // just merge in existing stuff |
| var curVers = Object.keys(current.versions || {}).map(function (v) { |
| return semver.clean(v, true) |
| }).concat(Object.keys(current.time || {}).map(function (v) { |
| if (semver.valid(v, true)) return semver.clean(v, true) |
| }).filter(function (v) { |
| return v |
| })) |
| |
| if (curVers.indexOf(newVersion) !== -1) { |
| return cb(conflictError(root.name, newVersion)) |
| } |
| |
| current.versions[newVersion] = root.versions[newVersion] |
| current._attachments = current._attachments || {} |
| for (var i in root) { |
| switch (i) { |
| // objects that copy over the new stuffs |
| case 'dist-tags': |
| case 'versions': |
| case '_attachments': |
| for (var j in root[i]) |
| current[i][j] = root[i][j] |
| break |
| |
| // ignore these |
| case 'maintainers': |
| break |
| |
| // copy |
| default: |
| current[i] = root[i] |
| } |
| } |
| var maint = JSON.parse(JSON.stringify(root.maintainers)) |
| root.versions[newVersion].maintainers = maint |
| |
| var uri = url.resolve(registry, escaped(root.name)) |
| var options = { |
| method: 'PUT', |
| body: current, |
| auth: auth |
| } |
| this.request(uri, options, cb) |
| } |
| |
| function conflictError (pkgid, version) { |
| var e = new Error('cannot modify pre-existing version') |
| e.code = 'EPUBLISHCONFLICT' |
| e.pkgid = pkgid |
| e.version = version |
| return e |
| } |