From 92367a837056d44a1eb93dc53d144c437fe1addc Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 11 Jul 2010 16:33:36 -0700 Subject: [PATCH] Add new 'bup web' command. 'bup web' starts a web server that allows one to browse the bup repository from a web browser. Also reorganized version-cmd to allow easy access to bup version from other places. Signed-off-by: Joe Beda --- Documentation/bup-web.1.md | 42 ++++++++ cmd/version-cmd.py | 19 +--- cmd/web-cmd.py | 191 +++++++++++++++++++++++++++++++++++++ lib/bup/helpers.py | 19 ++++ lib/bup/vfs.py | 3 + main.py | 1 + 6 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 Documentation/bup-web.1.md create mode 100755 cmd/web-cmd.py diff --git a/Documentation/bup-web.1.md b/Documentation/bup-web.1.md new file mode 100644 index 0000000..a334945 --- /dev/null +++ b/Documentation/bup-web.1.md @@ -0,0 +1,42 @@ +% bup-ftp(1) Bup %BUP_VERSION% +% Joe Beda +% %BUP_DATE% + +# NAME + +bup-web - Start web server to browse bup repositiory + +# SYNOPSIS + +bup web [[hostname]:port] + +# DESCRIPTION + +`bup web` is a starts a web server that can browse bup repositories. The file +hierarchy is the same as that shown by `bup-fuse`(1), `bup-ls`(1) and +`bup-ftp`(1). + +`hostname` and `port` default to 127.0.0.1 and 8080, respectively, and hence +`bup web` will only offer up the web server to locally running clients. If +you'd like to expose the web server to anyone on your network (dangerous!) you +can omit the bind address to bind to all available interfaces: `:8080`. + +# EXAMPLE + + $ bup web + Serving HTTP on 127.0.0.1:8080... + ^C + + $ bup web :8080 + Serving HTTP on 0.0.0.0:8080... + ^C + + +# SEE ALSO + +`bup-fuse`(1), `bup-ls`(1), `bup-ftp`(1) + + +# BUP + +Part of the `bup`(1) suite. diff --git a/cmd/version-cmd.py b/cmd/version-cmd.py index 1964ca0..3469086 100755 --- a/cmd/version-cmd.py +++ b/cmd/version-cmd.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import sys, os, glob -from bup import options, _version +from bup import options +from bup.helpers import * optspec = """ bup version [--date|--commit|--tag] @@ -12,24 +13,14 @@ tag display the tag name of this version. If no tag is available, display t o = options.Options('bup version', optspec) (opt, flags, extra) = o.parse(sys.argv[1:]) -def autoname(names): - names = names.strip() - assert(names[0] == '(') - assert(names[-1] == ')') - names = names[1:-1] - l = [n.strip() for n in names.split(',')] - for n in l: - if n.startswith('tag: bup-'): - return n[9:] - total = (opt.date or 0) + (opt.commit or 0) + (opt.tag or 0) if total > 1: o.fatal('at most one option expected') if opt.date: - print _version.DATE.split(' ')[0] + print version_date() elif opt.commit: - print _version.COMMIT + print version_commit() else: - print autoname(_version.NAMES) or 'unknown-%s' % _version.COMMIT[:7] + print version_tag() diff --git a/cmd/web-cmd.py b/cmd/web-cmd.py new file mode 100755 index 0000000..209e57d --- /dev/null +++ b/cmd/web-cmd.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +import sys, stat, cgi, shutil, urllib, mimetypes, posixpath +import BaseHTTPServer +from bup import options, git, vfs +from bup.helpers import * +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +handle_ctrl_c() + +class BupHTTPServer(BaseHTTPServer.HTTPServer): + def handle_error(self, request, client_address): + # If we get a KeyboardInterrupt error than just reraise it + # so that we cause the server to exit. + if sys.exc_info()[0] == KeyboardInterrupt: + raise + +class BupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + server_version = 'BupHTTP/%s' % version_tag() + protocol_version = 'HTTP/1.1' + def do_GET(self): + self._process_request() + + def do_HEAD(self): + self._process_request() + + def _process_request(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers along with the content + of the response. + """ + path = urllib.unquote(self.path) + try: + n = top.resolve(path) + except vfs.NoSuchFile: + self.send_error(404) + return + f = None + if stat.S_ISDIR(n.mode): + self._list_directory(path, n) + else: + self._get_file(path, n) + + def _list_directory(self, path, n): + """Helper to produce a directory listing. + + Return value is either a file object, or None (indicating an + error). In either case, the headers are sent. + """ + if not path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(301) + self.send_header("Location", path + "/") + self.end_headers() + return + + # Note that it is necessary to buffer the output into a StringIO here + # so that we can compute the content length before we send the + # content. The only other option would be to do chunked encoding, or + # not support content length. + f = StringIO() + displaypath = cgi.escape(path) + f.write(""" + + + Directory listing for %(displaypath)s + + + +

Directory listing for %(displaypath)s

+ + + + +""" % { 'displaypath': displaypath }) + for sub in n: + displayname = linkname = sub.name + # Append / for directories or @ for symbolic links + size = str(sub.size()) + if stat.S_ISDIR(sub.mode): + displayname = sub.name + "/" + linkname = sub.name + "/" + size = ' ' + if stat.S_ISLNK(sub.mode): + displayname = sub.name + "@" + # Note: a link to a directory displays with @ and links with / + size = ' ' + f.write(""" + + + """ % (urllib.quote(linkname), cgi.escape(displayname), size)) + f.write(""" + + +""") + length = f.tell() + f.seek(0) + self.send_response(200) + self.send_header("Content-type", "text/html") + self.send_header("Content-Length", str(length)) + self.end_headers() + self._send_content(f) + f.close() + + def _get_file(self, path, n): + """Process a request on a file. + + Return value is either a file object, or None (indicating an error). + In either case, the headers are sent. + """ + ctype = self._guess_type(path) + f = n.open() + self.send_response(200) + self.send_header("Content-type", ctype) + self.send_header("Content-Length", str(n.size())) + self.send_header("Last-Modified", self.date_time_string(n.mtime)) + self.end_headers() + self._send_content(f) + f.close() + + def _send_content(self, f): + """Send the content file as the response if necessary.""" + if self.command != 'HEAD': + for blob in chunkyreader(f): + self.wfile.write(blob) + + def _guess_type(self, path): + """Guess the type of a file. + + Argument is a PATH (a filename). + + Return value is a string of the form type/subtype, + usable for a MIME Content-type header. + + The default implementation looks the file's extension + up in the table self.extensions_map, using application/octet-stream + as a default; however it would be permissible (if + slow) to look inside the data to make a better guess. + """ + base, ext = posixpath.splitext(path) + if ext in self.extensions_map: + return self.extensions_map[ext] + ext = ext.lower() + if ext in self.extensions_map: + return self.extensions_map[ext] + else: + return self.extensions_map[''] + + if not mimetypes.inited: + mimetypes.init() # try to read system mime.types + extensions_map = mimetypes.types_map.copy() + extensions_map.update({ + '': 'text/plain', # Default + '.py': 'text/plain', + '.c': 'text/plain', + '.h': 'text/plain', + }) + + +optspec = """ +bup web [[hostname]:port] +-- +""" +o = options.Options('bup web', optspec) +(opt, flags, extra) = o.parse(sys.argv[1:]) + +if len(extra) > 1: + o.fatal("at most one argument expected") + +address = ('127.0.0.1', 8080) +if len(extra) > 0: + addressl = extra[0].split(':', 1) + addressl[1] = int(addressl[1]) + address = tuple(addressl) + +git.check_repo_or_die() +top = vfs.RefList(None) + +httpd = BupHTTPServer(address, BupRequestHandler) +sa = httpd.socket.getsockname() +print "Serving HTTP on %s:%d..." % sa +sys.stdout.flush() +httpd.serve_forever() diff --git a/lib/bup/helpers.py b/lib/bup/helpers.py index 931a1fd..5906dc8 100644 --- a/lib/bup/helpers.py +++ b/lib/bup/helpers.py @@ -1,4 +1,5 @@ import sys, os, pwd, subprocess, errno, socket, select, mmap, stat, re +from bup import _version # Write (blockingly) to sockets that may or may not be in blocking mode. @@ -309,3 +310,21 @@ except ImportError: Sha1 = sha.sha else: Sha1 = hashlib.sha1 + + +def version_date(): + return _version.DATE.split(' ')[0] + +def version_commit(): + return _version.COMMIT + +def version_tag(): + names = _version.NAMES.strip() + assert(names[0] == '(') + assert(names[-1] == ')') + names = names[1:-1] + l = [n.strip() for n in names.split(',')] + for n in l: + if n.startswith('tag: bup-'): + return n[9:] + return 'unknown-%s' % _version.COMMIT[:7] \ No newline at end of file diff --git a/lib/bup/vfs.py b/lib/bup/vfs.py index 6028793..e01c229 100644 --- a/lib/bup/vfs.py +++ b/lib/bup/vfs.py @@ -142,6 +142,9 @@ class _FileReader: self.ofs += len(buf) return buf + def close(self): + pass + class Node: def __init__(self, parent, name, mode, hash): diff --git a/main.py b/main.py index 840e3e5..90d0ec8 100755 --- a/main.py +++ b/main.py @@ -37,6 +37,7 @@ def usage(): midx = 'Index objects to speed up future backups', save = 'Save files into a backup set (note: run "bup index" first)', split = 'Split a single file into its own backup set', + web = 'Launch a web server to examine backup sets', ) log('Common commands:\n') -- 2.39.2
NameSize +
%s%s