git ssb

30+

cel / git-ssb-web



Tree: cce7638c52883b7a1e6aeb5c21028f6bf3f62e21

Files: cce7638c52883b7a1e6aeb5c21028f6bf3f62e21 / index.js

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

Built with git-ssb-web