3 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
4 export "BUP_ARGV_0"="$0"
7 export "BUP_ARGV_${arg_i}"="$arg"
11 # Here to end of preamble replaced during install
12 bup_python="$(dirname "$0")/bup-python" || exit $?
13 exec "$bup_python" "$0"
17 from __future__ import absolute_import, print_function
18 from collections import namedtuple
19 import mimetypes, os, posixpath, signal, stat, sys, time, urllib, webbrowser
20 from binascii import hexlify
22 from bup import compat, options, git, vfs
23 from bup.helpers import (chunkyreader, debug1, format_filesize, handle_ctrl_c,
25 from bup.metadata import Metadata
26 from bup.path import resource_path
27 from bup.repo import LocalRepo
28 from bup.io import path_msg
31 from tornado import gen
32 from tornado.httpserver import HTTPServer
33 from tornado.ioloop import IOLoop
34 from tornado.netutil import bind_unix_socket
37 log('error: cannot find the python "tornado" module; please install it\n')
41 # FIXME: right now the way hidden files are handled causes every
42 # directory to be traversed twice.
47 def http_date_from_utc_ns(utc_ns):
48 return time.strftime('%a, %d %b %Y %H:%M:%S', time.gmtime(utc_ns / 10**9))
51 def _compute_breadcrumbs(path, show_hidden=False):
52 """Returns a list of breadcrumb objects for a path."""
54 breadcrumbs.append((b'[root]', b'/'))
55 path_parts = path.split(b'/')[1:-1]
57 for part in path_parts:
58 full_path += part + b"/"
61 url_append = b'?hidden=1'
62 breadcrumbs.append((part, full_path+url_append))
66 def _contains_hidden_files(repo, dir_item):
67 """Return true if the directory contains items with names other than
68 '.' and '..' that begin with '.'
71 for name, item in vfs.contents(repo, dir_item, want_meta=False):
72 if name in (b'.', b'..'):
74 if name.startswith(b'.'):
79 def _dir_contents(repo, resolution, show_hidden=False):
80 """Yield the display information for the contents of dir_item."""
82 url_query = b'?hidden=1' if show_hidden else b''
84 def display_info(name, item, resolved_item, display_name=None):
85 # link should be based on fully resolved type to avoid extra
87 link = tornado.escape.url_escape(name, plus=False)
88 if stat.S_ISDIR(vfs.item_mode(resolved_item)):
90 link = link.encode('ascii')
92 size = vfs.item_size(repo, item)
93 if opt.human_readable:
94 display_size = format_filesize(size)
99 mode = vfs.item_mode(item)
100 if stat.S_ISDIR(mode):
101 display_name = name + b'/'
102 elif stat.S_ISLNK(mode):
103 display_name = name + b'@'
107 return display_name, link + url_query, display_size
109 dir_item = resolution[-1][1]
110 for name, item in vfs.contents(repo, dir_item):
112 if (name not in (b'.', b'..')) and name.startswith(b'.'):
115 yield display_info(name, item, item, b'.')
116 parent_item = resolution[-2][1] if len(resolution) > 1 else dir_item
117 yield display_info(b'..', parent_item, parent_item, b'..')
119 res = vfs.try_resolve(repo, name, parent=resolution, want_meta=False)
120 res_name, res_item = res[-1]
121 yield display_info(name, item, res_item)
124 class BupRequestHandler(tornado.web.RequestHandler):
126 def initialize(self, repo=None):
129 def decode_argument(self, value, name=None):
132 return super(BupRequestHandler, self).decode_argument(value, name)
135 return self._process_request(path)
137 def head(self, path):
138 return self._process_request(path)
140 def _process_request(self, path):
141 print('Handling request for %s' % path)
143 # Set want_meta because dir metadata won't be fetched, and if
144 # it's not a dir, then we're going to want the metadata.
145 res = vfs.resolve(self.repo, path, want_meta=True)
146 leaf_name, leaf_item = res[-1]
150 mode = vfs.item_mode(leaf_item)
151 if stat.S_ISDIR(mode):
152 self._list_directory(path, res)
154 self._get_file(self.repo, path, res)
156 def _list_directory(self, path, resolution):
157 """Helper to produce a directory listing.
159 Return value is either a file object, or None (indicating an
160 error). In either case, the headers are sent.
162 if not path.endswith(b'/') and len(path) > 0:
163 print('Redirecting from %s to %s' % (path_msg(path), path_msg(path + b'/')))
164 return self.redirect(path + b'/', permanent=True)
166 hidden_arg = self.request.arguments.get('hidden', [0])[-1]
168 show_hidden = int(hidden_arg)
169 except ValueError as e:
173 'list-directory.html',
175 breadcrumbs=_compute_breadcrumbs(path, show_hidden),
176 files_hidden=_contains_hidden_files(self.repo, resolution[-1][1]),
177 hidden_shown=show_hidden,
178 dir_contents=_dir_contents(self.repo, resolution,
179 show_hidden=show_hidden))
182 def _get_file(self, repo, path, resolved):
183 """Process a request on a file.
185 Return value is either a file object, or None (indicating an error).
186 In either case, the headers are sent.
188 file_item = resolved[-1][1]
189 file_item = vfs.augment_item_meta(repo, file_item, include_size=True)
190 meta = file_item.meta
191 ctype = self._guess_type(path)
192 self.set_header("Last-Modified", http_date_from_utc_ns(meta.mtime))
193 self.set_header("Content-Type", ctype)
195 self.set_header("Content-Length", str(meta.size))
196 assert len(file_item.oid) == 20
197 self.set_header("Etag", hexlify(file_item.oid))
198 if self.request.method != 'HEAD':
199 with vfs.fopen(self.repo, file_item) as f:
201 for blob in chunkyreader(f):
205 def _guess_type(self, path):
206 """Guess the type of a file.
208 Argument is a PATH (a filename).
210 Return value is a string of the form type/subtype,
211 usable for a MIME Content-type header.
213 The default implementation looks the file's extension
214 up in the table self.extensions_map, using application/octet-stream
215 as a default; however it would be permissible (if
216 slow) to look inside the data to make a better guess.
218 base, ext = posixpath.splitext(path)
219 if ext in self.extensions_map:
220 return self.extensions_map[ext]
222 if ext in self.extensions_map:
223 return self.extensions_map[ext]
225 return self.extensions_map['']
227 if not mimetypes.inited:
228 mimetypes.init() # try to read system mime.types
229 extensions_map = mimetypes.types_map.copy()
230 extensions_map.update({
231 '': 'text/plain', # Default
240 def handle_sigterm(signum, frame):
242 debug1('\nbup-web: signal %d received\n' % signum)
243 log('Shutdown requested\n')
249 signal.signal(signal.SIGTERM, handle_sigterm)
251 UnixAddress = namedtuple('UnixAddress', ['path'])
252 InetAddress = namedtuple('InetAddress', ['host', 'port'])
255 bup web [[hostname]:port]
258 human-readable display human readable file sizes (i.e. 3.9K, 4.7M)
259 browser show repository in default browser (incompatible with unix://)
261 o = options.Options(optspec)
262 opt, flags, extra = o.parse(compat.argv[1:])
265 o.fatal("at most one argument expected")
268 address = InetAddress(host='127.0.0.1', port=8080)
271 if bind_url.startswith('unix://'):
272 address = UnixAddress(path=bind_url[len('unix://'):])
274 addr_parts = extra[0].split(':', 1)
275 if len(addr_parts) == 1:
279 host, port = addr_parts
282 except (TypeError, ValueError) as ex:
283 o.fatal('port must be an integer, not %r' % port)
284 address = InetAddress(host=host, port=port)
286 git.check_repo_or_die()
290 template_path = resource_path(b'web').decode('utf-8'),
291 static_path = resource_path(b'web/static').decode('utf-8'),
294 # Disable buffering on stdout, for debug messages
296 sys.stdout._line_buffering = True
297 except AttributeError:
298 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
300 application = tornado.web.Application([
301 (r"(?P<path>/.*)", BupRequestHandler, dict(repo=LocalRepo())),
304 http_server = HTTPServer(application)
305 io_loop_pending = IOLoop.instance()
307 if isinstance(address, InetAddress):
308 sockets = tornado.netutil.bind_sockets(address.port, address.host)
309 http_server.add_sockets(sockets)
310 print('Serving HTTP on %s:%d...' % sockets[0].getsockname())
312 browser_addr = 'http://' + address[0] + ':' + str(address[1])
313 io_loop_pending.add_callback(lambda : webbrowser.open(browser_addr))
314 elif isinstance(address, UnixAddress):
315 unix_socket = bind_unix_socket(address.path)
316 http_server.add_socket(unix_socket)
317 print('Serving HTTP on filesystem socket %r' % address.path)
319 log('error: unexpected address %r', address)
322 io_loop = io_loop_pending
326 log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))