git ssb

3+

cel / ssb-npm-registry



Tree: 48847ef8b08eb9ce86e77cd125b703fafd601b17

Files: 48847ef8b08eb9ce86e77cd125b703fafd601b17 / index.js

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

Built with git-ssb-web