bfiles-httpstore / httpserve.patch

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

diff --git a/bfiles.py b/bfiles.py
--- a/bfiles.py
+++ b/bfiles.py
@@ -36,6 +36,8 @@
 import copy
 import inspect
 import fnmatch
+import posixpath
+import BaseHTTPServer
 
 from mercurial import \
      hg, cmdutil, util, error, extensions, commands, context, \
@@ -488,7 +490,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()
 
 
@@ -1717,6 +1722,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'
@@ -1902,6 +2003,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'))],
                  ''),
     }
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.