Files: 48847ef8b08eb9ce86e77cd125b703fafd601b17 / index.js
22636 bytesRaw
1 | var http = require('http') |
2 | var os = require('os') |
3 | var path = require('path') |
4 | var fs = require('fs') |
5 | var crypto = require('crypto') |
6 | var pkg = require('./package') |
7 | var semver = require('semver') |
8 | var toPull = require('stream-to-pull-stream') |
9 | var proc = require('child_process') |
10 | var pull = require('pull-stream') |
11 | |
12 | function escapeHTML(str) { |
13 | return String(str) |
14 | .replace(/</g, '<') |
15 | .replace(/>/g, '>') |
16 | } |
17 | |
18 | function 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 | |
38 | function 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 | |
50 | function 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 | |
84 | function 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 | |
98 | function formatHost(host) { |
99 | return /^[^\[]:.*:.*:/.test(host) ? '[' + host + ']' : host |
100 | } |
101 | |
102 | exports.name = 'npm-registry' |
103 | exports.version = '1.0.0' |
104 | exports.manifest = { |
105 | getAddress: 'async' |
106 | } |
107 | exports.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 | |
141 | exports.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 | |
148 | function 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 | |
161 | function 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 | |
170 | exports.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 | |
193 | function 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 | |
206 | SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype) |
207 | SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer |
208 | |
209 | SsbNpmRegistryServer.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 | |
221 | SsbNpmRegistryServer.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 | |
233 | SsbNpmRegistryServer.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 | |
242 | SsbNpmRegistryServer.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 | |
258 | SsbNpmRegistryServer.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 | |
282 | SsbNpmRegistryServer.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 | |
346 | function 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 | |
357 | Req.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 | |
371 | Req.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 | |
376 | Req.prototype.respondError = function (status, message) { |
377 | this.respond(status, {error: message}) |
378 | } |
379 | |
380 | var bootstrapName = 'ssb-npm-bootstrap' |
381 | |
382 | Req.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 | |
393 | Req.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 | |
418 | Req.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 | |
426 | Req.prototype.serveUser1 = function () { |
427 | this.respond(this.req.method === 'PUT' ? 201 : 200, {token: '1'}) |
428 | } |
429 | |
430 | function 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 | |
439 | Req.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 | |
500 | Req.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 | |
530 | var localhosts = { |
531 | '::1': true, |
532 | '127.0.0.1': true, |
533 | '::ffff:127.0.0.1': true, |
534 | } |
535 | |
536 | Req.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 | |
558 | Req.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 | |
587 | Req.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 | |
636 | Req.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 | |
648 | Req.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 | |
661 | Req.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