git ssb

3+

cel / ssb-npm-registry



Tree: c0b7765bc6ffe86bccfe01e9decf55b3794d7b38

Files: c0b7765bc6ffe86bccfe01e9decf55b3794d7b38 / index.js

22510 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 hash = require('pull-hash')
13
14function escapeHTML(str) {
15 return String(str)
16 .replace(/</g, '&lt;')
17 .replace(/>/g, '&gt;')
18}
19
20function onceify(fn, self) {
21 var cbs = [], err, data
22 return function (cb) {
23 if (fn) {
24 cbs.push(cb)
25 fn.call(self, function (_err, _data) {
26 err = _err, data = _data
27 var _cbs = cbs
28 cbs = null
29 while (_cbs.length) _cbs.shift()(err, data)
30 })
31 fn = null
32 } else if (cbs) {
33 cbs.push(cb)
34 } else {
35 cb(err, data)
36 }
37 }
38}
39
40function once(cb) {
41 var done
42 return function (err, result) {
43 if (done) {
44 if (err) console.trace(err)
45 } else {
46 done = true
47 cb(err, result)
48 }
49 }
50}
51
52function pkgLockToRegistryPkgs(pkgLock, wsPort) {
53 // convert a package-lock.json file into data for serving as an npm registry
54 var hasNonBlobUrl = false
55 var blobUrlRegex = new RegExp('^http://localhost:' + wsPort + '/blobs/get/&')
56 var pkgs = {}
57 var queue = [pkgLock, pkgLock.name]
58 while (queue.length) {
59 var dep = queue.shift(), name = queue.shift()
60 if (name) {
61 var pkg = pkgs[name] || (pkgs[name] = {
62 _id: name,
63 name: name,
64 versions: {}
65 })
66 if (dep.version && dep.integrity && dep.resolved) {
67 if (!hasNonBlobUrl && !blobUrlRegex.test(dep.resolved)) hasNonBlobUrl = true
68 pkg.versions[dep.version] = {
69 name: name,
70 version: dep.version,
71 dist: {
72 integrity: dep.integrity,
73 tarball: dep.resolved
74 }
75 }
76 }
77 }
78 if (dep.dependencies) for (var depName in dep.dependencies) {
79 queue.push(dep.dependencies[depName], depName)
80 }
81 }
82 pkgs._hasNonBlobUrl = hasNonBlobUrl
83 return pkgs
84}
85
86function npmLogin(registryAddress, cb) {
87 var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1'
88 var filename = path.join(os.homedir(), '.npmrc')
89 fs.readFile(filename, 'utf8', function (err, data) {
90 if (err && err.code === 'ENOENT') data = ''
91 else if (err) return cb(new Error(err.stack))
92 var lines = data ? data.split('\n') : []
93 if (lines.indexOf(tokenLine) > -1) return cb()
94 var trailingNewline = (lines.length === 0 || lines[lines.length-1] === '')
95 var line = trailingNewline ? tokenLine + '\n' : '\n' + tokenLine
96 fs.appendFile(filename, line, cb)
97 })
98}
99
100function formatHost(host) {
101 return /^[^\[]:.*:.*:/.test(host) ? '[' + host + ']' : host
102}
103
104exports.name = 'npm-registry'
105exports.version = '1.0.0'
106exports.manifest = {
107 getAddress: 'async'
108}
109exports.init = function (sbot, config) {
110 var conf = config.npm || {}
111 var port = conf.port || 8043
112 var host = conf.host || null
113 var autoAuth = conf.autoAuth !== false
114
115 var server = http.createServer(exports.respond(sbot, config))
116 var getAddress = onceify(function (cb) {
117 server.on('error', cb)
118 server.listen(port, host, function () {
119 server.removeListener('error', cb)
120 var regHost = formatHost(host || 'localhost')
121 var regPort = this.address().port
122 var regUrl = 'http://' + regHost + ':' + regPort + '/'
123 if (autoAuth) npmLogin(regUrl, next)
124 else next()
125 function next(err) {
126 cb(err, regUrl)
127 }
128 })
129 sbot.on('close', function () {
130 server.close()
131 })
132 })
133
134 getAddress(function (err, addr) {
135 if (err) return console.error(err)
136 console.log('[npm-registry] Listening on ' + addr)
137 })
138
139 return {
140 getAddress: getAddress
141 }
142}
143
144exports.respond = function (sbot, config) {
145 var reg = new SsbNpmRegistryServer(sbot, config)
146 return function (req, res) {
147 new Req(reg, req, res).serve()
148 }
149}
150
151function publishMsg(sbot, value, cb) {
152 var gotExpectedPrevious = false
153 sbot.publish(value, function next(err, msg) {
154 if (err && /^expected previous:/.test(err.message)) {
155 // retry once on this error
156 if (gotExpectedPrevious) return cb(err)
157 gotExpectedPrevious = true
158 return sbot.publish(value, next)
159 }
160 cb(err, msg)
161 })
162}
163
164function publishMentions(sbot, mentions, cb) {
165 // console.error("publishing %s mentions", mentions.length)
166 if (mentions.length === 0) return cb(new Error('Empty mentions list'))
167 publishMsg(sbot, {
168 type: 'npm-packages',
169 mentions: mentions,
170 }, cb)
171}
172
173exports.publishPkgMentions = function (sbot, mentions, cb) {
174 // try to fit the mentions into as few messages as possible,
175 // while fitting under the message size limit.
176 var msgs = []
177 ;(function next(i, chunks) {
178 if (i >= mentions.length) return cb(null, msgs)
179 var chunkLen = Math.ceil(mentions.length / chunks)
180 publishMentions(sbot, mentions.slice(i, i + chunkLen), function (err, msg) {
181 if (err && /must not be large/.test(err.message)) return next(i, chunks + 1)
182 if (err && msgs.length) return onPartialPublish(err)
183 if (err) return cb(err)
184 msgs.push(msg)
185 next(i + chunkLen, chunks)
186 })
187 })(0, 1)
188 function onPartialPublish(err) {
189 var remaining = mentions.length - i
190 return cb(new Error('Published messages ' +
191 msgs.map(function (msg) { return msg.key }).join(', ') + ' ' +
192 'but failed to publish remaining ' + remaining + ': ' + (err.stack || err)))
193 }
194}
195
196function SsbNpmRegistryServer(sbot, config) {
197 this.sbot = sbot
198 this.config = config
199 this.npmConfig = config.npm || {}
200 this.host = this.npmConfig.host || 'localhost'
201 this.fetchAll = this.npmConfig.fetchAll
202 this.links2 = sbot.links2
203 if (!this.links2) throw new Error('missing ssb-links2 scuttlebot plugin')
204 this.wsPort = config.ws && Number(config.ws.port) || '8989'
205 this.blobsPrefix = 'http://' + (config.host || 'localhost') + ':'
206 + this.wsPort + '/blobs/get/'
207 this.getBootstrapInfo = onceify(this.getBootstrapInfo, this)
208}
209
210SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype)
211SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer
212
213SsbNpmRegistryServer.prototype.pushBlobs = function (ids, cb) {
214 var self = this
215 if (!self.sbot.blobs.push) return cb(new Error('missing blobs.push'))
216 ;(function next(i) {
217 if (i >= ids.length) return cb()
218 self.sbot.blobs.push(ids[i], function (err) {
219 if (err) return cb(err)
220 next(i+1)
221 })
222 })(0)
223}
224
225SsbNpmRegistryServer.prototype.getBlob = function (id, cb) {
226 var blobs = this.sbot.blobs
227 blobs.size(id, function (err, size) {
228 if (typeof size === 'number') cb(null, blobs.get(id))
229 else blobs.want(id, function (err, got) {
230 if (err) cb(err)
231 else if (!got) cb('missing blob ' + id)
232 else cb(null, blobs.get(id))
233 })
234 })
235}
236
237SsbNpmRegistryServer.prototype.blobDist = function (id) {
238 var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id)
239 if (!m) throw new Error('bad blob id: ' + id)
240 return {
241 integrity: m[2] + '-' + m[1],
242 tarball: 'http://localhost:' + this.wsPort + '/blobs/get/' + id
243 }
244}
245
246SsbNpmRegistryServer.prototype.getMentions = function (name) {
247 return this.links2.read({
248 query: [
249 {$filter: {rel: ['mentions', name, {$gt: true}]}},
250 {$filter: {dest: {$prefix: '&'}}},
251 {$map: {
252 name: ['rel', 1],
253 size: ['rel', 2],
254 link: 'dest',
255 author: 'source',
256 ts: 'ts'
257 }}
258 ]
259 })
260}
261
262SsbNpmRegistryServer.prototype.getLocalPrebuildsLinks = function (cb) {
263 var self = this
264 var prebuildsDir = path.join(os.homedir(), '.npm', '_prebuilds')
265 var ids = {}
266 var nameRegex = new RegExp('^http-' + self.host.replace(/\./g, '.') + '-(?:[0-9]+)-prebuild-(.*)$')
267 fs.readdir(prebuildsDir, function (err, filenames) {
268 if (err) return cb(new Error(err.stack || err))
269 ;(function next(i) {
270 if (i >= filenames.length) return cb(null, ids)
271 var m = nameRegex.exec(filenames[i])
272 if (!m) return next(i+1)
273 var name = m[1]
274 fs.readFile(path.join(prebuildsDir, filenames[i]), function (err, data) {
275 if (err) return cb(new Error(err.stack || err))
276 self.sbot.blobs.add(function (err, id) {
277 if (err) return cb(new Error(err.stack || err))
278 ids[name] = id
279 next(i+1)
280 })(pull.once(data))
281 })
282 })(0)
283 })
284}
285
286SsbNpmRegistryServer.prototype.getBootstrapInfo = function (cb) {
287 var self = this
288 if (!self.sbot.bootstrap) return cb(new Error('missing sbot bootstrap plugin'))
289
290 self.sbot.bootstrap.getPackageLock(function (err, sbotPkgLock) {
291 if (err) return cb(new Error(err.stack || err))
292 var pkgs = pkgLockToRegistryPkgs(sbotPkgLock, self.wsPort)
293 if (pkgs._hasNonBlobUrl) {
294 console.error('[npm-registry] Warning: package-lock.json has non-blob URLs. Bootstrap installation may not be fully peer-to-peer.')
295 }
296
297 if (!sbotPkgLock.name) console.trace('missing pkg lock name')
298 if (!sbotPkgLock.version) console.trace('missing pkg lock version')
299
300 var waiting = 2
301
302 self.sbot.blobs.add(function (err, id) {
303 if (err) return next(new Error(err.stack || err))
304 var pkg = pkgs[sbotPkgLock.name] || (pkgs[sbotPkgLock.name] = {})
305 var versions = pkg.versions || (pkg.versions = {})
306 pkg.versions[sbotPkgLock.version] = {
307 name: sbotPkgLock.name,
308 version: sbotPkgLock.version,
309 dist: self.blobDist(id)
310 }
311 var distTags = pkg['dist-tags'] || (pkg['dist-tags'] = {})
312 distTags.latest = sbotPkgLock.version
313 next()
314 })(self.sbot.bootstrap.pack())
315
316 var prebuilds
317 self.getLocalPrebuildsLinks(function (err, _prebuilds) {
318 if (err) return next(err)
319 prebuilds = _prebuilds
320 next()
321 })
322
323 function next(err) {
324 if (err) return waiting = 0, cb(err)
325 if (--waiting) return
326 fs.readFile(path.join(__dirname, 'bootstrap.js'), {
327 encoding: 'utf8'
328 }, function (err, bootstrapScript) {
329 if (err) return cb(err)
330 var script = bootstrapScript + '\n' +
331 'exports.pkgs = ' + JSON.stringify(pkgs, 0, 2) + '\n' +
332 'exports.prebuilds = ' + JSON.stringify(prebuilds, 0, 2)
333
334 self.sbot.blobs.add(function (err, id) {
335 if (err) return cb(new Error(err.stack || err))
336 var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id)
337 if (!m) return cb(new Error('bad blob id: ' + id))
338 cb(null, {
339 name: sbotPkgLock.name,
340 blob: id,
341 hashType: m[2],
342 hashBuf: Buffer.from(m[1], 'base64'),
343 })
344 })(pull.once(script))
345 })
346 }
347 })
348}
349
350function Req(server, req, res) {
351 this.server = server
352 this.req = req
353 this.res = res
354 this.blobsToPush = []
355 this.fetchAll = server.fetchAll != null ? server.fetchAll : true
356}
357
358Req.prototype.serve = function () {
359 console.log(this.req.method, this.req.url, this.req.socket.remoteAddress.replace(/^::ffff:/, ''))
360 var pathname = this.req.url.replace(/\?.*/, '')
361 var m
362 if (pathname === '/') return this.serveHome()
363 if (pathname === '/bootstrap') return this.serveBootstrap()
364 if (pathname === '/-/whoami') return this.serveWhoami()
365 if (pathname === '/-/ping') return this.respond(200, true)
366 if (pathname === '/-/user/org.couchdb.user:1') return this.serveUser1()
367 if ((m = /^\/-\/prebuild\/(.*)$/.exec(pathname))) return this.servePrebuild(m[1])
368 if (!/^\/-\//.test(pathname)) return this.servePkg(pathname.substr(1))
369 return this.respond(404)
370}
371
372Req.prototype.respond = function (status, message) {
373 this.res.writeHead(status, {'content-type': 'application/json'})
374 this.res.end(message && JSON.stringify(message, 0, 2))
375}
376
377Req.prototype.respondError = function (status, message) {
378 this.respond(status, {error: message})
379}
380
381var bootstrapName = 'ssb-npm-bootstrap'
382
383Req.prototype.serveHome = function () {
384 var self = this
385 self.res.writeHead(200, {'content-type': 'text/html'})
386 var port = 8044
387 self.res.end('<!doctype html><html><head><meta charset=utf-8>' +
388 '<title>' + escapeHTML(pkg.name) + '</title></head><body>' +
389 '<h1>' + escapeHTML(pkg.name) + '</h1>\n' +
390 '<p><a href="/bootstrap">Bootstrap</a></p>\n' +
391 '</body></html>')
392}
393
394Req.prototype.serveBootstrap = function () {
395 var self = this
396 self.server.getBootstrapInfo(function (err, info) {
397 if (err) return self.respondError(err.stack || err)
398 var pkgNameText = info.name
399 var pkgTmpText = '/tmp/' + bootstrapName + '.js'
400 var host = String(self.req.headers.host).replace(/:[0-9]*$/, '') || self.req.socket.localAddress
401 var httpHost = /^[^\[]:.*:.*:/.test(host) ? '[' + host + ']' : host
402 var blobsHostname = httpHost + ':' + self.server.wsPort
403 var tarballLink = 'http://' + blobsHostname + '/blobs/get/' + info.blob
404 var pkgHashText = info.hashBuf.toString('hex')
405 var hashCmd = info.hashType + 'sum'
406
407 var script =
408 'wget \'' + tarballLink + '\' -O ' + pkgTmpText + ' &&\n' +
409 'echo ' + pkgHashText + ' ' + pkgTmpText + ' | ' + hashCmd + ' -c &&\n' +
410 'node ' + pkgTmpText + ' --blobs-remote ' + blobsHostname + ' -- ' +
411 'npm install -g ' + info.name + ' &&\n' +
412 'sbot server'
413
414 self.res.writeHead(200, {'content-type': 'text/plain'})
415 self.res.end(script)
416 })
417}
418
419Req.prototype.serveWhoami = function () {
420 var self = this
421 self.server.sbot.whoami(function (err, feed) {
422 if (err) return self.respondError(err.stack || err)
423 self.respond(200, {username: feed.id})
424 })
425}
426
427Req.prototype.serveUser1 = function () {
428 this.respond(this.req.method === 'PUT' ? 201 : 200, {token: '1'})
429}
430
431function decodeName(name) {
432 var parts = name.replace(/\.tgz$/, '').split(':')
433 return {
434 name: parts[1],
435 version: parts[2],
436 distTag: parts[3],
437 }
438}
439
440Req.prototype.servePkg = function (pathname) {
441 var self = this
442 var parts = pathname.split('/')
443 var pkgName = parts.shift()
444 if (parts[0] === '-rev') return this.respondError(501, 'Unpublish is not supported')
445 var spec = parts.shift()
446 if (spec) try { spec = decodeURIComponent(spec) } finally {}
447 if (parts.length > 0) return this.respondError(404)
448 if (self.req.method === 'PUT') return self.publishPkg(pkgName)
449 var obj = {
450 _id: pkgName,
451 name: pkgName,
452 'dist-tags': {},
453 versions: {}
454 }
455 pull(
456 self.server.getMentions({$prefix: 'npm:' + pkgName + ':'}),
457 pull.drain(function (mention) {
458 var data = decodeName(mention.name)
459 if (!data.version) return
460 if (data.distTag) obj['dist-tags'][data.distTag] = data.version
461 obj.versions[data.version] = {
462 author: {
463 url: mention.author
464 },
465 name: pkgName,
466 version: data.version,
467 dist: self.server.blobDist(mention.link)
468 }
469 }, function (err) {
470 if (err) return self.respondError(500, err.stack || err)
471 if (spec) resolveSpec()
472 else if (self.fetchAll) resolveAll()
473 else done()
474 })
475 )
476 function resolveSpec() {
477 var version = obj['dist-tags'][spec]
478 || semver.maxSatisfying(Object.keys(obj.versions), spec)
479 obj = obj.versions[version]
480 if (!obj) return self.respondError(404, 'version not found: ' + spec)
481 self.populatePackageJson(obj, function (err, pkg) {
482 if (err) return self.respondError(500, err.stack || err)
483 obj = pkg || obj
484 done()
485 })
486 }
487 function resolveAll() {
488 var waiting = 0
489 for (var version in obj.versions) (function (version) {
490 waiting++
491 self.populatePackageJson(obj.versions[version], function (err, pkg) {
492 if (err && waiting <= 0) return console.trace(err)
493 if (err) return waiting = 0, self.respondError(500, err.stack || err)
494 if (pkg) obj.versions[version] = pkg
495 if (!--waiting) done()
496 })
497 }(version))
498 if (!waiting) done()
499 }
500 function done() {
501 self.respond(200, obj)
502 }
503}
504
505Req.prototype.servePrebuild = function (name) {
506 var self = this
507 var getMention = self.server.getMentions('prebuild:' + name)
508 var blobsByAuthor = {/* <author>: BlobId */}
509 getMention(null, function next(err, link) {
510 if (err === true) return done()
511 if (err) return self.respondError(500, err.stack || err)
512 blobsByAuthor[link.author] = link.link
513 getMention(null, next)
514 })
515 function done() {
516 var authorsByLink = {/* <BlobId>: [FeedId...] */}
517 var blobId
518 for (var feed in blobsByAuthor) {
519 var blob = blobId = blobsByAuthor[feed]
520 var feeds = authorsByLink[blob] || (authorsByLink[blob] = [])
521 feeds.push(feed)
522 }
523 switch (Object.keys(authorsByLink).length) {
524 case 0:
525 return self.respondError(404, 'Not Found')
526 case 1:
527 self.res.writeHead(303, {Location: self.server.blobsPrefix + blobId})
528 return self.res.end()
529 default:
530 return self.respond(300, {choices: authorsByLink})
531 }
532 }
533}
534
535var localhosts = {
536 '::1': true,
537 '127.0.0.1': true,
538 '::ffff:127.0.0.1': true,
539}
540
541Req.prototype.publishPkg = function (pkgName) {
542 var self = this
543 var remoteAddress = self.req.socket.remoteAddress
544 if (!(remoteAddress in localhosts)) {
545 return self.respondError(403, 'You may not publish as this user.')
546 }
547
548 var chunks = []
549 self.req.on('data', function (data) {
550 chunks.push(data)
551 })
552 self.req.on('end', function () {
553 var data
554 try {
555 data = JSON.parse(Buffer.concat(chunks))
556 } catch(e) {
557 return self.respondError(400, e.stack)
558 }
559 return self.publishPkg2(pkgName, data || {})
560 })
561}
562
563Req.prototype.publishPkg2 = function (name, data) {
564 var self = this
565 if (data.users) console.trace('[npm-registry] users property is not supported')
566 var attachments = data._attachments || {}
567 var links = {/* <name>-<version>.tgz: {link: <BlobId>, size: number} */}
568 var waiting = 0
569 Object.keys(attachments).forEach(function (filename) {
570 waiting++
571 var tarball = new Buffer(attachments[filename].data, 'base64')
572 var length = attachments[filename].length
573 if (length && length !== tarball.length) return self.respondError(400,
574 'Length mismatch for attachment \'' + filename + '\'')
575 self.server.sbot.blobs.add(function (err, id) {
576 if (err) return self.respondError(500,
577 'Adding attachment \'' + filename + '\' as blob failed')
578 self.blobsToPush.push(id)
579 links[filename] = {link: id, size: tarball.length}
580 if (!--waiting) next()
581 })(pull.once(tarball))
582 })
583 function next() {
584 try {
585 self.publishPkg3(name, data, links)
586 } catch(e) {
587 self.respondError(500, e.stack || e)
588 }
589 }
590}
591
592Req.prototype.publishPkg3 = function (name, data, links) {
593 var self = this
594 var versions = data.versions || {}
595 var linksByVersion = {/* <version>: link */}
596
597 // associate tarball blobs with versions
598 for (var version in versions) {
599 var pkg = versions[version]
600 if (!pkg) return self.respondError(400, 'Bad package object')
601 if (!pkg.dist) return self.respondError(400, 'Missing package dist property')
602 if (!pkg.dist.tarball) return self.respondError(400, 'Missing dist.tarball property')
603 if (pkg.deprecated) return self.respondError(501, 'Deprecation is not supported')
604 var m = /\/-\/([^\/]+)$/.exec(pkg.dist.tarball)
605 if (!m) return self.respondError(400, 'Bad tarball URL \'' + pkg.dist.tarball + '\'')
606 var filename = m[1]
607 var link = links[filename]
608 if (!link) return self.respondError(501, 'Unable to find attachment \'' + filename + '\'')
609 // TODO?: try to find missing tarball mentioned in other messages
610 if (pkg.version && pkg.version !== version)
611 return self.respondError(400, 'Mismatched package version: ' + [pkg.version, version])
612 linksByVersion[version] = link
613 link.version = version
614 }
615
616 // associate blobs with dist-tags
617 var tags = data['dist-tags'] || {}
618 for (var tag in tags) {
619 var version = tags[tag]
620 var link = linksByVersion[version]
621 if (!link) return self.respondError(501, 'Setting a dist-tag for a version not being published is not supported.')
622 // TODO?: support setting dist-tag without version,
623 // by looking up a tarball blob for the version
624 link.tag = tag
625 }
626
627 // compute blob links to publish
628 var mentions = []
629 for (var filename in links) {
630 var link = links[filename] || {}
631 if (!link.version) return self.respondError(400, 'Attachment ' + filename + ' was not linked to in the package metadata')
632 mentions.push({
633 name: 'npm:' + name + ':' + link.version + (link.tag ? ':' + link.tag : ''),
634 link: link.link,
635 size: link.size,
636 })
637 }
638 return self.publishPkgs(mentions)
639}
640
641Req.prototype.publishPkgs = function (mentions) {
642 var self = this
643 exports.publishPkgMentions(self.server.sbot, mentions, function (err, msgs) {
644 if (err) self.respondError(500, err.stack || err)
645 self.server.pushBlobs(self.blobsToPush, function (err) {
646 if (err) console.error('[npm-registry] Failed to push blob ' + id + ': ' + (err.stack || err))
647 self.respond(201)
648 console.log(msgs.map(function (msg) { return msg.key }).join('\n'))
649 })
650 })
651}
652
653Req.prototype.populatePackageJson = function (obj, cb) {
654 var blobId = obj.dist.tarball.replace(/.*\/blobs\/get\//, '')
655 this.getPackageJsonFromTarballBlob(blobId, function (err, pkg) {
656 if (err) return cb(err)
657 pkg.dist = obj.dist
658 pkg.dist.shasum = pkg._shasum
659 pkg.author = pkg.author || obj.author
660 pkg.version = pkg.version || obj.version
661 pkg.name = pkg.name || obj.name
662 cb(null, pkg)
663 })
664}
665
666Req.prototype.getPackageJsonFromTarballBlob = function (id, cb) {
667 var self = this
668 self.server.getBlob(id, function (err, readBlob) {
669 if (err) return cb(err)
670 cb = once(cb)
671 var extract = tar.extract()
672 var pkg, shasum
673 extract.on('entry', function (header, stream, next) {
674 if (/^[^\/]*\/package\.json$/.test(header.name)) {
675 pull(toPull.source(stream), pull.collect(function (err, bufs) {
676 if (err) return cb(err)
677 try { pkg = JSON.parse(Buffer.concat(bufs)) }
678 catch(e) { return cb(e) }
679 next()
680 }))
681 } else {
682 stream.on('end', next)
683 stream.resume()
684 }
685 })
686 extract.on('finish', function () {
687 pkg._shasum = shasum
688 cb(null, pkg)
689 })
690 pull(
691 readBlob,
692 hash('sha1', 'hex', function (err, sum) {
693 if (err) return cb(err)
694 shasum = sum
695 }),
696 toPull(zlib.createGunzip()),
697 toPull(extract)
698 )
699 })
700}
701

Built with git-ssb-web