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