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