--- /dev/null
+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
+}