]> arthur.barton.de Git - bup.git/commitdiff
Add new 'bup web' command.
authorJoe Beda <joe@bedafamily.com>
Sun, 11 Jul 2010 23:33:36 +0000 (16:33 -0700)
committerAvery Pennarun <apenwarr@gmail.com>
Mon, 12 Jul 2010 06:17:44 +0000 (02:17 -0400)
'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 <joe@bedafamily.com>
Documentation/bup-web.1.md [new file with mode: 0644]
cmd/version-cmd.py
cmd/web-cmd.py [new file with mode: 0755]
lib/bup/helpers.py
lib/bup/vfs.py
main.py

diff --git a/Documentation/bup-web.1.md b/Documentation/bup-web.1.md
new file mode 100644 (file)
index 0000000..a334945
--- /dev/null
@@ -0,0 +1,42 @@
+% bup-ftp(1) Bup %BUP_VERSION%
+% Joe Beda <jbeda@gmail.com>
+% %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.
index 1964ca0187ebe16eed0b81129d652c4509ddcefd..3469086d1293c16afb4911ea36f91833f87f2446 100755 (executable)
@@ -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 (executable)
index 0000000..209e57d
--- /dev/null
@@ -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("""
+<HTML>
+  <HEAD>
+    <TITLE>Directory listing for %(displaypath)s</TITLE>
+    <STYLE>
+      BODY, TABLE { font-family: sans-serif }
+      .dir-name { text-align: left }
+      .dir-size { text-align: right }
+    </STYLE>
+  </HEAD>
+  <BODY>
+    <H2>Directory listing for %(displaypath)s</H2>
+    <TABLE>
+      <TR>
+        <TH class=dir-name>Name</TH>
+        <TH class=dir-size>Size<TH>
+      </TR>
+""" % { '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 = '&nbsp;'
+            if stat.S_ISLNK(sub.mode):
+                displayname = sub.name + "@"
+                # Note: a link to a directory displays with @ and links with /
+                size = '&nbsp;'
+            f.write("""      <TR>
+        <TD class=dir-name><A href="%s">%s</A></TD>
+        <TD class=dir-size>%s</TD>
+      </TR>""" % (urllib.quote(linkname), cgi.escape(displayname), size))
+        f.write("""
+    </UL>
+  </BODY>
+</HTML>""")
+        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()
index 931a1fdfc107187111617c96e39411a6e04a16a0..5906dc8062ed8b0bb1e8103ef920ce0a829d06c1 100644 (file)
@@ -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
index 60287935e0014105ea89937c4d58a662fd3e4ecc..e01c229b498a3816b1ed566625bf5dfa2a1f8192 100644 (file)
@@ -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 840e3e53e2614f0eed29540d5cc2256f45cd4c45..90d0ec8760791477719d6937fd170951e18a8ed9 100755 (executable)
--- 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')