]> arthur.barton.de Git - bup.git/blob - cmd/web-cmd.py
640e4ae8de3126983b414602b7d3504e5020aad0
[bup.git] / cmd / web-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
5 """
6 # end of bup preamble
7 import sys, stat, urllib, mimetypes, posixpath, time, webbrowser
8 import urllib
9 from bup import options, git, vfs
10 from bup.helpers import *
11 try:
12     import tornado.httpserver
13     import tornado.ioloop
14     import tornado.web
15 except ImportError:
16     log('error: cannot find the python "tornado" module; please install it\n')
17     sys.exit(1)
18
19 handle_ctrl_c()
20
21
22 def _compute_breadcrumbs(path, show_hidden=False):
23     """Returns a list of breadcrumb objects for a path."""
24     breadcrumbs = []
25     breadcrumbs.append(('[root]', '/'))
26     path_parts = path.split('/')[1:-1]
27     full_path = '/'
28     for part in path_parts:
29         full_path += part + "/"
30         url_append = ""
31         if show_hidden:
32             url_append = '?hidden=1'
33         breadcrumbs.append((part, full_path+url_append))
34     return breadcrumbs
35
36
37 def _contains_hidden_files(n):
38     """Return True if n contains files starting with a '.', False otherwise."""
39     for sub in n:
40         name = sub.name
41         if len(name)>1 and name.startswith('.'):
42             return True
43
44     return False
45
46
47 def _compute_dir_contents(n, path, show_hidden=False):
48     """Given a vfs node, returns an iterator for display info of all subs."""
49     url_append = ""
50     if show_hidden:
51         url_append = "?hidden=1"
52
53     if path != "/":
54         yield('..', '../' + url_append, '')
55     for sub in n:
56         display = sub.name
57         link = urllib.quote(sub.name)
58
59         # link should be based on fully resolved type to avoid extra
60         # HTTP redirect.
61         if stat.S_ISDIR(sub.try_resolve().mode):
62             link += "/"
63
64         if not show_hidden and len(display)>1 and display.startswith('.'):
65             continue
66
67         size = None
68         if stat.S_ISDIR(sub.mode):
69             display += '/'
70         elif stat.S_ISLNK(sub.mode):
71             display += '@'
72         else:
73             size = sub.size()
74             size = (opt.human_readable and format_filesize(size)) or size
75
76         yield (display, link + url_append, size)
77
78
79 class BupRequestHandler(tornado.web.RequestHandler):
80     def get(self, path):
81         return self._process_request(path)
82
83     def head(self, path):
84         return self._process_request(path)
85     
86     @tornado.web.asynchronous
87     def _process_request(self, path):
88         path = urllib.unquote(path)
89         print 'Handling request for %s' % path
90         try:
91             n = top.resolve(path)
92         except vfs.NoSuchFile:
93             self.send_error(404)
94             return
95         f = None
96         if stat.S_ISDIR(n.mode):
97             self._list_directory(path, n)
98         else:
99             self._get_file(path, n)
100
101     def _list_directory(self, path, n):
102         """Helper to produce a directory listing.
103
104         Return value is either a file object, or None (indicating an
105         error).  In either case, the headers are sent.
106         """
107         if not path.endswith('/') and len(path) > 0:
108             print 'Redirecting from %s to %s' % (path, path + '/')
109             return self.redirect(path + '/', permanent=True)
110
111         try:
112             show_hidden = int(self.request.arguments.get('hidden', [0])[-1])
113         except ValueError as e:
114             show_hidden = False
115
116         self.render(
117             'list-directory.html',
118             path=path,
119             breadcrumbs=_compute_breadcrumbs(path, show_hidden),
120             files_hidden=_contains_hidden_files(n),
121             hidden_shown=show_hidden,
122             dir_contents=_compute_dir_contents(n, path, show_hidden))
123
124     def _get_file(self, path, n):
125         """Process a request on a file.
126
127         Return value is either a file object, or None (indicating an error).
128         In either case, the headers are sent.
129         """
130         ctype = self._guess_type(path)
131
132         self.set_header("Last-Modified", self.date_time_string(n.mtime))
133         self.set_header("Content-Type", ctype)
134         size = n.size()
135         self.set_header("Content-Length", str(size))
136         assert(len(n.hash) == 20)
137         self.set_header("Etag", n.hash.encode('hex'))
138
139         if self.request.method != 'HEAD':
140             self.flush()
141             f = n.open()
142             it = chunkyreader(f)
143             def write_more(me):
144                 try:
145                     blob = it.next()
146                 except StopIteration:
147                     f.close()
148                     self.finish()
149                     return
150                 self.request.connection.stream.write(blob,
151                                                      callback=lambda: me(me))
152             write_more(write_more)
153         else:
154             self.finish()
155
156     def _guess_type(self, path):
157         """Guess the type of a file.
158
159         Argument is a PATH (a filename).
160
161         Return value is a string of the form type/subtype,
162         usable for a MIME Content-type header.
163
164         The default implementation looks the file's extension
165         up in the table self.extensions_map, using application/octet-stream
166         as a default; however it would be permissible (if
167         slow) to look inside the data to make a better guess.
168         """
169         base, ext = posixpath.splitext(path)
170         if ext in self.extensions_map:
171             return self.extensions_map[ext]
172         ext = ext.lower()
173         if ext in self.extensions_map:
174             return self.extensions_map[ext]
175         else:
176             return self.extensions_map['']
177
178     if not mimetypes.inited:
179         mimetypes.init() # try to read system mime.types
180     extensions_map = mimetypes.types_map.copy()
181     extensions_map.update({
182         '': 'text/plain', # Default
183         '.py': 'text/plain',
184         '.c': 'text/plain',
185         '.h': 'text/plain',
186         })
187
188     def date_time_string(self, t):
189         return time.strftime('%a, %d %b %Y %H:%M:%S', time.gmtime(t))
190
191
192 optspec = """
193 bup web [[hostname]:port]
194 --
195 human-readable    display human readable file sizes (i.e. 3.9K, 4.7M)
196 browser           open the site in the default browser
197 """
198 o = options.Options(optspec)
199 (opt, flags, extra) = o.parse(sys.argv[1:])
200
201 if len(extra) > 1:
202     o.fatal("at most one argument expected")
203
204 address = ('127.0.0.1', 8080)
205 if len(extra) > 0:
206     addressl = extra[0].split(':', 1)
207     addressl[1] = int(addressl[1])
208     address = tuple(addressl)
209
210 git.check_repo_or_die()
211 top = vfs.RefList(None)
212
213 settings = dict(
214     debug = 1,
215     template_path = resource_path('web'),
216     static_path = resource_path('web/static')
217 )
218
219 # Disable buffering on stdout, for debug messages
220 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
221
222 application = tornado.web.Application([
223     (r"(/.*)", BupRequestHandler),
224 ], **settings)
225
226 if __name__ == "__main__":
227     http_server = tornado.httpserver.HTTPServer(application)
228     http_server.listen(address[1], address=address[0])
229
230     try:
231         sock = http_server._socket # tornado < 2.0
232     except AttributeError as e:
233         sock = http_server._sockets.values()[0]
234
235     print "Serving HTTP on %s:%d..." % sock.getsockname()
236
237     loop = tornado.ioloop.IOLoop.instance()
238     if opt.browser:
239         browser_addr = 'http://' + address[0] + ':' + str(address[1])
240         loop.add_callback(lambda : webbrowser.open(browser_addr))
241     loop.start()