bzrbrowse.cgi 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #!/usr/bin/env python
  2. # Copyright (C) 2008 Lukas Lalinsky <lalinsky@gmail.com>
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  17. # CHANGE THIS:
  18. config = {
  19. 'root': '/home/ccan/ccan',
  20. 'base_url': '/browse',
  21. 'images_url': '',
  22. 'branch_url': 'http://ccan.ozlabs.org/repo',
  23. }
  24. import os, sys, string
  25. from bzrlib.branch import Branch
  26. from bzrlib.errors import NotBranchError
  27. from bzrlib import urlutils, osutils
  28. __version__ = '0.0.1-rusty'
  29. class HTTPError(Exception):
  30. def __init__(self, code, message):
  31. self.code = code
  32. self.message = message
  33. class NotFound(HTTPError):
  34. def __init__(self, message):
  35. super(NotFound, self).__init__('404 Not Found', message)
  36. def escape_html(text):
  37. return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace("\n", '<br />')
  38. class BzrBrowse(object):
  39. icons = {
  40. 'file': 'file.png',
  41. 'directory': 'folder.png',
  42. 'symlink': 'symlink.png',
  43. }
  44. page_tmpl = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
  45. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head><title>%(title)s</title><style type="text/css">
  46. body { font-family: sans-serif; font-size: 10pt; }
  47. div#page { padding: 0.5em; border: solid 1px #444; background: #FAFAFA; }
  48. #footer { font-size: 70%%; background-color: #444; color: #FFF; margin: 0; padding: 0.1em 0.3em; }
  49. #footer hr { display: none; }
  50. h1 { margin: 0; font-size: 14pt; background-color: #444; color: #FFF; padding: 0.1em 0.3em; }
  51. h1 a { color: #FFF; }
  52. h1 a:hover { color: #8CB1D8; }
  53. #page a { color: #244B7C; }
  54. #page a:hover { color: #B12319; }
  55. pre { margin: 0; font-size: 90%%; }
  56. .linenumbers { text-align: right; padding-right: 0.5em; border-right: solid 1px #444; }
  57. .text { padding-left: 0.5em; }
  58. .msg { margin: 0; margin-bottom: 0.5em; padding: 0.3em 0em; border-bottom: solid 1px #444;}
  59. code { background-color: #000; color: #FFF; font-size: 90%%;}
  60. </style>
  61. </head><body>
  62. <h1>%(header)s</h1>
  63. <div id="page">%(contents)s</div>
  64. <div id="footer"><hr />bzrbrowse/%(version)s</div>
  65. </body></html>'''
  66. def __init__(self, config):
  67. self.config = config
  68. self.base_url = None
  69. def list_to_html(self, entries):
  70. content = []
  71. for entry in entries:
  72. line = '<img src="%(images_url)s/%(icon)s" /> <a href="%(base_url)s/%(path)s">%(name)s</a><br />' % {
  73. 'base_url': self.config['base_url'],
  74. 'images_url': self.config['images_url'],
  75. 'path': entry['path'],
  76. 'name': entry['name'],
  77. 'icon': self.icons.get(entry['kind'], self.icons['file'])
  78. }
  79. content.append(line)
  80. return ''.join(content)
  81. def list_fs_directory(self, path):
  82. entries = []
  83. if path:
  84. entries.append({
  85. 'name': '..',
  86. 'path': os.path.dirname(path),
  87. 'kind': 'directory',
  88. })
  89. if path:
  90. prefix = path + '/'
  91. else:
  92. prefix = ''
  93. try:
  94. filelist = os.listdir(os.path.join(self.config['root'], path))
  95. except OSError:
  96. raise NotFound('Path not found: ' + path)
  97. for name in sorted(filelist):
  98. if name.startswith('.'):
  99. continue
  100. abspath = os.path.join(path, name)
  101. if os.path.isdir(os.path.join(self.config['root'], abspath)):
  102. entries.append({
  103. 'name': name,
  104. 'path': prefix + name,
  105. 'kind': 'directory',
  106. })
  107. return self.list_to_html(entries)
  108. def view_branch_file(self, tree, ie):
  109. if ie.text_size > 1024 * 1024:
  110. return 'File too big. (%d bytes)' % (ie.text_size)
  111. tree.lock_read()
  112. try:
  113. text = tree.get_file_text(ie.file_id)
  114. finally:
  115. tree.unlock()
  116. if '\0' in text:
  117. return 'Binary file. (%d bytes)' % (ie.text_size)
  118. try:
  119. text = text.decode('utf-8')
  120. except UnicodeDecodeError:
  121. text = text.decode('latin-1')
  122. linenumbers = []
  123. for i in range(1, text.count('\n') + 1):
  124. linenumbers.append('<a id="l-%d" href="#l-%d">%d</a>' % (i, i, i))
  125. linenumbers = '\n'.join(linenumbers)
  126. return ('<table cellspacing="0" cellpadding="0"><tr><td class="linenumbers"><pre>' +
  127. linenumbers + '</pre></td><td class="text"><pre>' + escape_html(text) +
  128. '</pre></td></tr></table>')
  129. # Symlinks in ccan contain .., and bzr refuses to serve that. Simplify.
  130. def squish(self, linkname):
  131. result = []
  132. for elem in string.split(linkname, os.sep):
  133. if elem == '..':
  134. result = result[:-1]
  135. else:
  136. result.append(elem)
  137. return string.join(result, os.sep)
  138. def list_branch_directory(self, branch, path, relpath):
  139. tree = branch.basis_tree()
  140. file_id = tree.path2id(relpath)
  141. ie = tree.inventory[file_id]
  142. if ie.kind == 'file':
  143. return self.view_branch_file(tree, ie)
  144. if ie.kind == 'symlink':
  145. return self.list_branch_directory(branch, path, self.squish(osutils.dirname(relpath) + os.sep + ie.symlink_target))
  146. entries = []
  147. if path:
  148. entries.append({
  149. 'name': '..',
  150. 'path': urlutils.dirname(path),
  151. 'kind': 'directory',
  152. })
  153. if path:
  154. prefix = path + '/'
  155. else:
  156. prefix = ''
  157. for name, child in sorted(ie.children.iteritems()):
  158. entries.append({
  159. 'name': name,
  160. 'path': prefix + name,
  161. 'kind': child.kind,
  162. })
  163. html = self.list_to_html(entries)
  164. base = self.config['branch_url'] + '/' + osutils.relpath(self.config['root'], urlutils.local_path_from_url(branch.base))
  165. html = ('<p class="msg">This is a <a href="http://bazaar-vcs.org/">Bazaar</a> branch. ' +
  166. 'Use <code>bzr branch ' + base + '</code> to download it.</p>' + html)
  167. return html
  168. def request(self, path):
  169. abspath = os.path.join(self.config['root'], path)
  170. try:
  171. branch, relpath = Branch.open_containing(abspath)
  172. except NotBranchError:
  173. return self.list_fs_directory(path)
  174. return self.list_branch_directory(branch, path, relpath)
  175. def title(self, path):
  176. return '/' + path
  177. def header(self, path):
  178. title = []
  179. p = ''
  180. title.append('<a href="%s%s">root</a>' % (self.config['base_url'], p))
  181. for name in path.split('/'):
  182. p += '/' + name
  183. title.append('<a href="%s%s">%s</a>' % (self.config['base_url'], p, name))
  184. return '/'.join(title)
  185. def __call__(self, environ, start_response):
  186. try:
  187. path = '/'.join(filter(bool, environ.get('PATH_INFO', '').split('/')))
  188. contents = self.page_tmpl % {
  189. 'title': self.title(path),
  190. 'header': self.header(path),
  191. 'contents': self.request(path),
  192. 'version': __version__
  193. }
  194. contents = contents.encode('utf-8')
  195. headers = [('Content-type','text/html; charset=UTF-8')]
  196. start_response('200 OK', headers)
  197. return [contents]
  198. except HTTPError, e:
  199. headers = [('Content-type','text/html; charset=UTF-8')]
  200. start_response(e.code, headers)
  201. return [e.message]
  202. except:
  203. import cgitb, sys
  204. traceback_html = cgitb.html(sys.exc_info())
  205. headers = [('Content-type','text/html; charset=UTF-8')]
  206. start_response('200 OK', headers)
  207. return [traceback_html]
  208. from wsgiref.handlers import CGIHandler
  209. CGIHandler().run(BzrBrowse(config))