git ssb

30+

cel / git-ssb-web



Tree: b7eecb050d51e32eb7f6a7ff4cb1ba69ba1c2c08

Files: b7eecb050d51e32eb7f6a7ff4cb1ba69ba1c2c08 / index.js

25370 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 return renderRepoPage(repo, null, pull.once(
784 (raw ? '<a href="?" class="raw-link">Info</a>' :
785 '<a href="?raw" class="raw-link">Data</a>') +
786 '<h3>Update</h3>' +
787 (raw ? '<section class="collapse">' + json(msg) + '</section>' :
788 renderRepoUpdate(repo, {key: id, value: msg}, true) +
789 (msg.content.objects ? '<h3>Objects</h3>' +
790 objsArr(msg.content.objects).map(renderObject).join('\n') : '') +
791 (msg.content.packs ? '<h3>Packs</h3>' +
792 msg.content.packs.map(renderPack).join('\n') : ''))))
793 }
794
795 function renderObject(obj) {
796 return '<section class="collapse">' +
797 obj.type + ' ' + link([obj.link], escapeHTML(obj.sha1)) + '<br>' +
798 obj.length + ' bytes' +
799 '</section>'
800 }
801
802 function renderPack(info) {
803 return '<section class="collapse">' +
804 'Pack: ' + link([info.pack.link]) + '<br>' +
805 'Index: ' + link([info.idx.link]) + '</section>'
806 }
807
808 /* Blob */
809
810 function serveRepoBlob(repo, branch, path) {
811 return readNext(function (cb) {
812 repo.getFile(branch, path, function (err, object) {
813 if (err) return cb(null, serveBlobNotFound(repoId, err))
814 cb(null, serveObjectRaw(object))
815 })
816 })
817 }
818
819 function serveBlobNotFound(repoId, err) {
820 return serveTemplate(400, 'Blob not found', pull.values([
821 '<h2>Blob not found</h2>',
822 '<p>Blob in repo ' + link([repoId]) + ' was not found</p>',
823 '<pre>' + escapeHTML(err.stack) + '</pre>'
824 ]))
825 }
826
827 function serveRaw(length) {
828 var inBody
829 var headers = {
830 'Content-Type': 'text/plain',
831 'Cache-Control': 'max-age=31536000'
832 }
833 if (length != null)
834 headers['Content-Length'] = length
835 return function (read) {
836 return function (end, cb) {
837 if (inBody) return read(end, cb)
838 if (end) return cb(true)
839 cb(null, [200, headers])
840 inBody = true
841 }
842 }
843 }
844
845 function serveObjectRaw(object) {
846 return pull(object.read, serveRaw(object.length))
847 }
848
849 function serveBlob(req, key) {
850 return readNext(function (cb) {
851 ssb.blobs.want(key, function (err, got) {
852 if (err) cb(null, serveError(err))
853 else if (!got) cb(null, serve404(req))
854 else cb(null, serveRaw()(ssb.blobs.get(key)))
855 })
856 })
857 }
858}
859

Built with git-ssb-web