]> arthur.barton.de Git - bup.git/blob - cmd/web-cmd.py
All HTML attribute values should be enclosed by doublequotes.
[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.send_header("Content-Length", 0)
58             self.end_headers()
59             return
60
61         # Note that it is necessary to buffer the output into a StringIO here
62         # so that we can compute the content length before we send the
63         # content.  The only other option would be to do chunked encoding, or
64         # not support content length.
65         f = StringIO()
66         displaypath = cgi.escape(path)
67         f.write("""
68 <HTML>
69   <HEAD>
70     <TITLE>Directory listing for %(displaypath)s</TITLE>
71     <STYLE>
72       BODY, TABLE { font-family: sans-serif }
73       .dir-name { text-align: left }
74       .dir-size { text-align: right }
75     </STYLE>
76   </HEAD>
77   <BODY>
78     <H2>Directory listing for %(displaypath)s</H2>
79     <TABLE>
80       <TR>
81         <TH class="dir-name">Name</TH>
82         <TH class="dir-size">Size<TH>
83       </TR>
84 """ % { 'displaypath': displaypath })
85         for sub in n:
86             displayname = linkname = sub.name
87             # Append / for directories or @ for symbolic links
88             size = str(sub.size())
89             if stat.S_ISDIR(sub.mode):
90                 displayname = sub.name + "/"
91                 linkname = sub.name + "/"
92                 size = '&nbsp;'
93             if stat.S_ISLNK(sub.mode):
94                 displayname = sub.name + "@"
95                 # Note: a link to a directory displays with @ and links with /
96                 size = '&nbsp;'
97             f.write("""      <TR>
98         <TD class="dir-name"><A href="%s">%s</A></TD>
99         <TD class="dir-size">%s</TD>
100       </TR>""" % (urllib.quote(linkname), cgi.escape(displayname), size))
101         f.write("""
102     </TABLE>
103   </BODY>
104 </HTML>""")
105         length = f.tell()
106         f.seek(0)
107         self.send_response(200)
108         self.send_header("Content-type", "text/html")
109         self.send_header("Content-Length", str(length))
110         self.end_headers()
111         self._send_content(f)
112         f.close()
113
114     def _get_file(self, path, n):
115         """Process a request on a file.
116
117         Return value is either a file object, or None (indicating an error).
118         In either case, the headers are sent.
119         """
120         ctype = self._guess_type(path)
121         f = n.open()
122         self.send_response(200)
123         self.send_header("Content-type", ctype)
124         self.send_header("Content-Length", str(n.size()))
125         self.send_header("Last-Modified", self.date_time_string(n.mtime))
126         self.end_headers()
127         self._send_content(f)
128         f.close()
129
130     def _send_content(self, f):
131         """Send the content file as the response if necessary."""
132         if self.command != 'HEAD':
133             for blob in chunkyreader(f):
134                 self.wfile.write(blob)
135
136     def _guess_type(self, path):
137         """Guess the type of a file.
138
139         Argument is a PATH (a filename).
140
141         Return value is a string of the form type/subtype,
142         usable for a MIME Content-type header.
143
144         The default implementation looks the file's extension
145         up in the table self.extensions_map, using application/octet-stream
146         as a default; however it would be permissible (if
147         slow) to look inside the data to make a better guess.
148         """
149         base, ext = posixpath.splitext(path)
150         if ext in self.extensions_map:
151             return self.extensions_map[ext]
152         ext = ext.lower()
153         if ext in self.extensions_map:
154             return self.extensions_map[ext]
155         else:
156             return self.extensions_map['']
157
158     if not mimetypes.inited:
159         mimetypes.init() # try to read system mime.types
160     extensions_map = mimetypes.types_map.copy()
161     extensions_map.update({
162         '': 'text/plain', # Default
163         '.py': 'text/plain',
164         '.c': 'text/plain',
165         '.h': 'text/plain',
166         })
167
168
169 optspec = """
170 bup web [[hostname]:port]
171 --
172 """
173 o = options.Options('bup web', optspec)
174 (opt, flags, extra) = o.parse(sys.argv[1:])
175
176 if len(extra) > 1:
177     o.fatal("at most one argument expected")
178
179 address = ('127.0.0.1', 8080)
180 if len(extra) > 0:
181     addressl = extra[0].split(':', 1)
182     addressl[1] = int(addressl[1])
183     address = tuple(addressl)
184
185 git.check_repo_or_die()
186 top = vfs.RefList(None)
187
188 try:
189     httpd = BupHTTPServer(address, BupRequestHandler)
190 except socket.error, e:
191     log('socket%r: %s\n' % (address, e.args[1]))
192     sys.exit(1)
193
194 sa = httpd.socket.getsockname()
195 log("Serving HTTP on %s:%d...\n" % sa)
196 httpd.serve_forever()