[PATCH 2 of 2 bfiles] Make bfserve serve and accept files over HTTP

alexandru alex at hackd.net
Thu Jun 3 17:47:06 CDT 2010


 bfiles.py |  110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 108 insertions(+), 2 deletions(-)


# HG changeset patch
# User alexandru <alex at hackd.net>
# Date 1275599619 25200
# Node ID 67358cbf0f23d51a017089160b6c4489869abdc2
# Parent  2bfadd624bb7ed9c29350ee9e5a791c3bbea222d
Make bfserve serve and accept files over HTTP

diff -r 2bfadd624bb7 -r 67358cbf0f23 bfiles.py
--- a/bfiles.py	Thu Jun 03 13:58:19 2010 -0700
+++ b/bfiles.py	Thu Jun 03 14:13:39 2010 -0700
@@ -36,6 +36,8 @@
 import copy
 import inspect
 import fnmatch
+import posixpath
+import BaseHTTPServer
 
 from mercurial import \
      hg, cmdutil, util, error, extensions, commands, context, \
@@ -489,7 +491,10 @@
 def bfserve(ui, **opts):
     """export the bfile store via SSH
     """
-    s = sshstoreserver(ui)
+    if opts.get('http'):
+        s = httpstoreserver(ui, **opts)
+    else: 
+        s = sshstoreserver(ui)
     s.serve_forever()
 
 
@@ -1697,6 +1702,102 @@
 
 # -- Private helper store classes --------------------------------------------
 
+class httpputhandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    """bfiles-customized request handler"""
+    def do_PUT(self):
+        relpath = self._normpath()
+        clen = int(self.headers.getheader('content-length'))
+        dirs = os.path.dirname(relpath)
+        if dirs != '' and not os.path.isdir(dirs):
+            util.makedirs(dirs)
+        try:
+            fd = util.atomictempfile(relpath, 'wb', 0644)
+            fhash = self._copier(self.rfile, fd, clen)
+            if fhash != os.path.basename(relpath):
+                self.send_response(httplib.CONFLICT)
+                self.send_header('Conflict', 'resource hash is: %s but'
+                ' the given name was: %s' % (fhash, 
+                os.path.basename(relpath)))
+            else:
+                fd.rename() # hash is good, keep the file
+                self.send_response(httplib.CREATED)
+                self.send_header('Location', self.path)
+        except IOError:
+            self.send_response(httplib.FORBIDDEN)
+            self.send_header('Reason', 'cannot open resource for writing')
+
+        self.end_headers()
+
+    def do_GET(self):
+        """return a file to caller"""
+        if self._send_headers():
+            fpath = self._normpath()
+            clen = os.path.getsize(fpath)
+            with open(fpath, 'rb') as fd:
+                self._copier(fd, self.wfile, clen)
+
+    def do_HEAD(self):
+        """return a file hash to the caller"""
+        self._send_headers()
+
+    def _send_headers(self):
+        relpath = self._normpath()
+        if not os.path.isfile(relpath):
+            self.send_response(httplib.NOT_FOUND)
+            return False
+        clen = int(os.path.getsize(relpath))
+        self.send_response(httplib.OK)
+        self.send_header('Content-length', clen)
+        with open(relpath, 'rb') as fd:
+            self.send_header('Content-SHA1', _hashfile(fd))
+        self.end_headers()
+        return True
+
+    def _copier(self, sin, sout, clen):
+        chunksize = 4096 * 1024 # 4MBs
+        hasher = hashlib.sha1()
+        while clen > 0:
+            if chunksize > clen:
+                chunksize = clen
+            buff = sin.read(chunksize)
+            sout.write(buff)
+            hasher.update(buff)
+            clen -= len(buff)
+        return hasher.hexdigest()
+
+    def _normpath(self):
+        '''normalize and clean the path, and prevent traversal attacks. 
+        adjusted from http://djangobook.com/en/2.0/chapter20/'''
+
+        path = posixpath.normpath(urllib.unquote(self.path))
+        newpath = ''
+        for part in path.split('/'):
+            if not part:
+                continue # strip empty path components
+
+            drive, part = os.path.splitdrive(part)
+            head, part = os.path.split(part)
+            if part in (os.curdir, os.pardir):
+                continue # strip '.' and '..' in path
+
+            newpath = os.path.join(newpath, part).replace('\\', '/')
+        return os.path.abspath(newpath)
+
+class httpstoreserver(object):
+    """Turn bfserve into a valid HTTP PUT target"""
+    def __init__(self, ui, **opts):
+        self.ui = ui
+        self.port = opts.get('port')
+
+    def serve_forever(self):
+        server_class = BaseHTTPServer.HTTPServer
+        httpd = server_class(('localhost', self.port), httpputhandler)
+        try:
+            httpd.serve_forever()
+        except KeyboardInterrupt:
+            pass
+        httpd.server_close()
+
 # XXX *snip* class heavily based on sshserver (in mercurial/sshserver.py).
 
 # We do *not* subclass it here cause it does not have as many 'do_xxx'
@@ -1882,6 +1983,11 @@
                     _('check file contents, not just existence'))],
                   _('hg bfverify [--all] [--contents]')),
     'bfserve' : (bfserve,
-                 [],
+                 [('','http', False,
+                   _('serve over HTTP')),
+                  ('','port', 8080,
+                   _('select HTTP port')),
+                   ('','ssh', True,
+                   _('server over SSH'))],
                  ''),
     }
-------------- next part --------------
A non-text attachment was scrubbed...
Name: PGP.sig
Type: application/pgp-signature
Size: 314 bytes
Desc: not available
URL: <http://selenic.com/pipermail/mercurial-devel/attachments/20100603/7b500a62/attachment.pgp>


More information about the Mercurial-devel mailing list