Source

heechee / heechee / webdav / props.py

Full commit

from lxml.etree import tostring, Element, SubElement
from werkzeug.wrappers import Response
from werkzeug.exceptions import BadRequest, NotFound

from heechee.constants import *
from heechee.repo import Directory, File

class PropsMixin(object):
    
    """
    Mixin that provides the PROPFIND method and related private methods.
    """
    
    def PROPFIND(self, request):
        """
        Responds to SVN's PROPFIND requests, which usually detail finding out
        various things about the repo.
        """
        # Try getting a cached copy
        cache_key = self._cache_key(request)
        try:
            return self.cache[cache_key]
        except KeyError:
            pass
        
        # Find out what properties they want
        tree = request.xml_body
        
        try:
            props = [x.tag.split("}")[-1] for x in tree.find(DAV_NS+"prop").getchildren()]
        except AttributeError:
            # It might be allprop
            if tree.find(DAV_NS + "allprop") is not None:
                props = ["checked-in", "version-controlled-configuration", "resourcetype", "baseline-relative-path", "repository-uuid"]
            else:
                return BadRequest()
        
        top_revision = str(self.repo.get_top_revision())
        revision = top_revision
        depth = int(request.headers.get("Depth", 0))
        
        if "!svn" not in request.path:
            # If it doesn't exist, don't show it.
            file_ = self.repo.get_file(request.path, revision)

            if file_ is None:
                return NotFound()

            # Return a response
            answers = self._props_for_file(request, "", props, top_revision, isinstance(file_, Directory))
            response = self._propstat_response([(request.path, answers)])
        
        # They're asking for the Version Control Configuration.
        # Basically, tell them the paths of the top revision.
        elif request.path == "/!svn/vcc/default":
            answers = self._props_for_file(request, "", props, top_revision)
            response = self._propstat_response([(request.path, answers)])
        
        # This is the "BLN", which I think means 'Baseline'.
        # Isn't blindly returning values fun?
        elif request.path.startswith("/!svn/bln/"):
            revision = request.path.strip("/").split("/")[2]
            answers = self._props_for_file(request, "", ["baseline-collection", "version-name"], revision)
            response = self._propstat_response([(request.path, answers)])
        
        # Baseline collections.
        elif request.path.startswith("/!svn/bc/"):            
            parts = request.path.strip("/").split("/")
            revision = parts[2]
            path = "/".join(parts[3:])
            
            # How deep do they want to go?
            if depth == 0:
                file_ = self.repo.get_file(path, revision)
                
                if isinstance(file_, File):
                    paths = [(path, False)]
                else:
                    paths = [("", True)]
            elif depth == 1:
                paths = [("", True)]
                for name, item in self.repo.get_file(path, revision).children.items():
                    paths.append((name, isinstance(item, Directory)))
            else:
                raise BadRequest("Cannot PROPFIND more than 1 deep.")
                
            response = self._propstat_response([
                (
                    path,
                    self._props_for_file(request, path, props, revision, is_dir),
                )
                for path, is_dir in paths
            ])
        
        # We don't recognise it! How unlikely.
        else:
            raise BadRequest()
        
        # Cache the response
        self.cache[cache_key] = response
        
        return Response(response, status = 207)

    
    def _props_for_file(self, request, path, props, revision, is_dir=True):
        """
        Given the set or list of props, and a file path, returns a correct
        dictionary of property values.
        """
        answers = {}
        if "baseline-collection" in props:
            answers[DAV_NS+"baseline-collection"] = self._href(request.script_root + "/!svn/bc/%s/" % revision)
        if "resourcetype" in props:
            # If it's a directory, that's a "collection". Files get an empty response.
            if is_dir:
                answers[DAV_NS+"resourcetype"] = Element(DAV_NS+"collection")
            else:
                answers[DAV_NS+"resourcetype"] = None
        if "repository-uuid" in props:
            answers[SVN_DAV_NS+"repository-uuid"] = self.repo.uuid
        if "baseline-relative-path" in props:
            answers[SVN_DAV_NS+"baseline-relative-path"] = (request.path.rstrip("/") + "/" + path.strip("/")).strip("/")
        if "version-controlled-configuration" in props:
            answers[DAV_NS+"version-controlled-configuration"] = self._href(request.script_root + "/!svn/vcc/default")
        if "checked-in" in props:
            if "!svn" in request.path:
                answers[DAV_NS+"checked-in"] = self._href(request.script_root + "/!svn/bln/%s/" % revision)
            else:
                answers[DAV_NS+"checked-in"] = self._href(request.script_root + "/!svn/ver/%s/%s" % (revision, request.path.strip("/")))
        if "version-name" in props:
            answers[DAV_NS+"version-name"] = revision
        if "md5-checksum" in props:
            answers[SVN_DAV_NS+"md5-checksum"] = self.repo.get_file(path, revision).md5()
        return answers
    
    def _propstat_response(self, path_props, href_path=None, status="HTTP/1.1 200 OK"):
        "Creates a multi-status response."
        multi = Element(DAV_NS + "multistatus")
        for path, props in path_props:
            response = SubElement(multi, DAV_NS + "response")
            # Path of this request
            href = SubElement(response, DAV_NS + "href")
            href.text = href_path or path
            # It's gonna be a propstat
            propstat = SubElement(response, DAV_NS + "propstat")
            prop = SubElement(propstat, DAV_NS + "prop")
            # Add in the props we're told about
            for tag, value in props.items():
                elem = SubElement(prop, tag)
                # Is it a string value?
                if isinstance(value, basestring):
                    elem.text = value
                # Is it None? (do nothing)
                elif value is None:
                    pass
                # Then it's hopefully an element.
                else:
                    elem.append(value)
            # Finally, add in the status
            SubElement(propstat, DAV_NS + "status").text = status
        return tostring(multi)