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