git ssb

3+

cel / ssb-npm-registry



Tree: 8a4974fd8a7728b57aaa09d797b14fb065313500

Files: 8a4974fd8a7728b57aaa09d797b14fb065313500 / index.js

28030 bytesRaw
1var http = require('http')
2var os = require('os')
3var path = require('path')
4var fs = require('fs')
5var crypto = require('crypto')
6var pkg = require('./package')
7var semver = require('semver')
8var toPull = require('stream-to-pull-stream')
9var pull = require('pull-stream')
10var tar = require('tar-stream')
11var zlib = require('zlib')
12var cat = require('pull-cat')
13var hash = require('pull-hash')
14var multicb = require('multicb')
15var memo = require('asyncmemo')
16var lru = require('hashlru')
17
18function escapeHTML(str) {
19 return String(str)
20 .replace(/</g, '&lt;')
21 .replace(/>/g, '&gt;')
22}
23
24function onceify(fn, self) {
25 var cbs = [], err, data
26 return function (cb) {
27 if (fn) {
28 cbs.push(cb)
29 fn.call(self, function (_err, _data) {
30 err = _err, data = _data
31 var _cbs = cbs
32 cbs = null
33 while (_cbs.length) _cbs.shift()(err, data)
34 })
35 fn = null
36 } else if (cbs) {
37 cbs.push(cb)
38 } else {
39 cb(err, data)
40 }
41 }
42}
43
44function once(cb) {
45 var done
46 return function (err, result) {
47 if (done) {
48 if (err) console.trace(err)
49 } else {
50 done = true
51 cb(err, result)
52 }
53 }
54}
55
56function npmLogin(registryAddress, cb) {
57 var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1'
58 var filename = path.join(os.homedir(), '.npmrc')
59 fs.readFile(filename, 'utf8', function (err, data) {
60 if (err && err.code === 'ENOENT') data = ''
61 else if (err) return cb(new Error(err.stack))
62 var lines = data ? data.split('\n') : []
63 if (lines.indexOf(tokenLine) > -1) return cb()
64 var trailingNewline = (lines.length === 0 || lines[lines.length-1] === '')
65 var line = trailingNewline ? tokenLine + '\n' : '\n' + tokenLine
66 fs.appendFile(filename, line, cb)
67 })
68}
69
70function formatHost(host) {
71 return /^[^\[]:.*:.*:/.test(host) ? '[' + host + ']' : host
72}
73
74exports.name = 'npm-registry'
75exports.version = '1.0.0'
76exports.manifest = {
77 getAddress: 'async'
78}
79exports.init = function (sbot, config) {
80 var conf = config.npm || {}
81 var port = conf.port || 8043
82 var host = conf.host || null
83 var autoAuth = conf.autoAuth !== false
84
85 var server = http.createServer(exports.respond(sbot, config))
86 var getAddress = onceify(function (cb) {
87 server.on('error', cb)
88 server.listen(port, host, function () {
89 server.removeListener('error', cb)
90 var regHost = formatHost(host || 'localhost')
91 var regPort = this.address().port
92 var regUrl = 'http://' + regHost + ':' + regPort + '/'
93 if (autoAuth) npmLogin(regUrl, next)
94 else next()
95 function next(err) {
96 cb(err, regUrl)
97 }
98 })
99 sbot.on('close', function () {
100 server.close()
101 })
102 })
103
104 getAddress(function (err, addr) {
105 if (err) return console.error(err)
106 console.log('[npm-registry] Listening on ' + addr)
107 })
108
109 return {
110 getAddress: getAddress
111 }
112}
113
114exports.respond = function (sbot, config) {
115 var reg = new SsbNpmRegistryServer(sbot, config)
116 return function (req, res) {
117 new Req(reg, req, res).serve()
118 }
119}
120
121function publishMsg(sbot, value, cb) {
122 var gotExpectedPrevious = false
123 sbot.publish(value, function next(err, msg) {
124 if (err && /^expected previous:/.test(err.message)) {
125 // retry once on this error
126 if (gotExpectedPrevious) return cb(err)
127 gotExpectedPrevious = true
128 return sbot.publish(value, next)
129 }
130 cb(err, msg)
131 })
132}
133
134function getDependencyBranches(sbot, id, cb) {
135 // get ids of heads of tree of dependencyBranch message links to include all
136 // the dependencies of the given tarball id.
137 var getPackageJsonCached = memo(getPackageJsonFromTarballBlob, sbot)
138 var msgs = {}
139 var branched = {}
140 var blobs = {}
141
142 function addPkgById(id, cb) {
143 if (blobs[id]) return cb()
144 blobs[id] = true
145 getPackageJsonCached(id, function (err, pkg) {
146 if (err) return cb(err)
147 var done = multicb()
148 for (var name in pkg.dependencies || {}) {
149 addPkgBySpec(name, pkg.dependencies[name], done())
150 }
151 done(cb)
152 })
153 }
154
155 function addPkgBySpec(name, spec, cb) {
156 var versions = {}
157 var distTags = {}
158 pull(
159 getMentions(sbot.links2, {$prefix: 'npm:' + name + ':'}),
160 pull.filter(mentionMatches(null, name, spec)),
161 pull.map(function (mention) {
162 // query to get the messages since links2 does not include the msg id
163 return sbot.links({
164 source: mention.author,
165 dest: mention.link,
166 rel: 'mentions',
167 values: true,
168 })
169 }),
170 pull.flatten(),
171 pull.drain(function (msg) {
172 var c = msg && msg.value && msg.value.content
173 if (!c || !Array.isArray(c.mentions)) return
174 c.mentions.forEach(function (link) {
175 var data = link && link.name && decodeName(link.name)
176 if (!data || data.name !== name) return
177 versions[data.version] = {msg: msg, mention: link, mentionData: data}
178 if (data.distTag) distTags[data.distTag] = data.version
179 })
180 }, function (err) {
181 if (err) return cb(err)
182 var version = distTags[spec]
183 || semver.maxSatisfying(Object.keys(versions), spec)
184 var item = versions[version]
185 if (!item) return cb(new Error('Dependency version not found: ' + name + '@' + spec))
186 msgs[item.msg.key] = item.msg.value
187 var c = item.msg.value.content
188 if (Array.isArray(c.dependencyBranch)) {
189 for (var k = 0; k < c.dependencyBranch.length; k++) {
190 branched[c.dependencyBranch[k]] = true
191 }
192 }
193 // console.log('add', item.msg.key, item.mentionData.name, item.mentionData.version)
194 addPkgById(item.mention.link, cb)
195 })
196 )
197 }
198
199 addPkgById(id, function (err) {
200 if (err) return cb(err)
201 var ids = []
202 for (var key in msgs) {
203 if (!branched[key]) ids.push(key)
204 }
205 cb(null, ids)
206 })
207}
208
209function packageLinks(sbot, feed, id, name, spec) {
210 var matches = mentionMatches(id, name, spec)
211 return pull(
212 sbot.links({
213 source: feed,
214 dest: id,
215 rel: 'mentions',
216 values: true,
217 }),
218 pull.filter(function (msg) {
219 var c = msg && msg.value && msg.value.content
220 return c && Array.isArray(c.mentions) && c.mentions.some(matches)
221 })
222 )
223}
224
225function getVersionBranches(sbot, link, cb) {
226 var data = decodeName(link.name)
227 var msgs = {}, branched = {}
228 pull(
229 getMentions(sbot.links2, {$prefix: 'npm:' + data.name + ':'}),
230 pull.map(function (mention) {
231 return packageLinks(sbot, mention.author, mention.link, data.name)
232 }),
233 pull.flatten(),
234 pull.drain(function (msg) {
235 var c = msg && msg.value && msg.value.content
236 if (!c) return
237 msgs[msg.key] = msg.value
238 if (Array.isArray(c.versionBranch)) {
239 for (var k = 0; k < c.versionBranch.length; k++) {
240 branched[c.versionBranch[k]] = true
241 }
242 }
243 }, function (err) {
244 if (err) return cb(err)
245 var ids = []
246 for (var key in msgs) {
247 if (!branched[key]) ids.push(key)
248 }
249 cb(null, ids)
250 })
251 )
252}
253
254// For each dependency that is not a bundledDependency, get message ids for
255// that dependency name + version.
256function publishSingleMention(sbot, mention, cb) {
257 if (!sbot.links2) return cb(new Error('ssb-links scuttlebot plugin is required to publish ssb-npm packages'))
258 // Calculate dependencyBranch and versionBranch message ids.
259 var value = {
260 type: 'npm-packages',
261 mentions: [mention]
262 }
263 var done = multicb({pluck: 1, spread: true})
264 getDependencyBranches(sbot, mention.link, done())
265 getVersionBranches(sbot, mention, done())
266 done(function (err, dependencyBranches, versionBranches) {
267 if (err) return cb(err)
268 value.dependencyBranch = dependencyBranches || undefined
269 value.versionBranch = versionBranches || undefined
270 publishMsg(sbot, value, cb)
271 })
272}
273
274function publishMentions(sbot, mentions, cb) {
275 // console.error("publishing %s mentions", mentions.length)
276 if (mentions.length === 0) return cb(new Error('Empty mentions list'))
277 // if it is just one mention, fetch and add useful metadata
278 if (mentions.length === 1) return publishSingleMention(sbot, mentions[0], cb)
279 publishMsg(sbot, {
280 type: 'npm-packages',
281 mentions: mentions,
282 }, cb)
283}
284
285exports.publishPkgMentions = function (sbot, mentions, cb) {
286 // try to fit the mentions into as few messages as possible,
287 // while fitting under the message size limit.
288 var msgs = []
289 ;(function next(i, chunks) {
290 if (i >= mentions.length) return cb(null, msgs)
291 var chunkLen = Math.ceil(mentions.length / chunks)
292 publishMentions(sbot, mentions.slice(i, i + chunkLen), function (err, msg) {
293 if (err && /must not be large/.test(err.message)) return next(i, chunks + 1)
294 if (err && msgs.length) return onPartialPublish(err)
295 if (err) return cb(err)
296 msgs.push(msg)
297 next(i + chunkLen, chunks)
298 })
299 })(0, 1)
300 function onPartialPublish(err) {
301 var remaining = mentions.length - i
302 return cb(new Error('Published messages ' +
303 msgs.map(function (msg) { return msg.key }).join(', ') + ' ' +
304 'but failed to publish remaining ' + remaining + ': ' + (err.stack || err)))
305 }
306}
307
308exports.expandPkgMentions = function (sbot, mentions, props, cb) {
309 cb = once(cb)
310 var waiting = 0
311 var expandedMentions = mentions.map(function (link) {
312 var id = link && link.link
313 if (!id) return link
314 waiting++
315 var newLink = {}
316 for (var k in link) newLink[k] = link[k]
317 getPackageJsonFromTarballBlob(sbot, id, function (err, pkg) {
318 if (err) return cb(err)
319 for (var k in props) newLink[k] = pkg[k]
320 if (props.shasum && !pkg.shasum) newLink.shasum = pkg._shasum
321 if (!--waiting) next()
322 })
323 return newLink
324 })
325 if (!waiting) next()
326 function next() {
327 cb(null, expandedMentions)
328 }
329}
330
331function SsbNpmRegistryServer(sbot, config) {
332 this.sbot = sbot
333 this.config = config
334 this.npmConfig = config.npm || {}
335 this.host = this.npmConfig.host || 'localhost'
336 this.fetchAll = this.npmConfig.fetchAll
337 this.needShasum = this.npmConfig.needShasum
338 this.wsPort = config.ws && Number(config.ws.port) || '8989'
339 this.blobsPrefix = 'http://' + (config.host || 'localhost') + ':'
340 + this.wsPort + '/blobs/get/'
341 this.getMsg = memo({cache: lru(100)}, this.getMsg)
342}
343
344SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype)
345SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer
346
347SsbNpmRegistryServer.prototype.pushBlobs = function (ids, cb) {
348 var self = this
349 if (!self.sbot.blobs.push) return cb(new Error('missing blobs.push'))
350 ;(function next(i) {
351 if (i >= ids.length) return cb()
352 self.sbot.blobs.push(ids[i], function (err) {
353 if (err) return cb(err)
354 next(i+1)
355 })
356 })(0)
357}
358
359function getBlob(sbot, id, cb) {
360 var blobs = sbot.blobs
361 blobs.size(id, function (err, size) {
362 if (typeof size === 'number') cb(null, blobs.get(id))
363 else blobs.want(id, function (err, got) {
364 if (err) cb(err)
365 else if (!got) cb('missing blob ' + id)
366 else cb(null, blobs.get(id))
367 })
368 })
369}
370
371SsbNpmRegistryServer.prototype.blobDist = function (id) {
372 var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id)
373 if (!m) throw new Error('bad blob id: ' + id)
374 return {
375 integrity: m[2] + '-' + m[1],
376 tarball: 'http://localhost:' + this.wsPort + '/blobs/get/' + id
377 }
378}
379
380function getMentions(links2, name) {
381 return links2.read({
382 query: [
383 {$filter: {rel: ['mentions', name, {$gt: true}]}},
384 {$filter: {dest: {$prefix: '&'}}},
385 {$map: {
386 name: ['rel', 1],
387 size: ['rel', 2],
388 link: 'dest',
389 author: 'source',
390 ts: 'ts'
391 }}
392 ]
393 })
394}
395
396function mentionMatches(id, name, spec) {
397 return function (mention) {
398 var data = mention
399 && (!id || id === mention.link)
400 && mention.name
401 && decodeName(mention.name)
402 return data
403 && data.name === name
404 && (spec ? semver.satisfies(data.version, spec) : true)
405 }
406}
407
408SsbNpmRegistryServer.prototype.getMentions = function (name) {
409 if (!this.sbot.links2) return pull.empty()
410 return getMentions(this.sbot.links2, name)
411}
412
413SsbNpmRegistryServer.prototype.getMsg = function (id, cb) {
414 if (this.sbot.ooo) return this.sbot.ooo.get(id, cb)
415 else this.sbot.get(id, function (err, value) {
416 if (err) return cb(err)
417 cb(null, {key: id, value: value})
418 })
419}
420
421SsbNpmRegistryServer.prototype.streamTree = function (heads, prop) {
422 var self = this
423 var stack = heads.slice()
424 var seen = {}
425 return function (abort, cb) {
426 if (abort) return cb(abort)
427 for (var id; stack.length && seen[id = stack.pop()];);
428 if (!id) return cb(true)
429 seen[id] = true
430 // TODO: use DFS
431 self.getMsg(id, function (err, msg) {
432 if (err) return cb(new Error(err.stack || err))
433 var c = msg && msg.value && msg.value.content
434 var links = c && c[prop]
435 if (Array.isArray(links)) {
436 stack.push.apply(stack, links)
437 } else if (links) {
438 stack.push(links)
439 }
440 cb(null, msg)
441 })
442 }
443}
444
445function Req(server, req, res) {
446 this.server = server
447 this.req = req
448 this.res = res
449 this.blobsToPush = []
450 this.fetchAll = server.fetchAll != null ? server.fetchAll : true
451 var ua = this.req.headers['user-agent']
452 var m = /\bnpm\/([0-9]*)/.exec(ua)
453 var npmVersion = m && m[1]
454 this.needShasum = server.needShasum != null ? server.needShasum :
455 (npmVersion && npmVersion < 5)
456}
457
458Req.prototype.serve = function () {
459 console.log(this.req.method, this.req.url, this.req.socket.remoteAddress.replace(/^::ffff:/, ''))
460 this.res.setTimeout(0)
461 var pathname = this.req.url.replace(/\?.*/, '')
462 var m
463 if ((m = /^\/(\^|%5[Ee])?(%25.*sha256)(\/.*)$/.exec(pathname))) {
464 try {
465 this.headMsgId = decodeURIComponent(m[2])
466 this.headMsgPlus = !!m[1]
467 // ^ means also include packages published after the head message id
468 } catch(e) {
469 return this.respondError(400, e.stack || e)
470 }
471 pathname = m[3]
472 }
473 if (pathname === '/') return this.serveHome()
474 if (pathname === '/-/whoami') return this.serveWhoami()
475 if (pathname === '/-/ping') return this.respond(200, true)
476 if (pathname === '/-/user/org.couchdb.user:1') return this.serveUser1()
477 if ((m = /^\/-\/prebuild\/(.*)$/.exec(pathname))) return this.servePrebuild(m[1])
478 if (!/^\/-\//.test(pathname)) return this.servePkg(pathname.substr(1))
479 return this.respond(404)
480}
481
482Req.prototype.respond = function (status, message) {
483 this.res.writeHead(status, {'content-type': 'application/json'})
484 this.res.end(message && JSON.stringify(message, 0, 2))
485}
486
487Req.prototype.respondError = function (status, message) {
488 this.respond(status, {error: message})
489}
490
491Req.prototype.serveHome = function () {
492 var self = this
493 self.res.writeHead(200, {'content-type': 'text/html'})
494 var port = 8044
495 self.res.end('<!doctype html><html><head><meta charset=utf-8>' +
496 '<title>' + escapeHTML(pkg.name) + '</title></head><body>' +
497 '<h1>' + escapeHTML(pkg.name) + '</h1>\n' +
498 '</body></html>')
499}
500
501Req.prototype.serveWhoami = function () {
502 var self = this
503 self.server.sbot.whoami(function (err, feed) {
504 if (err) return self.respondError(err.stack || err)
505 self.respond(200, {username: feed.id})
506 })
507}
508
509Req.prototype.serveUser1 = function () {
510 this.respond(this.req.method === 'PUT' ? 201 : 200, {token: '1'})
511}
512
513function decodeName(name) {
514 var parts = String(name).replace(/\.tgz$/, '').split(':')
515 return {
516 name: parts[1],
517 version: parts[2],
518 distTag: parts[3],
519 }
520}
521
522Req.prototype.getMsgMentions = function (name) {
523 return pull(
524 this.server.streamTree([this.headMsgId], 'dependencyBranch'),
525 // decryption could be done here
526 pull.map(function (msg) {
527 var c = msg.value && msg.value.content
528 if (!c.mentions || !Array.isArray(c.mentions)) return []
529 return c.mentions.map(function (mention) {
530 return {
531 name: mention.name,
532 size: mention.size,
533 link: mention.link,
534 author: msg.value.author,
535 ts: msg.value.timestamp,
536 }
537 })
538 }),
539 pull.flatten(),
540 pull.filter(typeof name === 'string' ? function (link) {
541 return link.name === name
542 } : name && name.$prefix ? function (link) {
543 return link.name.substr(0, name.$prefix.length) === name.$prefix
544 } : function () {
545 throw new TypeError('unsupported name filter')
546 })
547 )
548}
549
550Req.prototype.getMentions = function (name) {
551 var useMsgMentions = this.headMsgId
552 var useServerMentions = !this.headMsgId || this.headMsgPlus
553 if (useServerMentions && !this.server.sbot.links2) {
554 return this.headMsgPlus
555 ? pull.error(new Error('ssb-links scuttlebot plugin is needed for ^msgid queries'))
556 : pull.error(new Error('ssb-links scuttlebot plugin is needed for non-msgid queries'))
557 }
558 return cat([
559 useMsgMentions ? this.getMsgMentions(name) : pull.empty(),
560 useServerMentions ? this.server.getMentions(name) : pull.empty()
561 ])
562}
563
564Req.prototype.getMentionLinks = function (blobId) {
565 return pull(
566 this.headMsgId
567 ? this.server.streamTree([this.headMsgId], 'dependencyBranch')
568 : this.server.sbot.links({
569 dest: blobId,
570 rel: 'mentions',
571 values: true,
572 }),
573 // decryption could be done here
574 pull.map(function (msg) {
575 var c = msg.value && msg.value.content
576 return c && Array.isArray(c.mentions) && c.mentions || []
577 }),
578 pull.flatten(),
579 pull.filter(function (link) {
580 return link && link.link === blobId
581 })
582 )
583}
584
585Req.prototype.servePkg = function (pathname) {
586 var self = this
587 var parts = pathname.split('/')
588 var pkgName = parts.shift().replace(/%2f/i, '/')
589 if (parts[0] === '-rev') return this.respondError(501, 'Unpublish is not supported')
590 var spec = parts.shift()
591 if (spec) try { spec = decodeURIComponent(spec) } finally {}
592 if (parts.length > 0) return this.respondError(404)
593 if (self.req.method === 'PUT') return self.publishPkg(pkgName)
594 var obj = {
595 _id: pkgName,
596 name: pkgName,
597 'dist-tags': {},
598 versions: {}
599 }
600 var distTags = {/* <tag>: {version, ts}*/}
601 pull(
602 self.getMentions({$prefix: 'npm:' + pkgName + ':'}),
603 pull.drain(function (mention) {
604 var data = decodeName(mention.name)
605 if (!data.version) return
606 if (data.distTag) {
607 var tag = distTags[data.distTag]
608 if (!tag || mention.ts > tag.ts) {
609 /* TODO: sort by causal order (versionBranch links) instead of just
610 * by timestamps */
611 distTags[data.distTag] = {ts: mention.ts, version: data.version}
612 }
613 }
614 obj.versions[data.version] = {
615 author: {
616 url: mention.author
617 },
618 name: pkgName,
619 version: data.version,
620 dist: self.server.blobDist(mention.link)
621 }
622 }, function (err) {
623 if (err) return self.respondError(500, err.stack || err)
624 for (var tag in distTags) {
625 obj['dist-tags'][tag] = distTags[tag].version
626 }
627 if (spec) resolveSpec()
628 else if (self.fetchAll) resolveAll()
629 else resolved()
630 })
631 )
632 function resolveSpec() {
633 var version = obj['dist-tags'][spec]
634 || semver.maxSatisfying(Object.keys(obj.versions), spec)
635 obj = obj.versions[version]
636 if (!obj) return self.respondError(404, 'version not found: ' + spec)
637 self.populatePackageJson(obj, function (err, pkg) {
638 if (err) return resolved(err)
639 obj = pkg || obj
640 resolved()
641 })
642 }
643 function resolveVersion(version, cb) {
644 self.populatePackageJson(obj.versions[version], function (err, pkg) {
645 if (err) return cb(err)
646 if (pkg) obj.versions[version] = pkg
647 cb()
648 })
649 }
650 function resolveAll() {
651 var done = multicb()
652 for (var version in obj.versions) {
653 resolveVersion(version, done())
654 }
655 done(resolved)
656 }
657 function resolved(err) {
658 if (err) return self.respondError(500, err.stack || err)
659 self.respond(200, obj)
660 }
661}
662
663Req.prototype.servePrebuild = function (name) {
664 var self = this
665 var getMention = self.getMentions('prebuild:' + name)
666 var blobsByAuthor = {/* <author>: BlobId */}
667 getMention(null, function next(err, link) {
668 if (err === true) return done()
669 if (err) return self.respondError(500, err.stack || err)
670 blobsByAuthor[link.author] = link.link
671 getMention(null, next)
672 })
673 function done() {
674 var authorsByLink = {/* <BlobId>: [FeedId...] */}
675 var blobId
676 for (var feed in blobsByAuthor) {
677 var blob = blobId = blobsByAuthor[feed]
678 var feeds = authorsByLink[blob] || (authorsByLink[blob] = [])
679 feeds.push(feed)
680 }
681 switch (Object.keys(authorsByLink).length) {
682 case 0:
683 return self.respondError(404, 'Not Found')
684 case 1:
685 self.res.writeHead(303, {Location: self.server.blobsPrefix + blobId})
686 return self.res.end()
687 default:
688 return self.respond(300, {choices: authorsByLink})
689 }
690 }
691}
692
693var localhosts = {
694 '::1': true,
695 '127.0.0.1': true,
696 '::ffff:127.0.0.1': true,
697}
698
699Req.prototype.publishPkg = function (pkgName) {
700 var self = this
701 var remoteAddress = self.req.socket.remoteAddress
702 if (!(remoteAddress in localhosts)) {
703 return self.respondError(403, 'You may not publish as this user.')
704 }
705
706 var chunks = []
707 self.req.on('data', function (data) {
708 chunks.push(data)
709 })
710 self.req.on('end', function () {
711 var data
712 try {
713 data = JSON.parse(Buffer.concat(chunks))
714 } catch(e) {
715 return self.respondError(400, e.stack)
716 }
717 return self.publishPkg2(pkgName, data || {})
718 })
719}
720
721Req.prototype.publishPkg2 = function (name, data) {
722 var self = this
723 if (data.users) console.trace('[npm-registry] users property is not supported')
724 var attachments = data._attachments || {}
725 var links = {/* <name>-<version>.tgz: {link: <BlobId>, size: number} */}
726 var done = multicb()
727 function addAttachmentAsBlob(filename, cb) {
728 var data = attachments[filename].data
729 var tarball = new Buffer(data, 'base64')
730 var length = attachments[filename].length
731 if (length && length !== tarball.length) return self.respondError(400,
732 'Length mismatch for attachment \'' + filename + '\'')
733 self.server.sbot.blobs.add(function (err, id) {
734 if (err) return cb(err)
735 self.blobsToPush.push(id)
736 var shasum = crypto.createHash('sha1').update(tarball).digest('hex')
737 links[filename] = {link: id, size: tarball.length, shasum: shasum}
738 cb()
739 })(pull.once(tarball))
740 }
741 for (var filename in attachments) {
742 addAttachmentAsBlob(filename, done())
743 }
744 done(function (err) {
745 if (err) return self.respondError(500, err.stack || err)
746 try {
747 self.publishPkg3(name, data, links)
748 } catch(e) {
749 self.respondError(500, e.stack || e)
750 }
751 })
752}
753
754Req.prototype.publishPkg3 = function (name, data, links) {
755 var self = this
756 var versions = data.versions || {}
757 var linksByVersion = {/* <version>: link */}
758
759 // associate tarball blobs with versions
760 for (var version in versions) {
761 var pkg = versions[version]
762 if (!pkg) return self.respondError(400, 'Bad package object')
763 if (!pkg.dist) return self.respondError(400, 'Missing package dist property')
764 if (!pkg.dist.tarball) return self.respondError(400, 'Missing dist.tarball property')
765 if (pkg.deprecated) return self.respondError(501, 'Deprecation is not supported')
766 var m = /\/-\/([^\/]+)$/.exec(pkg.dist.tarball)
767 if (!m) return self.respondError(400, 'Bad tarball URL \'' + pkg.dist.tarball + '\'')
768 var filename = m[1]
769 var link = links[filename]
770 if (!link) return self.respondError(501, 'Unable to find attachment \'' + filename + '\'')
771 // TODO?: try to find missing tarball mentioned in other messages
772 if (pkg.version && pkg.version !== version)
773 return self.respondError(400, 'Mismatched package version: ' + [pkg.version, version])
774 linksByVersion[version] = link
775 link.version = version
776 link.dependencies = pkg.dependencies || {}
777 link.bundledDependencies = pkg.bundledDependencies || pkg.bundleDependencies
778 }
779
780 // associate blobs with dist-tags
781 var tags = data['dist-tags'] || {}
782 for (var tag in tags) {
783 var version = tags[tag]
784 var link = linksByVersion[version]
785 if (!link) return self.respondError(501, 'Setting a dist-tag for a version not being published is not supported.')
786 // TODO?: support setting dist-tag without version,
787 // by looking up a tarball blob for the version
788 link.tag = tag
789 }
790
791 // compute blob links to publish
792 var mentions = []
793 for (var filename in links) {
794 var link = links[filename] || {}
795 if (!link.version) return self.respondError(400, 'Attachment ' + filename + ' was not linked to in the package metadata')
796 mentions.push({
797 name: 'npm:' + name + ':' + link.version + (link.tag ? ':' + link.tag : ''),
798 link: link.link,
799 size: link.size,
800 shasum: link.shasum,
801 dependencies: link.dependencies,
802 bundledDependencies: link.bundledDependencies,
803 })
804 }
805 return self.publishPkgs(mentions)
806}
807
808Req.prototype.publishPkgs = function (mentions) {
809 var self = this
810 exports.publishPkgMentions(self.server.sbot, mentions, function (err, msgs) {
811 if (err) return self.respondError(500, err.stack || err)
812 self.server.pushBlobs(self.blobsToPush, function (err) {
813 if (err) console.error('[npm-registry] Failed to push blob ' + id + ': ' + (err.stack || err))
814 self.respond(201)
815 console.log(msgs.map(function (msg) { return msg.key }).join('\n'))
816 })
817 })
818}
819
820Req.prototype.populatePackageJson = function (obj, cb) {
821 var self = this
822 var blobId = obj.dist.tarball.replace(/.*\/blobs\/get\//, '')
823 var deps, bundledDeps, shasum
824
825 // look for dependencies in links.
826 // then fallback to getting it from the tarball blob
827
828 pull(
829 self.getMentionLinks(blobId),
830 pull.drain(function (link) {
831 if (link.dependencies) deps = link.dependencies
832 if (link.shasum) shasum = link.shasum
833 bundledDeps = link.bundledDependencies || link.bundleDependencies || bundledDeps
834 // how to handle multiple assignments of dependencies to a package?
835 }, function (err) {
836 if (err) return cb(new Error(err.stack || err))
837 if (deps && (shasum || !self.needShasum)) {
838 // assume that the dependencies in the links to the blob are
839 // correct.
840 obj.dependencies = deps
841 obj.bundledDependencies = bundledDeps
842 if (shasum) obj.dist.shasum = obj._shasum = shasum
843 cb(null, obj)
844 } else {
845 // get dependencies from the tarball
846 getPackageJsonFromTarballBlob(self.server.sbot, blobId, function (err, pkg) {
847 if (err) return cb(err)
848 pkg.dist = obj.dist
849 pkg.dist.shasum = pkg._shasum
850 pkg.author = pkg.author || obj.author
851 pkg.version = pkg.version || obj.version
852 pkg.name = pkg.name || obj.name
853 cb(null, pkg)
854 })
855 }
856 })
857 )
858}
859
860function getPackageJsonFromTarballBlob(sbot, id, cb) {
861 var self = this
862 getBlob(sbot, id, function (err, readBlob) {
863 if (err) return cb(err)
864 cb = once(cb)
865 var extract = tar.extract()
866 var pkg, shasum
867 extract.on('entry', function (header, stream, next) {
868 if (/^[^\/]*\/package\.json$/.test(header.name)) {
869 pull(toPull.source(stream), pull.collect(function (err, bufs) {
870 if (err) return cb(err)
871 try { pkg = JSON.parse(Buffer.concat(bufs)) }
872 catch(e) { return cb(e) }
873 next()
874 }))
875 } else {
876 stream.on('end', next)
877 stream.resume()
878 }
879 })
880 extract.on('finish', function () {
881 pkg._shasum = shasum
882 cb(null, pkg)
883 })
884 pull(
885 readBlob,
886 hash('sha1', 'hex', function (err, sum) {
887 if (err) return cb(err)
888 shasum = sum
889 }),
890 toPull(zlib.createGunzip()),
891 toPull(extract)
892 )
893 })
894}
895

Built with git-ssb-web