3 bup_python="$(dirname "$0")/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 from __future__ import absolute_import, print_function
9 from collections import namedtuple
10 import mimetypes, os, posixpath, signal, stat, sys, time, urllib, webbrowser
11 from binascii import hexlify
13 from bup import options, git, vfs
14 from bup.helpers import (chunkyreader, debug1, format_filesize, handle_ctrl_c,
16 from bup.metadata import Metadata
17 from bup.path import resource_path
18 from bup.repo import LocalRepo
19 from bup.io import path_msg
22 from tornado import gen
23 from tornado.httpserver import HTTPServer
24 from tornado.ioloop import IOLoop
25 from tornado.netutil import bind_unix_socket
28 log('error: cannot find the python "tornado" module; please install it\n')
32 # FIXME: right now the way hidden files are handled causes every
33 # directory to be traversed twice.
38 def http_date_from_utc_ns(utc_ns):
39 return time.strftime('%a, %d %b %Y %H:%M:%S', time.gmtime(utc_ns / 10**9))
42 def _compute_breadcrumbs(path, show_hidden=False):
43 """Returns a list of breadcrumb objects for a path."""
45 breadcrumbs.append((b'[root]', b'/'))
46 path_parts = path.split(b'/')[1:-1]
48 for part in path_parts:
49 full_path += part + b"/"
52 url_append = b'?hidden=1'
53 breadcrumbs.append((part, full_path+url_append))
57 def _contains_hidden_files(repo, dir_item):
58 """Return true if the directory contains items with names other than
59 '.' and '..' that begin with '.'
62 for name, item in vfs.contents(repo, dir_item, want_meta=False):
63 if name in (b'.', b'..'):
65 if name.startswith(b'.'):
70 def _dir_contents(repo, resolution, show_hidden=False):
71 """Yield the display information for the contents of dir_item."""
73 url_query = b'?hidden=1' if show_hidden else b''
75 def display_info(name, item, resolved_item, display_name=None):
76 # link should be based on fully resolved type to avoid extra
78 link = tornado.escape.url_escape(name, plus=False)
79 if stat.S_ISDIR(vfs.item_mode(resolved_item)):
81 link = link.encode('ascii')
83 size = vfs.item_size(repo, item)
84 if opt.human_readable:
85 display_size = format_filesize(size)
90 mode = vfs.item_mode(item)
91 if stat.S_ISDIR(mode):
92 display_name = name + b'/'
93 elif stat.S_ISLNK(mode):
94 display_name = name + b'@'
98 return display_name, link + url_query, display_size
100 dir_item = resolution[-1][1]
101 for name, item in vfs.contents(repo, dir_item):
103 if (name not in (b'.', b'..')) and name.startswith(b'.'):
106 yield display_info(name, item, item, b'.')
107 parent_item = resolution[-2][1] if len(resolution) > 1 else dir_item
108 yield display_info(b'..', parent_item, parent_item, b'..')
110 res = vfs.try_resolve(repo, name, parent=resolution, want_meta=False)
111 res_name, res_item = res[-1]
112 yield display_info(name, item, res_item)
115 class BupRequestHandler(tornado.web.RequestHandler):
117 def initialize(self, repo=None):
120 def decode_argument(self, value, name=None):
123 return super(BupRequestHandler, self).decode_argument(value, name)
126 return self._process_request(path)
128 def head(self, path):
129 return self._process_request(path)
131 def _process_request(self, path):
132 print('Handling request for %s' % path)
134 # Set want_meta because dir metadata won't be fetched, and if
135 # it's not a dir, then we're going to want the metadata.
136 res = vfs.resolve(self.repo, path, want_meta=True)
137 leaf_name, leaf_item = res[-1]
141 mode = vfs.item_mode(leaf_item)
142 if stat.S_ISDIR(mode):
143 self._list_directory(path, res)
145 self._get_file(self.repo, path, res)
147 def _list_directory(self, path, resolution):
148 """Helper to produce a directory listing.
150 Return value is either a file object, or None (indicating an
151 error). In either case, the headers are sent.
153 if not path.endswith(b'/') and len(path) > 0:
154 print('Redirecting from %s to %s' % (path_msg(path), path_msg(path + b'/')))
155 return self.redirect(path + b'/', permanent=True)
157 hidden_arg = self.request.arguments.get('hidden', [0])[-1]
159 show_hidden = int(hidden_arg)
160 except ValueError as e:
164 'list-directory.html',
166 breadcrumbs=_compute_breadcrumbs(path, show_hidden),
167 files_hidden=_contains_hidden_files(self.repo, resolution[-1][1]),
168 hidden_shown=show_hidden,
169 dir_contents=_dir_contents(self.repo, resolution,
170 show_hidden=show_hidden))
173 def _get_file(self, repo, path, resolved):
174 """Process a request on a file.
176 Return value is either a file object, or None (indicating an error).
177 In either case, the headers are sent.
179 file_item = resolved[-1][1]
180 file_item = vfs.augment_item_meta(repo, file_item, include_size=True)
181 meta = file_item.meta
182 ctype = self._guess_type(path)
183 self.set_header("Last-Modified", http_date_from_utc_ns(meta.mtime))
184 self.set_header("Content-Type", ctype)
186 self.set_header("Content-Length", str(meta.size))
187 assert len(file_item.oid) == 20
188 self.set_header("Etag", hexlify(file_item.oid))
189 if self.request.method != 'HEAD':
190 with vfs.fopen(self.repo, file_item) as f:
192 for blob in chunkyreader(f):
196 def _guess_type(self, path):
197 """Guess the type of a file.
199 Argument is a PATH (a filename).
201 Return value is a string of the form type/subtype,
202 usable for a MIME Content-type header.
204 The default implementation looks the file's extension
205 up in the table self.extensions_map, using application/octet-stream
206 as a default; however it would be permissible (if
207 slow) to look inside the data to make a better guess.
209 base, ext = posixpath.splitext(path)
210 if ext in self.extensions_map:
211 return self.extensions_map[ext]
213 if ext in self.extensions_map:
214 return self.extensions_map[ext]
216 return self.extensions_map['']
218 if not mimetypes.inited:
219 mimetypes.init() # try to read system mime.types
220 extensions_map = mimetypes.types_map.copy()
221 extensions_map.update({
222 '': 'text/plain', # Default
231 def handle_sigterm(signum, frame):
233 debug1('\nbup-web: signal %d received\n' % signum)
234 log('Shutdown requested\n')
240 signal.signal(signal.SIGTERM, handle_sigterm)
242 UnixAddress = namedtuple('UnixAddress', ['path'])
243 InetAddress = namedtuple('InetAddress', ['host', 'port'])
246 bup web [[hostname]:port]
249 human-readable display human readable file sizes (i.e. 3.9K, 4.7M)
250 browser show repository in default browser (incompatible with unix://)
252 o = options.Options(optspec)
253 (opt, flags, extra) = o.parse(sys.argv[1:])
256 o.fatal("at most one argument expected")
259 address = InetAddress(host='127.0.0.1', port=8080)
262 if bind_url.startswith('unix://'):
263 address = UnixAddress(path=bind_url[len('unix://'):])
265 addr_parts = extra[0].split(':', 1)
266 if len(addr_parts) == 1:
270 host, port = addr_parts
273 except (TypeError, ValueError) as ex:
274 o.fatal('port must be an integer, not %r' % port)
275 address = InetAddress(host=host, port=port)
277 git.check_repo_or_die()
281 template_path = resource_path(b'web').decode('utf-8'),
282 static_path = resource_path(b'web/static').decode('utf-8'),
285 # Disable buffering on stdout, for debug messages
287 sys.stdout._line_buffering = True
288 except AttributeError:
289 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
291 application = tornado.web.Application([
292 (r"(?P<path>/.*)", BupRequestHandler, dict(repo=LocalRepo())),
295 http_server = HTTPServer(application)
296 io_loop_pending = IOLoop.instance()
298 if isinstance(address, InetAddress):
299 sockets = tornado.netutil.bind_sockets(address.port, address.host)
300 http_server.add_sockets(sockets)
301 print('Serving HTTP on %s:%d...' % sockets[0].getsockname())
303 browser_addr = 'http://' + address[0] + ':' + str(address[1])
304 io_loop_pending.add_callback(lambda : webbrowser.open(browser_addr))
305 elif isinstance(address, UnixAddress):
306 unix_socket = bind_unix_socket(address.path)
307 http_server.add_socket(unix_socket)
308 print('Serving HTTP on filesystem socket %r' % address.path)
310 log('error: unexpected address %r', address)
313 io_loop = io_loop_pending
317 log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))