]> arthur.barton.de Git - bup.git/blob - cmd/web-cmd.py
c4e019c7a751e2b5c4d42ee747e1a393c62362ba
[bup.git] / cmd / web-cmd.py
1 #!/usr/bin/env python
2 import sys, stat, cgi, shutil, 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 class BupRequestHandler(tornado.web.RequestHandler):
12     def get(self, path):
13         return self._process_request(path)
14
15     def head(self, path):
16         return self._process_request(path)
17
18     def _process_request(self, path):
19         path = urllib.unquote(path)
20         print 'Handling request for %s' % path
21         try:
22             n = top.resolve(path)
23         except vfs.NoSuchFile:
24             self.send_error(404)
25             return
26         f = None
27         if stat.S_ISDIR(n.mode):
28             self._list_directory(path, n)
29         else:
30             self._get_file(path, n)
31
32     def _list_directory(self, path, n):
33         """Helper to produce a directory listing.
34
35         Return value is either a file object, or None (indicating an
36         error).  In either case, the headers are sent.
37         """
38         if not path.endswith('/') and len(path) > 0:
39             print 'Redirecting from %s to %s' % (path, path + '/')
40             return self.redirect(path + '/', permanent=True)
41
42         self.set_header("Content-Type", "text/html")
43
44         displaypath = cgi.escape(path)
45         self.write("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
46 <html>
47   <head>
48     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
49     <title>Directory listing for %(displaypath)s</title>
50     <style type="text/css">
51       body, table { font-family: sans-serif }
52       #breadcrumb { margin: 10px 0; }
53       .dir-name { text-align: left }
54       .dir-size { text-align: right }
55     </style>
56   </head>
57   <body>
58     <div id="breadcrumb">
59 """ % { 'displaypath': displaypath })
60         if path == "/":
61             self.write("""<strong>[root]</strong>""")
62         else:
63             self.write("""<a href="/">[root]</a> """)
64             path_parts = path.split("/")
65             path_parts_cleaned = path_parts[1:-1]
66             for index, value in enumerate(path_parts_cleaned[0:-1]):
67                 self.write("""/ <a href="/%(path)s/">%(element)s</a> """ % { 'path' : "/".join(path_parts_cleaned[0:(index + 1)]) , 'element' : value})
68             self.write("""/ <strong>%s</strong>""" % path_parts_cleaned[-1])
69         self.write("""
70     </div>
71     <table>
72       <tr>
73         <th class="dir-name">Name</th>
74         <th class="dir-size">Size</th>
75       </tr>
76 """)
77         for sub in n:
78             displayname = linkname = sub.name
79             # Append / for directories or @ for symbolic links
80             size = str(sub.size())
81             if stat.S_ISDIR(sub.mode):
82                 displayname = sub.name + "/"
83                 linkname = sub.name + "/"
84                 size = '&nbsp;'
85             if stat.S_ISLNK(sub.mode):
86                 displayname = sub.name + "@"
87                 # Note: a link to a directory displays with @ and links with /
88                 size = '&nbsp;'
89             self.write("""      <tr>
90         <td class="dir-name"><a href="%s">%s</a></td>
91         <td class="dir-size">%s</td>
92       </tr>""" % (urllib.quote(linkname), cgi.escape(displayname), size))
93         self.write("""
94     </table>
95   </body>
96 </html>""")
97
98     def _get_file(self, path, n):
99         """Process a request on a file.
100
101         Return value is either a file object, or None (indicating an error).
102         In either case, the headers are sent.
103         """
104         ctype = self._guess_type(path)
105
106         self.set_header("Last-Modified", self.date_time_string(n.mtime))
107         self.set_header("Content-Type", ctype)
108         size = n.size()
109         self.set_header("Content-Length", str(size))
110
111         if self.request.method != 'HEAD':
112             f = n.open()
113             for blob in chunkyreader(f):
114                 self.write(blob)
115             f.close()
116
117     def _guess_type(self, path):
118         """Guess the type of a file.
119
120         Argument is a PATH (a filename).
121
122         Return value is a string of the form type/subtype,
123         usable for a MIME Content-type header.
124
125         The default implementation looks the file's extension
126         up in the table self.extensions_map, using application/octet-stream
127         as a default; however it would be permissible (if
128         slow) to look inside the data to make a better guess.
129         """
130         base, ext = posixpath.splitext(path)
131         if ext in self.extensions_map:
132             return self.extensions_map[ext]
133         ext = ext.lower()
134         if ext in self.extensions_map:
135             return self.extensions_map[ext]
136         else:
137             return self.extensions_map['']
138
139     if not mimetypes.inited:
140         mimetypes.init() # try to read system mime.types
141     extensions_map = mimetypes.types_map.copy()
142     extensions_map.update({
143         '': 'text/plain', # Default
144         '.py': 'text/plain',
145         '.c': 'text/plain',
146         '.h': 'text/plain',
147         })
148
149     def date_time_string(self, t):
150         return time.strftime('%a, %d %b %Y %H:%M:%S', time.gmtime(t))
151
152
153 optspec = """
154 bup web [[hostname]:port]
155 --
156 """
157 o = options.Options('bup web', optspec)
158 (opt, flags, extra) = o.parse(sys.argv[1:])
159
160 if len(extra) > 1:
161     o.fatal("at most one argument expected")
162
163 address = ('', 8080)
164 if len(extra) > 0:
165     addressl = extra[0].split(':', 1)
166     addressl[1] = int(addressl[1])
167     address = tuple(addressl)
168
169 git.check_repo_or_die()
170 top = vfs.RefList(None)
171
172 (pwd,junk) = os.path.split(sys.argv[0])
173
174 settings = dict(
175     debug = 1,
176 )
177
178 # Disable buffering on stdout, for debug messages
179 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
180
181 application = tornado.web.Application([
182     (r"(/.*)", BupRequestHandler),
183 ], **settings)
184
185 if __name__ == "__main__":
186     http_server = tornado.httpserver.HTTPServer(application)
187     http_server.listen(address[1], address=address[0])
188
189     print "Listening on port %s" % http_server._socket.getsockname()[1]
190     loop = tornado.ioloop.IOLoop.instance()
191     loop.start()
192