git ssb

30+

cel / git-ssb-web



Tree: 26e05692a6d576fbcd9a6e5099e11a1bd9e19f94

Files: 26e05692a6d576fbcd9a6e5099e11a1bd9e19f94 / index.js

25695 bytesRaw
1var fs = require('fs')
2var http = require('http')
3var path = require('path')
4var url = require('url')
5var qs = require('querystring')
6var ref = require('ssb-ref')
7var pull = require('pull-stream')
8var ssbGit = require('ssb-git-repo')
9var toPull = require('stream-to-pull-stream')
10var cat = require('pull-cat')
11var Repo = require('pull-git-repo')
12var ssbAbout = require('./about')
13var ssbVotes = require('./votes')
14var marked = require('ssb-marked')
15var asyncMemo = require('asyncmemo')
16var multicb = require('multicb')
17var schemas = require('ssb-msg-schemas')
18
19marked.setOptions({
20 gfm: true,
21 mentions: true,
22 tables: true,
23 breaks: true,
24 pedantic: false,
25 sanitize: true,
26 smartLists: true,
27 smartypants: false
28})
29
30function parseAddr(str, def) {
31 if (!str) return def
32 var i = str.lastIndexOf(':')
33 if (~i) return {host: str.substr(0, i), port: str.substr(i+1)}
34 if (isNaN(str)) return {host: str, port: def.port}
35 return {host: def.host, port: str}
36}
37
38function link(parts, html) {
39 var href = '/' + parts.map(encodeURIComponent).join('/')
40 var innerHTML = html || escapeHTML(parts[parts.length-1])
41 return '<a href="' + escapeHTML(href) + '">' + innerHTML + '</a>'
42}
43
44function timestamp(time) {
45 time = Number(time)
46 var d = new Date(time)
47 return '<span title="' + time + '">' + d.toLocaleString() + '</span>'
48}
49
50function pre(text) {
51 return '<pre>' + escapeHTML(text) + '</pre>'
52}
53
54function json(obj) {
55 return pre(JSON.stringify(obj, null, 2))
56}
57
58function escapeHTML(str) {
59 return String(str)
60 .replace(/&/g, '&amp;')
61 .replace(/</g, '&lt;')
62 .replace(/>/g, '&gt;')
63 .replace(/"/g, '&quot;')
64}
65
66function escapeHTMLStream() {
67 return pull.map(function (buf) {
68 return escapeHTML(buf.toString('utf8'))
69 })
70}
71
72function table(props) {
73 return function (read) {
74 return cat([
75 pull.once('<table' + (props ? ' ' + props : '') + '>'),
76 pull(
77 read,
78 pull.map(function (row) {
79 return row ? '<tr>' + row.map(function (cell) {
80 return '<td>' + cell + '</td>'
81 }).join('') + '</tr>' : ''
82 })
83 ),
84 pull.once('</table>')
85 ])
86 }
87}
88
89function readNext(fn) {
90 var next
91 return function (end, cb) {
92 if (next) return next(end, cb)
93 fn(function (err, _next) {
94 if (err) return cb(err)
95 next = _next
96 next(null, cb)
97 })
98 }
99}
100
101function readOnce(fn) {
102 var ended
103 return function (end, cb) {
104 fn(function (err, data) {
105 if (err || ended) return cb(err || ended)
106 ended = true
107 cb(null, data)
108 })
109 }
110}
111
112function tryDecodeURIComponent(str) {
113 if (!str || (str[0] == '%' && ref.isBlobId(str)))
114 return str
115 try {
116 str = decodeURIComponent(str)
117 } finally {
118 return str
119 }
120}
121
122function getRepoName(repoId, cb) {
123 // TODO: use petnames
124 cb(null, repoId.substr(0, 20) + '…')
125}
126
127var hasOwnProp = Object.prototype.hasOwnProperty
128
129function getContentType(filename) {
130 var ext = filename.split('.').pop()
131 return hasOwnProp.call(contentTypes, ext)
132 ? contentTypes[ext]
133 : 'text/plain'
134}
135
136var contentTypes = {
137 css: 'text/css'
138}
139
140var staticBase = path.join(__dirname, 'static')
141
142function readReqJSON(req, cb) {
143 pull(
144 toPull(req),
145 pull.collect(function (err, bufs) {
146 if (err) return cb(err)
147 var data
148 try {
149 data = qs.parse(Buffer.concat(bufs).toString('ascii'))
150 } catch(e) {
151 return cb(e)
152 }
153 cb(null, data)
154 })
155 )
156}
157
158var msgTypes = {
159 'git-repo': true,
160 'git-update': true
161}
162
163var refLabels = {
164 heads: 'Branches'
165}
166
167module.exports = function (opts, cb) {
168 var ssb, reconnect, myId, getRepo, getVotes, getMsg
169 var about = function (id, cb) { cb(null, {name: id}) }
170 var reqQueue = []
171 var isPublic = opts.public
172 var ssbAppname = opts.appname || 'ssb'
173
174 var addr = parseAddr(opts.listenAddr, {host: 'localhost', port: 7718})
175 http.createServer(onRequest).listen(addr.port, addr.host, onListening)
176
177 var server = {
178 setSSB: function (_ssb, _reconnect) {
179 _ssb.whoami(function (err, feed) {
180 if (err) throw err
181 ssb = _ssb
182 reconnect = _reconnect
183 myId = feed.id
184 about = ssbAbout(ssb, myId)
185 while (reqQueue.length)
186 onRequest.apply(this, reqQueue.shift())
187 getRepo = asyncMemo(function (id, cb) {
188 getMsg(id, function (err, msg) {
189 if (err) return cb(err)
190 ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb)
191 })
192 })
193 getVotes = ssbVotes(ssb)
194 getMsg = asyncMemo(ssb.get)
195 })
196 }
197 }
198
199 function onListening() {
200 var host = ~addr.host.indexOf(':') ? '[' + addr.host + ']' : addr.host
201 console.log('Listening on http://' + host + ':' + addr.port + '/')
202 cb(null, server)
203 }
204
205 /* Serving a request */
206
207 function onRequest(req, res) {
208 console.log(req.method, req.url)
209 if (!ssb) return reqQueue.push(arguments)
210 pull(
211 handleRequest(req),
212 pull.filter(function (data) {
213 if (Array.isArray(data)) {
214 res.writeHead.apply(res, data)
215 return false
216 }
217 return true
218 }),
219 toPull(res)
220 )
221 }
222
223 function handleRequest(req) {
224 var u = req._u = url.parse(req.url)
225 var dirs = u.pathname.slice(1).split(/\/+/).map(tryDecodeURIComponent)
226 var dir = dirs[0]
227 if (dir == '')
228 return serveIndex(req)
229 else if (ref.isBlobId(dir))
230 return serveBlob(req, dir)
231 else if (ref.isMsgId(dir))
232 return serveMessage(req, dir, dirs.slice(1))
233 else if (ref.isFeedId(dir))
234 return serveUserPage(dir)
235 else
236 return serveFile(req, dirs)
237 }
238
239 function serveFile(req, dirs) {
240 var filename = path.join.apply(path, [staticBase].concat(dirs))
241 // prevent escaping base dir
242 if (filename.indexOf(staticBase) !== 0)
243 return servePlainError(403, '403 Forbidden')
244
245 return readNext(function (cb) {
246 fs.stat(filename, function (err, stats) {
247 cb(null, err ?
248 err.code == 'ENOENT' ? serve404(req)
249 : servePlainError(500, err.message)
250 : 'if-modified-since' in req.headers &&
251 new Date(req.headers['if-modified-since']) >= stats.mtime ?
252 pull.once([304])
253 : stats.isDirectory() ?
254 servePlainError(403, 'Directory not listable')
255 : cat([
256 pull.once([200, {
257 'Content-Type': getContentType(filename),
258 'Content-Length': stats.size,
259 'Last-Modified': stats.mtime.toGMTString()
260 }]),
261 toPull(fs.createReadStream(filename))
262 ]))
263 })
264 })
265 }
266
267 function servePlainError(code, msg) {
268 return pull.values([
269 [code, {
270 'Content-Length': Buffer.byteLength(msg),
271 'Content-Type': 'text/plain'
272 }],
273 msg
274 ])
275 }
276
277 function serve404(req) {
278 return servePlainError(404, '404 Not Found')
279 }
280
281 function serveRedirect(path) {
282 var msg = '<!doctype><html><head><meta charset=utf-8>' +
283 '<title>Redirect</title></head>' +
284 '<body><p><a href="' + path + '">Continue</a></p></body></html>'
285 return pull.values([
286 [302, {
287 'Content-Length': Buffer.byteLength(msg),
288 'Content-Type': 'text/html',
289 Location: path
290 }],
291 msg
292 ])
293 }
294
295 function renderTry(read) {
296 var ended
297 return function (end, cb) {
298 if (ended) return cb(ended)
299 read(end, function (err, data) {
300 if (err === true)
301 cb(true)
302 else if (err) {
303 ended = true
304 cb(null,
305 '<h3>' + err.name + '</h3>' +
306 '<pre>' + escapeHTML(err.stack) + '</pre>')
307 } else
308 cb(null, data)
309 })
310 }
311 }
312
313 function serveTemplate(title, code, read) {
314 if (read === undefined) return serveTemplate.bind(this, title, code)
315 return cat([
316 pull.values([
317 [code || 200, {
318 'Content-Type': 'text/html'
319 }],
320 '<!doctype html><html><head><meta charset=utf-8>',
321 '<title>' + escapeHTML(title || 'git ssb') + '</title>',
322 '<link rel=stylesheet href="/styles.css"/>',
323 '</head>\n',
324 '<body>',
325 '<header>',
326 '<h1><a href="/">git ssb' +
327 (ssbAppname != 'ssb' ? ' <sub>' + ssbAppname + '</sub>' : '') +
328 '</a></h1>',
329 '</header>',
330 '<article>']),
331 renderTry(read),
332 pull.once('<hr/></article></body></html>')
333 ])
334 }
335
336 function serveError(err) {
337 if (err.message == 'stream is closed')
338 reconnect()
339 return pull(
340 pull.once(
341 '<h2>' + err.name + '</h3>' +
342 '<pre>' + escapeHTML(err.stack) + '</pre>'),
343 serveTemplate(err.name, 500)
344 )
345 }
346
347 /* Feed */
348
349 function renderFeed(feedId) {
350 var opts = {
351 reverse: true,
352 id: feedId
353 }
354 return pull(
355 feedId ? ssb.createUserStream(opts) : ssb.createLogStream(opts),
356 pull.filter(function (msg) {
357 return msg.value.content.type in msgTypes
358 }),
359 pull.take(20),
360 pull.asyncMap(function (msg, cb) {
361 about.getName(msg.value.author, function (err, name) {
362 if (err) return cb(err)
363 switch (msg.value.content.type) {
364 case 'git-repo': return renderRepoCreated(msg, name, cb)
365 case 'git-update': return renderUpdate(msg, name, cb)
366 }
367 })
368 })
369 )
370 }
371
372 function renderRepoCreated(msg, authorName, cb) {
373 var repoLink = link([msg.key])
374 var authorLink = link([msg.value.author], authorName)
375 cb(null, '<section class="collapse">' + timestamp(msg.value.timestamp) + '<br>' +
376 authorLink + ' created repo ' + repoLink + '</section>')
377 }
378
379 function renderUpdate(msg, authorName, cb) {
380 var repoLink = link([msg.value.content.repo])
381 var authorLink = link([msg.value.author], authorName)
382 cb(null, '<section class="collapse">' + timestamp(msg.value.timestamp) + '<br>' +
383 authorLink + ' pushed to ' + repoLink + '</section>')
384 }
385
386 /* Index */
387
388 function serveIndex() {
389 return serveTemplate('git ssb')(renderFeed())
390 }
391
392 function serveUserPage(feedId) {
393 return serveTemplate(feedId)(cat([
394 readOnce(function (cb) {
395 about.getName(feedId, function (err, name) {
396 cb(null, '<h2>' + link([feedId], name) +
397 '<code class="user-id">' + feedId + '</code></h2>')
398 })
399 }),
400 renderFeed(feedId),
401 ]))
402 }
403
404 /* Message */
405
406 function serveMessage(req, id, path) {
407 return readNext(function (cb) {
408 ssb.get(id, function (err, msg) {
409 if (err) return cb(null, serveError(err))
410 var c = msg.content || {}
411 switch (c.type) {
412 case 'git-repo':
413 return getRepo(id, function (err, repo) {
414 if (err) return cb(null, serveError(err))
415 cb(null, serveRepoPage(req, Repo(repo), path))
416 })
417 case 'git-update':
418 return getRepo(c.repo, function (err, repo) {
419 if (err) return cb(null, serveRepoNotFound(repo.id, err))
420 cb(null, serveRepoUpdate(req, Repo(repo), id, msg, path))
421 })
422 default:
423 if (ref.isMsgId(c.repo))
424 return getRepo(c.repo, function (err, repo) {
425 if (err) return cb(null, serveRepoNotFound(repo.id, err))
426 cb(null, serveRepoSomething(req, Repo(repo), id, msg, path))
427 })
428 else
429 return cb(null, serveGenericMessage(req, id, msg, path))
430 }
431 })
432 })
433 }
434
435 function serveGenericMessage(req, id, msg, path) {
436 return serveTemplate(id)(pull.once(
437 '<section><h2>' + link([id]) + '</h2>' +
438 json(msg) +
439 '</section>'))
440 }
441
442 /* Repo */
443
444 function serveRepoPage(req, repo, path) {
445 var defaultBranch = 'master'
446
447 if (req.method == 'POST') {
448 return readNext(function (cb) {
449 readReqJSON(req, function (err, data) {
450 if (data && data.vote != null) {
451 var voteValue = +data.vote || 0
452 ssb.publish(schemas.vote(repo.id, voteValue), function (err) {
453 if (err) return cb(null, serveError(err))
454 cb(null, serveRedirect(req.url))
455 })
456 } else {
457 cb(null, servePlainError(400, 'What are you trying to do?'))
458 }
459 })
460 })
461 }
462
463 var branch = path[1] || defaultBranch
464 var filePath = path.slice(2)
465 switch (path[0]) {
466 case undefined:
467 return serveRepoTree(repo, branch, [])
468 case 'activity':
469 return serveRepoActivity(repo, branch)
470 case 'commits':
471 return serveRepoCommits(repo, branch)
472 case 'commit':
473 return serveRepoCommit(repo, path[1])
474 case 'tree':
475 return serveRepoTree(repo, branch, filePath)
476 case 'blob':
477 return serveRepoBlob(repo, branch, filePath)
478 default:
479 return serve404(req)
480 }
481 }
482
483 function serveRepoNotFound(id, err) {
484 return serveTemplate('Repo not found', 404, pull.values([
485 '<h2>Repo not found</h2>',
486 '<p>Repo ' + id + ' was not found</p>',
487 '<pre>' + escapeHTML(err.stack) + '</pre>',
488 ]))
489 }
490
491 function renderRepoPage(repo, branch, body) {
492 var gitUrl = 'ssb://' + repo.id
493 var gitLink = '<input class="clone-url" readonly="readonly" ' +
494 'value="' + gitUrl + '" size="' + gitUrl.length + '" ' +
495 'onclick="this.select()"/>'
496
497 var done = multicb({ pluck: 1, spread: true })
498 getRepoName(repo.id, done())
499 about.getName(repo.feed, done())
500 getVotes(repo.id, done())
501
502 return readNext(function (cb) {
503 done(function (err, repoName, authorName, votes) {
504 if (err) return cb(null, serveError(err))
505 var upvoted = votes.upvoters[myId] > 0
506 cb(null, serveTemplate(repo.id)(cat([
507 pull.once(
508 '<div class="repo-title">' +
509 '<form class="upvotes" action="" method="post">' +
510 (isPublic
511 ? '<button disabled="disabled">✌ Dig</button> '
512 : '<input type="hidden" name="vote" value="' +
513 (upvoted ? '0' : '1') + '">' +
514 '<button type="submit">✌ ' + (upvoted ? 'Undig' : 'Dig') +
515 '</button>') +
516 '<strong>' + votes.upvotes + '</strong>' +
517 '</form>' +
518 '<h2>' + link([repo.feed], authorName) + ' / ' +
519 link([repo.id], repoName) + '</h2>' +
520 '</div><div class="repo-nav">' + link([repo.id], 'Code') +
521 link([repo.id, 'activity'], 'Activity') +
522 link([repo.id, 'commits', branch || ''], 'Commits') +
523 gitLink +
524 '</div>'),
525 body
526 ])))
527 })
528 })
529 }
530
531 function serveRepoTree(repo, rev, path) {
532 var type = repo.isCommitHash(rev) ? 'Tree' : 'Branch'
533 return renderRepoPage(repo, rev, cat([
534 pull.once('<section><h3>' + type + ': ' + rev + ' '),
535 revMenu(repo, rev),
536 pull.once('</h3>'),
537 type == 'Branch' && renderRepoLatest(repo, rev),
538 pull.once('</section><section>'),
539 renderRepoTree(repo, rev, path),
540 pull.once('</section>'),
541 renderRepoReadme(repo, rev, path)
542 ]))
543 }
544
545 /* Repo activity */
546
547 function serveRepoActivity(repo, branch) {
548 return renderRepoPage(repo, branch, cat([
549 pull.once('<h3>Activity</h3>'),
550 pull(
551 ssb.links({
552 type: 'git-update',
553 dest: repo.id,
554 source: repo.feed,
555 rel: 'repo',
556 values: true,
557 reverse: true,
558 limit: 8
559 }),
560 pull.map(renderRepoUpdate.bind(this, repo))
561 )
562 ]))
563 }
564
565 function renderRepoUpdate(repo, msg, full) {
566 var c = msg.value.content
567
568 var refs = c.refs ? Object.keys(c.refs).map(function (ref) {
569 return {name: ref, value: c.refs[ref]}
570 }) : []
571 var numObjects = c.objects ? Object.keys(c.objects).length : 0
572
573 return '<section class="collapse">' +
574 link([msg.key], new Date(msg.value.timestamp).toLocaleString()) +
575 '<br>' +
576 (numObjects ? 'Pushed ' + numObjects + ' objects<br>' : '') +
577 refs.map(function (update) {
578 var name = escapeHTML(update.name)
579 if (!update.value) {
580 return 'Deleted ' + name
581 } else {
582 var commitLink = link([repo.id, 'commit', update.value])
583 return name + ' &rarr; ' + commitLink
584 }
585 }).join('<br>') +
586 '</section>'
587 }
588
589 /* Repo commits */
590
591 function serveRepoCommits(repo, branch) {
592 return renderRepoPage(repo, branch, cat([
593 pull.once('<h3>Commits</h3>'),
594 pull(
595 repo.readLog(branch),
596 pull.asyncMap(function (hash, cb) {
597 repo.getCommitParsed(hash, function (err, commit) {
598 if (err) return cb(err)
599 var commitPath = [repo.id, 'commit', commit.id]
600 var treePath = [repo.id, 'tree', commit.id]
601 cb(null, '<section class="collapse">' +
602 '<strong>' + link(commitPath, escapeHTML(commit.title)) + '</strong><br>' +
603 '<code>' + commit.id + '</code> ' +
604 link(treePath, 'Tree') + '<br>' +
605 (commit.separateAuthor ? escapeHTML(commit.author.name) + ' authored on ' + commit.author.date.toLocaleString() + '<br>' : '') +
606 escapeHTML(commit.committer.name) + ' committed on ' + commit.committer.date.toLocaleString() +
607 '</section>')
608 })
609 })
610 )
611 ]))
612 }
613
614 /* Repo tree */
615
616 function revMenu(repo, currentName) {
617 var baseHref = '/' + encodeURIComponent(repo.id) + '/tree/'
618 var onchange = 'location.href="' + baseHref + '" + this.value'
619 var currentGroup
620 return cat([
621 pull.once('<select onchange="' + escapeHTML(onchange) + '">'),
622 pull(
623 repo.refs(),
624 pull.map(function (ref) {
625 var m = ref.name.match(/^refs\/([^\/]*)\/(.*)$/) || [,, ref.name]
626 var group = m[1]
627 var name = m[2]
628
629 var optgroup = (group === currentGroup) ? '' :
630 (currentGroup ? '</optgroup>' : '') +
631 '<optgroup label="' + (refLabels[group] || group) + '">'
632 currentGroup = group
633 var selected = (name == currentName) ? ' selected="selected"' : ''
634 var htmlName = escapeHTML(name)
635 return optgroup +
636 '<option value="' + htmlName + '"' + selected + '>' +
637 htmlName + '</option>'
638 })
639 ),
640 readOnce(function (cb) {
641 cb(null, currentGroup ? '</optgroup>' : '')
642 }),
643 pull.once('</select>')
644 ])
645 }
646
647 function renderRepoLatest(repo, rev) {
648 return readOnce(function (cb) {
649 repo.getCommitParsed(rev, function (err, commit) {
650 if (err) return cb(err)
651 var commitPath = [repo.id, 'commit', commit.id]
652 cb(null,
653 'Latest: <strong>' + link(commitPath, escapeHTML(commit.title)) +
654 '</strong><br>' +
655 '<code>' + commit.id + '</code><br> ' +
656 escapeHTML(commit.committer.name) + ' committed on ' +
657 commit.committer.date.toLocaleString() +
658 (commit.separateAuthor ? '<br>' +
659 escapeHTML(commit.author.name) + ' authored on ' +
660 commit.author.date.toLocaleString() : ''))
661 })
662 })
663 }
664
665 // breadcrumbs
666 function linkPath(basePath, path) {
667 path = path.slice()
668 var last = path.pop()
669 return path.map(function (dir, i) {
670 return link(basePath.concat(path.slice(0, i+1)), dir)
671 }).concat(last).join(' / ')
672 }
673
674 function renderRepoTree(repo, rev, path) {
675 var pathLinks = path.length === 0 ? '' :
676 ': ' + linkPath([repo.id, 'tree'], [rev].concat(path))
677 return cat([
678 pull.once('<h3>Files' + pathLinks + '</h3>'),
679 pull(
680 repo.readDir(rev, path),
681 pull.map(function (file) {
682 var type = (file.mode === 040000) ? 'tree' : 'blob'
683 var filePath = [repo.id, type, rev].concat(path, file.name)
684 return [type == 'tree' ? '📁' : '📄', link(filePath, file.name)]
685 }),
686 table('class="files"')
687 )
688 ])
689 }
690
691 /* Repo readme */
692
693 function renderRepoReadme(repo, branch, path) {
694 return readNext(function (cb) {
695 pull(
696 repo.readDir(branch, path),
697 pull.filter(function (file) {
698 return /readme(\.|$)/i.test(file.name)
699 }),
700 pull.take(1),
701 pull.collect(function (err, files) {
702 if (err) return cb(null, pull.empty())
703 var file = files[0]
704 if (!file)
705 return cb(null, pull.once(path.length ? '' : '<p>No readme</p>'))
706 repo.getObjectFromAny(file.id, function (err, obj) {
707 if (err) return cb(err)
708 cb(null, cat([
709 pull.once('<section><h4>' + escapeHTML(file.name) + '</h4><hr/>'),
710 /\.md|\/.markdown/i.test(file.name) ?
711 readOnce(function (cb) {
712 pull(obj.read, pull.collect(function (err, bufs) {
713 if (err) return cb(err)
714 var buf = Buffer.concat(bufs, obj.length)
715 cb(null, marked(buf.toString()))
716 }))
717 })
718 : cat([
719 pull.once('<pre>'),
720 pull(obj.read, escapeHTMLStream()),
721 pull.once('</pre>')
722 ]),
723 pull.once('</section>')
724 ]))
725 })
726 })
727 )
728 })
729 }
730
731 /* Repo commit */
732
733 function serveRepoCommit(repo, rev) {
734 return renderRepoPage(repo, rev, cat([
735 pull.once('<h3>Commit ' + rev + '</h3>'),
736 readOnce(function (cb) {
737 repo.getCommitParsed(rev, function (err, commit) {
738 if (err) return cb(err)
739 var commitPath = [repo.id, 'commit', commit.id]
740 var treePath = [repo.id, 'tree', commit.tree]
741 cb(null,
742 '<p><strong>' + link(commitPath, escapeHTML(commit.title)) +
743 '</strong></p>' +
744 pre(commit.body) +
745 '<p>' +
746 (commit.separateAuthor ? escapeHTML(commit.author.name) +
747 ' authored on ' + commit.author.date.toLocaleString() + '<br>'
748 : '') +
749 escapeHTML(commit.committer.name) + ' committed on ' +
750 commit.committer.date.toLocaleString() + '</p>' +
751 '<p>' + commit.parents.map(function (id) {
752 return 'Parent: ' + link([repo.id, 'commit', id], id)
753 }).join('<br>') + '</p>' +
754 '<p>' +
755 (commit.tree ? 'Tree: ' + link(treePath) : 'No tree') +
756 '</p>')
757 })
758 })
759 ]))
760 }
761
762 /* An unknown message linking to a repo */
763
764 function serveRepoSomething(req, repo, id, msg, path) {
765 return renderRepoPage(repo, null,
766 pull.once('<section><h3>' + link([id]) + '</h3>' +
767 json(msg) + '</section>'))
768 }
769
770 /* Repo update */
771
772 function objsArr(objs) {
773 return Array.isArray(objs) ? objs :
774 Object.keys(objs).map(function (sha1) {
775 var obj = Object.create(objs[sha1])
776 obj.sha1 = sha1
777 return obj
778 })
779 }
780
781 function serveRepoUpdate(req, repo, id, msg, path) {
782 var raw = String(req._u.query).split('&').indexOf('raw') > -1
783
784 // convert packs to old single-object style
785 if (msg.content.indexes) {
786 for (var i = 0; i < msg.content.indexes.length; i++) {
787 msg.content.packs[i] = {
788 pack: {link: msg.content.packs[i].link},
789 idx: msg.content.indexes[i]
790 }
791 }
792 }
793
794 return renderRepoPage(repo, null, pull.once(
795 (raw ? '<a href="?" class="raw-link">Info</a>' :
796 '<a href="?raw" class="raw-link">Data</a>') +
797 '<h3>Update</h3>' +
798 (raw ? '<section class="collapse">' + json(msg) + '</section>' :
799 renderRepoUpdate(repo, {key: id, value: msg}, true) +
800 (msg.content.objects ? '<h3>Objects</h3>' +
801 objsArr(msg.content.objects).map(renderObject).join('\n') : '') +
802 (msg.content.packs ? '<h3>Packs</h3>' +
803 msg.content.packs.map(renderPack).join('\n') : ''))))
804 }
805
806 function renderObject(obj) {
807 return '<section class="collapse">' +
808 obj.type + ' ' + link([obj.link], escapeHTML(obj.sha1)) + '<br>' +
809 obj.length + ' bytes' +
810 '</section>'
811 }
812
813 function renderPack(info) {
814 return '<section class="collapse">' +
815 (info.pack ? 'Pack: ' + link([info.pack.link]) + '<br>' : '') +
816 (info.idx ? 'Index: ' + link([info.idx.link]) : '') + '</section>'
817 }
818
819 /* Blob */
820
821 function serveRepoBlob(repo, branch, path) {
822 return readNext(function (cb) {
823 repo.getFile(branch, path, function (err, object) {
824 if (err) return cb(null, serveBlobNotFound(repoId, err))
825 cb(null, serveObjectRaw(object))
826 })
827 })
828 }
829
830 function serveBlobNotFound(repoId, err) {
831 return serveTemplate(400, 'Blob not found', pull.values([
832 '<h2>Blob not found</h2>',
833 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
834 '<pre>' + escapeHTML(err.stack) + '</pre>'
835 ]))
836 }
837
838 function serveRaw(length) {
839 var inBody
840 var headers = {
841 'Content-Type': 'text/plain',
842 'Cache-Control': 'max-age=31536000'
843 }
844 if (length != null)
845 headers['Content-Length'] = length
846 return function (read) {
847 return function (end, cb) {
848 if (inBody) return read(end, cb)
849 if (end) return cb(true)
850 cb(null, [200, headers])
851 inBody = true
852 }
853 }
854 }
855
856 function serveObjectRaw(object) {
857 return pull(object.read, serveRaw(object.length))
858 }
859
860 function serveBlob(req, key) {
861 return readNext(function (cb) {
862 ssb.blobs.want(key, function (err, got) {
863 if (err) cb(null, serveError(err))
864 else if (!got) cb(null, serve404(req))
865 else cb(null, serveRaw()(ssb.blobs.get(key)))
866 })
867 })
868 }
869}
870

Built with git-ssb-web