blob: 13e61debfd4acb43e2a4d3312c8be3b38e0785b8 [file] [log] [blame]
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
}