]> arthur.barton.de Git - bup.git/blob - cmd/web-cmd.py
Add new 'bup web' command.
[bup.git] / cmd / web-cmd.py
1 #!/usr/bin/env python
2 import sys, stat, cgi, shutil, urllib, mimetypes, posixpath
3 import BaseHTTPServer
4 from bup import options, git, vfs
5 from bup.helpers import *
6 try:
7     from cStringIO import StringIO
8 except ImportError:
9     from StringIO import StringIO
10
11 handle_ctrl_c()
12
13 class BupHTTPServer(BaseHTTPServer.HTTPServer):
14     def handle_error(self, request, client_address):
15         # If we get a KeyboardInterrupt error than just reraise it
16         # so that we cause the server to exit.
17         if sys.exc_info()[0] == KeyboardInterrupt:
18             raise
19
20 class BupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
21     server_version = 'BupHTTP/%s' % version_tag()
22     protocol_version = 'HTTP/1.1'
23     def do_GET(self):
24         self._process_request()
25
26     def do_HEAD(self):
27         self._process_request()
28
29     def _process_request(self):
30         """Common code for GET and HEAD commands.
31
32         This sends the response code and MIME headers along with the content
33         of the response.
34         """
35         path = urllib.unquote(self.path)
36         try:
37             n = top.resolve(path)
38         except vfs.NoSuchFile:
39             self.send_error(404)
40             return
41         f = None
42         if stat.S_ISDIR(n.mode):
43             self._list_directory(path, n)
44         else:
45             self._get_file(path, n)
46
47     def _list_directory(self, path, n):
48         """Helper to produce a directory listing.
49
50         Return value is either a file object, or None (indicating an
51         error).  In either case, the headers are sent.
52         """
53         if not path.endswith('/'):
54             # redirect browser - doing basically what apache does
55             self.send_response(301)
56             self.send_header("Location", path + "/")
57             self.end_headers()
58             return
59
60         # Note that it is necessary to buffer the output into a StringIO here
61         # so that we can compute the content length before we send the
62         # content.  The only other option would be to do chunked encoding, or
63         # not support content length.
64         f = StringIO()
65         displaypath = cgi.escape(path)
66         f.write("""
67 <HTML>
68   <HEAD>
69     <TITLE>Directory listing for %(displaypath)s</TITLE>
70     <STYLE>
71       BODY, TABLE { font-family: sans-serif }
72       .dir-name { text-align: left }
73       .dir-size { text-align: right }
74     </STYLE>
75   </HEAD>
76   <BODY>
77     <H2>Directory listing for %(displaypath)s</H2>
78     <TABLE>
79       <TR>
80         <TH class=dir-name>Name</TH>
81         <TH class=dir-size>Size<TH>
82       </TR>
83 """ % { 'displaypath': displaypath })
84         for sub in n:
85             displayname = linkname = sub.name
86             # Append / for directories or @ for symbolic links
87             size = str(sub.size())
88             if stat.S_ISDIR(sub.mode):
89                 displayname = sub.name + "/"
90                 linkname = sub.name + "/"
91                 size = '&nbsp;'
92             if stat.S_ISLNK(sub.mode):
93                 displayname = sub.name + "@"
94                 # Note: a link to a directory displays with @ and links with /
95                 size = '&nbsp;'
96             f.write("""      <TR>
97         <TD class=dir-name><A href="%s">%s</A></TD>
98         <TD class=dir-size>%s</TD>
99       </TR>""" % (urllib.quote(linkname), cgi.escape(displayname), size))
100         f.write("""
101     </UL>
102   </BODY>
103 </HTML>""")
104         length = f.tell()
105         f.seek(0)
106         self.send_response(200)
107         self.send_header("Content-type", "text/html")
108         self.send_header("Content-Length", str(length))
109         self.end_headers()
110         self._send_content(f)
111         f.close()
112
113     def _get_file(self, path, n):
114         """Process a request on a file.
115
116         Return value is either a file object, or None (indicating an error).
117         In either case, the headers are sent.
118         """
119         ctype = self._guess_type(path)
120         f = n.open()
121         self.send_response(200)
122         self.send_header("Content-type", ctype)
123         self.send_header("Content-Length", str(n.size()))
124         self.send_header("Last-Modified", self.date_time_string(n.mtime))
125         self.end_headers()
126         self._send_content(f)
127         f.close()
128
129     def _send_content(self, f):
130         """Send the content file as the response if necessary."""
131         if self.command != 'HEAD':
132             for blob in chunkyreader(f):
133                 self.wfile.write(blob)
134
135     def _guess_type(self, path):
136         """Guess the type of a file.
137
138         Argument is a PATH (a filename).
139
140         Return value is a string of the form type/subtype,
141         usable for a MIME Content-type header.
142
143         The default implementation looks the file's extension
144         up in the table self.extensions_map, using application/octet-stream
145         as a default; however it would be permissible (if
146         slow) to look inside the data to make a better guess.
147         """
148         base, ext = posixpath.splitext(path)
149         if ext in self.extensions_map:
150             return self.extensions_map[ext]
151         ext = ext.lower()
152         if ext in self.extensions_map:
153             return self.extensions_map[ext]
154         else:
155             return self.extensions_map['']
156
157     if not mimetypes.inited:
158         mimetypes.init() # try to read system mime.types
159     extensions_map = mimetypes.types_map.copy()
160     extensions_map.update({
161         '': 'text/plain', # Default
162         '.py': 'text/plain',
163         '.c': 'text/plain',
164         '.h': 'text/plain',
165         })
166
167
168 optspec = """
169 bup web [[hostname]:port]
170 --
171 """
172 o = options.Options('bup web', optspec)
173 (opt, flags, extra) = o.parse(sys.argv[1:])
174
175 if len(extra) > 1:
176     o.fatal("at most one argument expected")
177
178 address = ('127.0.0.1', 8080)
179 if len(extra) > 0:
180     addressl = extra[0].split(':', 1)
181     addressl[1] = int(addressl[1])
182     address = tuple(addressl)
183
184 git.check_repo_or_die()
185 top = vfs.RefList(None)
186
187 httpd = BupHTTPServer(address, BupRequestHandler)
188 sa = httpd.socket.getsockname()
189 print "Serving HTTP on %s:%d..." % sa
190 sys.stdout.flush()
191 httpd.serve_forever()