git ssb

3+

cel / ssb-npm-registry



Tree: bbb627c630157a93529a060120ba79e391daa5d6

Files: bbb627c630157a93529a060120ba79e391daa5d6 / index.js

10669 bytesRaw
1var http = require('http')
2var os = require('os')
3var path = require('path')
4var fs = require('fs')
5
6function pullOnce(data) {
7 var ended
8 return function (abort, cb) {
9 if (ended || (ended = abort)) return cb(ended)
10 ended = true
11 cb(null, data)
12 }
13}
14
15exports.name = 'npm-registry'
16exports.version = '1.0.0'
17exports.manifest = {
18 getAddress: 'async'
19}
20exports.init = function (sbot, config) {
21 var port = config.npm ? config.npm.port : 8043
22 var host = config.npm && config.npm.host || 'localhost'
23 var registryAddress
24 var getAddressCbs = []
25
26 var server = http.createServer(exports.respond(sbot, config))
27 server.listen(port, host, function () {
28 registryAddress = 'http://' + host + ':' + this.address().port + '/'
29 if (config.npm && config.npm.autoAuth !== false) login()
30 console.log('[npm-registry] Listening on ' + registryAddress)
31 while (getAddressCbs.length) getAddressCbs.shift()(null, registryAddress)
32 })
33 sbot.on('close', function () {
34 server.close()
35 })
36
37 function login() {
38 var filename = path.join(os.homedir(), '.npmrc')
39 var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1'
40 var lines = fs.readFileSync(filename, 'utf8').split('\n')
41 if (lines.indexOf(tokenLine) === -1) {
42 fs.appendFileSync(filename, (lines.pop() ? '' : '\n') + tokenLine)
43 }
44 }
45
46 return {
47 getAddress: function (cb) {
48 if (registryAddress) cb(null, registryAddress)
49 else getAddressCbs.push(cb)
50 }
51 }
52}
53
54exports.respond = function (sbot, config) {
55 var reg = new SsbNpmRegistryServer(sbot, config)
56 return function (req, res) {
57 new Req(reg, req, res).serve()
58 }
59}
60
61function publishMsg(sbot, value, cb) {
62 var gotExpectedPrevious = false
63 sbot.publish(value, function next(err, msg) {
64 if (err && /^expected previous:/.test(err.message)) {
65 // retry once on this error
66 if (gotExpectedPrevious) return cb(err)
67 gotExpectedPrevious = true
68 return sbot.publish(value, next)
69 }
70 cb(err, msg)
71 })
72}
73
74function publishMentions(sbot, mentions, cb) {
75 // console.error("publishing %s mentions", mentions.length)
76 if (mentions.length === 0) return cb(new Error('Empty mentions list'))
77 publishMsg(sbot, {
78 type: 'npm-packages',
79 mentions: mentions,
80 }, cb)
81}
82
83exports.publishPkgMentions = function (sbot, mentions, cb) {
84 // try to fit the mentions into as few messages as possible,
85 // while fitting under the message size limit.
86 var msgs = []
87 ;(function next(i, chunks) {
88 if (i >= mentions.length) return cb(null, msgs)
89 var chunkLen = Math.ceil(mentions.length / chunks)
90 publishMentions(sbot, mentions.slice(i, i + chunkLen), function (err, msg) {
91 if (err && /must not be large/.test(err.message)) return next(i, chunks + 1)
92 if (err && msgs.length) return onPartialPublish(err)
93 if (err) return cb(err)
94 msgs.push(msg)
95 next(i + chunkLen, chunks)
96 })
97 })(0, 1)
98 function onPartialPublish(err) {
99 var remaining = mentions.length - i
100 return cb(new Error('Published messages ' +
101 msgs.map(function (msg) { return msg.key }).join(', ') + ' ' +
102 'but failed to publish remaining ' + remaining + ': ' + (err.stack || err)))
103 }
104}
105
106function SsbNpmRegistryServer(sbot, config) {
107 this.sbot = sbot
108 this.config = config
109 this.links2 = sbot.links2
110 if (!this.links2) throw new Error('missing ssb-links2 scuttlebot plugin')
111 this.blobsPrefix = 'http://' + (config.host || 'localhost') + ':'
112 + (config.ws && config.ws.port || '8989') + '/blobs/get/'
113}
114
115SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype)
116SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer
117
118SsbNpmRegistryServer.prototype.pushBlobs = function (ids, cb) {
119 var self = this
120 if (!self.sbot.blobs.push) return cb(new Error('missing blobs.push'))
121 ;(function next(i) {
122 if (i >= ids.length) return cb()
123 self.sbot.blobs.push(ids[i], function (err) {
124 if (err) return cb(err)
125 next(i+1)
126 })
127 })(0)
128}
129
130SsbNpmRegistryServer.prototype.blobUrl = function (id) {
131 return this.blobsPrefix + id
132}
133
134SsbNpmRegistryServer.prototype.getMentions = function (name) {
135 return this.links2.read({
136 query: [
137 {$filter: {rel: ['mentions', name]}},
138 {$filter: {dest: {$prefix: '&'}}},
139 {$map: {
140 name: ['rel', 1],
141 size: ['rel', 2],
142 link: 'dest',
143 author: 'source',
144 ts: 'ts'
145 }}
146 ]
147 })
148}
149
150function Req(server, req, res) {
151 this.server = server
152 this.req = req
153 this.res = res
154 this.blobsToPush = []
155}
156
157Req.prototype.serve = function () {
158 // console.log(this.req.method, this.req.url)
159 var pathname = this.req.url.replace(/\?.*/, '')
160 if (pathname === '/-/whoami') return this.serveWhoami()
161 if (pathname === '/-/ping') return this.respond(200, true)
162 if (!/^\/-\//.test(pathname)) return this.servePkg(pathname.substr(1))
163 return this.respond(404)
164}
165
166Req.prototype.respond = function (status, message) {
167 this.res.writeHead(status, {'content-type': 'application/json'})
168 this.res.end(message && JSON.stringify(message))
169}
170
171Req.prototype.respondError = function (status, message) {
172 this.respond(status, {error: message})
173}
174
175Req.prototype.serveWhoami = function () {
176 var self = this
177 self.server.sbot.whoami(function (err, feed) {
178 if (err) return self.respondError(err.stack || err)
179 self.respond(200, {username: feed.id})
180 })
181}
182
183function decodeName(name) {
184 var parts = name.replace(/\.tgz$/, '').split(':')
185 return {
186 name: parts[1],
187 version: parts[2],
188 distTag: parts[3],
189 }
190}
191
192Req.prototype.servePkg = function (pathname) {
193 var self = this
194 var parts = pathname.split('/')
195 var pkgName = parts.shift()
196 if (parts[0] === '-rev') return this.respondError(501, 'Unpublish is not supported')
197 if (parts.length > 0) return this.respondError(404)
198 if (self.req.method === 'PUT') return self.publishPkg(pkgName)
199 var obj = {
200 _id: pkgName,
201 name: pkgName,
202 'dist-tags': {},
203 versions: {}
204 }
205 var oldest, newest
206 var getMention = self.server.getMentions({$prefix: 'npm:' + pkgName + ':'})
207 getMention(null, function next(err, mention) {
208 if (err === true) return self.respond(200, obj)
209 if (err) return self.respondError(500, err.stack || err)
210 var data = decodeName(mention.name)
211 if (!data.version) return
212 var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(mention.link)
213 if (!m) return
214 if (data.distTag) obj['dist-tags'][data.distTag] = data.version
215 obj.versions[data.version] = {
216 author: {
217 url: mention.author
218 },
219 dist: {
220 integrity: m[2] + '-' + m[1],
221 tarball: self.server.blobUrl(mention.link)
222 }
223 }
224 getMention(null, next)
225 })
226}
227
228Req.prototype.publishPkg = function (pkgName) {
229 var chunks = []
230 var self = this
231 self.req.on('data', function (data) {
232 chunks.push(data)
233 })
234 self.req.on('end', function () {
235 var data
236 try {
237 data = JSON.parse(Buffer.concat(chunks))
238 } catch(e) {
239 return self.respondError(400, e.stack)
240 }
241 return self.publishPkg2(pkgName, data || {})
242 })
243}
244
245Req.prototype.publishPkg2 = function (name, data) {
246 var self = this
247 if (data.users) console.trace('[npm-registry users property is not supported')
248 var attachments = data._attachments || {}
249 var links = {/* <name>-<version>.tgz: {link: <BlobId>, size: number} */}
250 var waiting = 0
251 Object.keys(attachments).forEach(function (filename) {
252 waiting++
253 var tarball = new Buffer(attachments[filename].data, 'base64')
254 var length = attachments[filename].length
255 if (length && length !== tarball.length) return self.respondError(400,
256 'Length mismatch for attachment \'' + filename + '\'')
257 self.server.sbot.blobs.add(function (err, id) {
258 if (err) return self.respondError(500,
259 'Adding attachment \'' + filename + '\' as blob failed')
260 self.blobsToPush.push(id)
261 links[filename] = {link: id, size: tarball.length}
262 if (!--waiting) next()
263 })(pullOnce(tarball))
264 })
265 function next() {
266 try {
267 self.publishPkg3(name, data, links)
268 } catch(e) {
269 self.respondError(500, e.stack || e)
270 }
271 }
272}
273
274Req.prototype.publishPkg3 = function (name, data, links) {
275 var self = this
276 var versions = data.versions || {}
277 var linksByVersion = {/* <version>: link */}
278
279 // associate tarball blobs with versions
280 for (var version in versions) {
281 var pkg = versions[version]
282 if (!pkg) return self.respondError(400, 'Bad package object')
283 if (!pkg.dist) return self.respondError(400, 'Missing package dist property')
284 if (!pkg.dist.tarball) return self.respondError(400, 'Missing dist.tarball property')
285 if (pkg.deprecated) return self.respondError(501, 'Deprecation is not supported')
286 var m = /\/-\/([^\/]+)$/.exec(pkg.dist.tarball)
287 if (!m) return self.respondError(400, 'Bad tarball URL \'' + pkg.dist.tarball + '\'')
288 var filename = m[1]
289 var link = links[filename]
290 if (!link) return self.respondError(501, 'Unable to find attachment \'' + filename + '\'')
291 // TODO?: try to find missing tarball mentioned in other messages
292 if (pkg.version && pkg.version !== version)
293 return self.respondError(400, 'Mismatched package version: ' + [pkg.version, version])
294 linksByVersion[version] = link
295 link.version = version
296 }
297
298 // associate blobs with dist-tags
299 var tags = data['dist-tags'] || {}
300 for (var tag in tags) {
301 var version = tags[tag]
302 var link = linksByVersion[version]
303 if (!link) return self.respondError(501, 'Setting a dist-tag for a version not being published is not supported.')
304 // TODO?: support setting dist-tag without version,
305 // by looking up a tarball blob for the version
306 link.tag = tag
307 }
308
309 // compute blob links to publish
310 var mentions = []
311 for (var filename in links) {
312 var link = links[filename] || {}
313 if (!link.version) return self.respondError(400, 'Attachment ' + filename + ' was not linked to in the package metadata')
314 mentions.push({
315 name: 'npm:' + name + ':' + link.version + (link.tag ? ':' + link.tag : ''),
316 link: link.link,
317 size: link.size,
318 })
319 }
320 return self.publishPkgs(mentions)
321}
322
323Req.prototype.publishPkgs = function (mentions) {
324 var self = this
325 exports.publishPkgMentions(self.server.sbot, mentions, function (err, msgs) {
326 if (err) self.respondError(500, err.stack || err)
327 self.server.pushBlobs(self.blobsToPush, function (err) {
328 if (err) console.error('[npm-registry] Failed to push blob ' + id + ': ' + (err.stack || err))
329 self.respond(201)
330 console.log(msgs.map(function (msg) { return msg.key }).join('\n'))
331 })
332 })
333}
334

Built with git-ssb-web