Rev 50: First implementation for stat. Some test are still failing. in http://bazaar.launchpad.net/%7Ebzr/bzr.webdav/webdav

Vincent Ladeuil v.ladeuil+lp at free.fr
Sun Jun 8 20:34:38 BST 2008


At http://bazaar.launchpad.net/%7Ebzr/bzr.webdav/webdav

------------------------------------------------------------
revno: 50
revision-id: v.ladeuil+lp at free.fr-20080608193435-02kt8x2camnawwmz
parent: v.ladeuil+lp at free.fr-20080608104358-0n80zg1lz9lbth51
committer: Vincent Ladeuil <v.ladeuil+lp at free.fr>
branch nick: webdav
timestamp: Sun 2008-06-08 21:34:35 +0200
message:
  First implementation for stat. Some test are still failing.
  
  * webdav.py:
  (DavResponseHandler): Extracted from DavStatHandler and
  DavListDirHandler.
  (DavListDirHandler): New class. Updated after some modifications
  in DavResponseHandler.
  (_extract_dir_content): merge DavListDirHandler.get_dir_content.
  (DavStatHandler): New class.
  (_DAVStat): New class.
  (_extract_stat_info): New helper.
  (HttpDavTransport.list_dir): D:prop is enough to get file list.
  (HttpDavTransport.stat): Implement stat.
  
  * test_webdav.py:
  (TestDavSaxParser): Add tests for DavStatHandler.
modified:
  TODO                           todo-20060820113924-ioaocfzvsb4wq9z1-1
  test_webdav.py                 test_webdav.py-20060823130244-qvg4wqdodnmf5nhs-1
  webdav.py                      webdav.py-20060816232542-enpjxth2743ttqpq-3
-------------- next part --------------
=== modified file 'TODO'
--- a/TODO	2007-11-04 17:44:22 +0000
+++ b/TODO	2008-06-08 19:34:35 +0000
@@ -1,4 +1,10 @@
-- add specific tests
-
-- handle the TODOs in webdav.py
+* handle PROPFIND in the test server
+
+** handle depths 0, 1, Infinity
+
+** handle prop and allprop for file and directory as apache2
+
+* handle the TODOs in webdav.py
+
+** move the relevant TODOs in webdav.py and elsewhere here :)
 

=== modified file 'test_webdav.py'
--- a/test_webdav.py	2008-06-08 10:43:58 +0000
+++ b/test_webdav.py	2008-06-08 19:34:35 +0000
@@ -34,6 +34,7 @@
 import socket
 import string
 import shutil
+import stat
 import sys
 import time
 import urlparse
@@ -422,6 +423,48 @@
 
 class TestDavSaxParser(tests.TestCase):
 
+    def _extract_dir_content_from_str(self, str):
+        return webdav._extract_dir_content(
+            'http://localhost/blah', StringIO(str))
+
+    def _extract_stat_from_str(self, str):
+        return webdav._extract_stat_info(
+            'http://localhost/blah', StringIO(str))
+
+    def test_unkown_format_response(self):
+        # Valid but unrelated xml
+        example = """<document/>"""
+        self.assertRaises(errors.InvalidHttpResponse,
+                          self._extract_dir_content_from_str, example)
+
+    def test_list_dir_malformed_response(self):
+        # Invalid xml, neither multistatus nor response are properly closed
+        example = """<?xml version="1.0" encoding="utf-8"?>
+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
+<D:response>
+<D:href>http://localhost/</D:href>"""
+        self.assertRaises(errors.InvalidHttpResponse,
+                          self._extract_dir_content_from_str, example)
+
+    def test_list_dir_incomplete_format_response(self):
+        # The minimal information is present but doesn't conform to RFC 2518
+        # (well, as I understand it since the reference servers disagree on
+        # more than details).
+
+        # The last href below is not enclosed in a response element and is
+        # therefore ignored.
+        example = """<?xml version="1.0" encoding="utf-8"?>
+<D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
+<D:response>
+<D:href>http://localhost/</D:href>
+</D:response>
+<D:response>
+<D:href>http://localhost/titi</D:href>
+</D:response>
+<D:href>http://localhost/toto</D:href>
+</D:multistatus>"""
+        self.assertEqual(['titi'], self._extract_dir_content_from_str(example))
+
     def test_list_dir_apache2_example(self):
         example = """<?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
@@ -459,8 +502,7 @@
     </D:response>
 </D:multistatus>"""
         self.assertEqual(['a', 'b', 'c'],
-                         webdav._extract_dir_content('http://localhost/blah',
-                                                     StringIO(example)))
+                         self._extract_dir_content_from_str(example))
 
     def test_list_dir_lighttpd_example(self):
         example = """<?xml version="1.0" encoding="utf-8"?>
@@ -476,43 +518,224 @@
 </D:response>
 </D:multistatus>"""
         self.assertEqual(['titi', 'toto'],
-                         webdav._extract_dir_content('http://localhost/blah',
-                                                     StringIO(example)))
+                         self._extract_dir_content_from_str(example))
 
-    def test_list_dir_malformed_response(self):
+    def test_stat_malformed_response(self):
         # Invalid xml, neither multistatus nor response are properly closed
         example = """<?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
 <D:response>
 <D:href>http://localhost/</D:href>"""
         self.assertRaises(errors.InvalidHttpResponse,
-                          webdav._extract_dir_content,
-                          'http://localhost/blah', StringIO(example))
-
-    def test_unkown_format_response(self):
-        # Valid but unrelated xml
-        example = """<document/>"""
-        self.assertRaises(errors.InvalidHttpResponse,
-                          webdav._extract_dir_content,
-                          'http://localhost/blah', StringIO(example))
-
-    def test_list_dir_incomplete_format_response(self):
+                          self._extract_stat_from_str, example)
+
+    def test_stat_incomplete_format_response(self):
         # The minimal information is present but doesn't conform to RFC 2518
         # (well, as I understand it since the reference servers disagree on
         # more than details).
 
-        # The last href below is not enclosed in a response element and is
+        # The href below is not enclosed in a response element and is
         # therefore ignored.
         example = """<?xml version="1.0" encoding="utf-8"?>
 <D:multistatus xmlns:D="DAV:" xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
-<D:response>
-<D:href>http://localhost/</D:href>
-</D:response>
-<D:response>
-<D:href>http://localhost/titi</D:href>
-</D:response>
 <D:href>http://localhost/toto</D:href>
 </D:multistatus>"""
-        self.assertEqual(['titi'],
-                         webdav._extract_dir_content('http://localhost/blah',
-                                                     StringIO(example)))
+        self.assertRaises(errors.InvalidHttpResponse,
+                          self._extract_stat_from_str, example)
+
+    def test_stat_apache2_file_example(self):
+        example = """<?xml version="1.0" encoding="utf-8"?>
+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/executable</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype/>
+<lp1:creationdate>2008-06-08T09:50:15Z</lp1:creationdate>
+<lp1:getcontentlength>12</lp1:getcontentlength>
+<lp1:getlastmodified>Sun, 08 Jun 2008 09:50:11 GMT</lp1:getlastmodified>
+<lp1:getetag>"da9f81-0-9ef33ac0"</lp1:getetag>
+<lp2:executable>T</lp2:executable>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+</D:multistatus>"""
+        st = self._extract_stat_from_str(example)
+        self.assertEquals(12, st.st_size)
+        self.assertFalse(stat.S_ISDIR(st.st_mode))
+        self.assertTrue(stat.S_ISREG(st.st_mode))
+        self.assertTrue(st.st_mode & stat.S_IXUSR)
+
+    def test_stat_apache2_dir_depth_1_example(self):
+        example = """<?xml version="1.0" encoding="utf-8"?>
+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype><D:collection/></lp1:resourcetype>
+<lp1:creationdate>2008-06-08T10:50:38Z</lp1:creationdate>
+<lp1:getlastmodified>Sun, 08 Jun 2008 10:50:38 GMT</lp1:getlastmodified>
+<lp1:getetag>"da7f5a-cc-7722db80"</lp1:getetag>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/executable</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype/>
+<lp1:creationdate>2008-06-08T09:50:15Z</lp1:creationdate>
+<lp1:getcontentlength>0</lp1:getcontentlength>
+<lp1:getlastmodified>Sun, 08 Jun 2008 09:50:11 GMT</lp1:getlastmodified>
+<lp1:getetag>"da9f81-0-9ef33ac0"</lp1:getetag>
+<lp2:executable>T</lp2:executable>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/read-only</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype/>
+<lp1:creationdate>2008-06-08T09:50:11Z</lp1:creationdate>
+<lp1:getcontentlength>0</lp1:getcontentlength>
+<lp1:getlastmodified>Sun, 08 Jun 2008 09:50:11 GMT</lp1:getlastmodified>
+<lp1:getetag>"da9f80-0-9ef33ac0"</lp1:getetag>
+<lp2:executable>F</lp2:executable>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/titi</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype/>
+<lp1:creationdate>2008-06-08T09:49:53Z</lp1:creationdate>
+<lp1:getcontentlength>6</lp1:getcontentlength>
+<lp1:getlastmodified>Sun, 08 Jun 2008 09:49:53 GMT</lp1:getlastmodified>
+<lp1:getetag>"da8cbc-6-9de09240"</lp1:getetag>
+<lp2:executable>F</lp2:executable>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/toto/</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype><D:collection/></lp1:resourcetype>
+<lp1:creationdate>2008-06-06T08:07:07Z</lp1:creationdate>
+<lp1:getlastmodified>Fri, 06 Jun 2008 08:07:07 GMT</lp1:getlastmodified>
+<lp1:getetag>"da8cb9-44-f2ac20c0"</lp1:getetag>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+</D:multistatus>
+"""
+        self.assertRaises(errors.InvalidHttpResponse,
+                          self._extract_stat_from_str, example)
+
+    def test_stat_apache2_dir_depth_0_example(self):
+        example = """<?xml version="1.0" encoding="utf-8"?>
+<D:multistatus xmlns:D="DAV:" xmlns:ns0="DAV:">
+<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
+<D:href>/</D:href>
+<D:propstat>
+<D:prop>
+<lp1:resourcetype><D:collection/></lp1:resourcetype>
+<lp1:creationdate>2008-06-08T10:50:38Z</lp1:creationdate>
+<lp1:getlastmodified>Sun, 08 Jun 2008 10:50:38 GMT</lp1:getlastmodified>
+<lp1:getetag>"da7f5a-cc-7722db80"</lp1:getetag>
+<D:supportedlock>
+<D:lockentry>
+<D:lockscope><D:exclusive/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+<D:lockentry>
+<D:lockscope><D:shared/></D:lockscope>
+<D:locktype><D:write/></D:locktype>
+</D:lockentry>
+</D:supportedlock>
+<D:lockdiscovery/>
+</D:prop>
+<D:status>HTTP/1.1 200 OK</D:status>
+</D:propstat>
+</D:response>
+</D:multistatus>
+"""
+        st = self._extract_stat_from_str(example)
+        self.assertEquals(None, st.st_size)
+        self.assertTrue(stat.S_ISDIR(st.st_mode))
+        self.assertTrue(st.st_mode & stat.S_IXUSR)

=== modified file 'webdav.py'
--- a/webdav.py	2008-06-08 10:43:58 +0000
+++ b/webdav.py	2008-06-08 19:34:35 +0000
@@ -66,21 +66,10 @@
 
 # TODO: Factor out the error handling.
 
-# TODO: implement list_dir, it's currently used by the pack format
-# The pack format is still experimental but may become the default
-# format in the near future (2007-11-04).
-# 
-# list_dir is considered usable for writable transports:
-# 
-# <lifeless> there is no technical reason I know of yet to avoid
-#    list_dir for writable transports
-# <lifeless> I plan in a future packs format to see if we can remove list_dir
-# <lifeless> but for the current format there is no alternative
-# <lifeless> that isn't worse.
-
 from cStringIO import StringIO
 import os
 import random
+import re
 import sys
 import time
 import urllib2
@@ -102,18 +91,14 @@
 
 
 class DavResponseHandler(xml.sax.handler.ContentHandler):
-    """Handle a mutli-status DAV response.
-
-    Currently this class focus on handling a response for a PROPFIND request of
-    depth 1 targeted as getting the content of a directory. This may evolve to
-    handle more responses.
-    """
+    """Handle a multi-status DAV response."""
 
     def __init__(self):
         self.url = None
-        self.dir_content = None
         self.elt_stack = None
         self.chars = None
+        self.chars_wanted = False
+        self.expected_content_handled = False
 
     def set_url(self, url):
         """Set the url used for error reporting when handling a response."""
@@ -121,63 +106,79 @@
 
     def startDocument(self):
         self.elt_stack = []
-        self.dir_content = None
         self.chars = None
+        self.expected_content_handled = False
 
     def endDocument(self):
-        if self.dir_content is None:
+        self._validate_handling()
+        if not self.expected_content_handled:
             raise errors.InvalidHttpResponse(self.url,
                                              msg='Unknown xml response')
 
     def startElement(self, name, attrs):
-        self.elt_stack.append(name)
-        if name == 'D:href':
-            self.chars = []
+        self.elt_stack.append(self._strip_ns(name))
+        # The following is incorrect in the general case where elements are
+        # intermixed with chars in a higher level element. That's not the case
+        # here (otherwise the chars_wanted will have to be stacked too).
+        if self.chars_wanted:
+            self.chars = ''
+        else:
+            self.chars = None
+
+    def endElement(self, name):
+        self.chars = None
+        self.chars_wanted = False
+        self.elt_stack.pop()
+
+    def characters(self, chrs):
+        if self.chars_wanted:
+            self.chars += chrs
+
+    def _current_element(self):
+        return self.elt_stack[-1]
+
+    def _strip_ns(self, name):
+        """Strip the leading namespace from name.
+
+        We don't have namespaces clashes in our context, stripping it makes the
+        code simpler.
+        """
+        where = name.find(':')
+        if where == -1:
+            return name
+        else:
+            return name[where +1:]
+
+
+class DavListDirHandler(DavResponseHandler):
+    """Handle a PROPPFIND depth 1 DAV response for a directory.
+
+    The expected content is a multi-status containing a list of response
+    containing at least a href property.
+    """
+    def __init__(self):
+        DavResponseHandler.__init__(self)
+        self.dir_content = None
+
+    def _validate_handling(self):
+        if self.dir_content is not None:
+            self.expected_content_handled = True
+
+    def startElement(self, name, attrs):
+        self.chars_wanted = (self._strip_ns(name) == 'href')
+        DavResponseHandler.startElement(self, name, attrs)
 
     def endElement(self, name):
         stack = self.elt_stack
         if (len(stack) == 3
-            and stack[0] == 'D:multistatus'
-            and stack[1] == 'D:response'
-            and name == 'D:href'): # sax guarantees that stack[2] is also D:href
+            and stack[0] == 'multistatus'
+            and stack[1] == 'response'
+             # sax guarantees that name is also href (when ns is stripped)
+            and stack[2] == 'href'):
             if self.dir_content is None:
                 self.dir_content = []
-            self.dir_content.append(''.join(self.chars))
-        self.chars = None
-        stack.pop()
-
-    def characters(self, chrs):
-        if self._current_element() == 'D:href':
-            self.chars.append(chrs)
-
-    def _current_element(self):
-        return self.elt_stack[-1]
-
-    def get_dir_content(self):
-        # Surprisingly enough (or not), our two references DAV servers disagree
-        # on almost every detail, expect using xml.
-        # For the href element:
-
-        # - apache2 use the path part of the URL (i.e. http://host/path) and
-        #   append a '/' to directory names.
-
-        # - lighttpd use the full URL (i.e. /path) and doesn't distinguish
-        #   between files and directories.
-
-        # Fortunately they both put the directory requested in front of the
-        # list. So we take that directory and strip it from all other
-        # elements...
-        dir = self.dir_content[0]
-        dir_len = len(dir)
-        elements = []
-        for href in self.dir_content[1:]: # Ignore first element
-            if href.startswith(dir):
-                name = href[dir_len:]
-                if name.endswith('/'):
-                    # Get rid of final '/'
-                    name = name[0:-1]
-                elements.append(name)
-        return elements
+            self.dir_content.append(self.chars)
+        DavResponseHandler.endElement(self, name)
 
 
 def _extract_dir_content(url, infile):
@@ -188,16 +189,152 @@
     """
     parser = xml.sax.make_parser()
 
-    handler = DavResponseHandler()
-    parser.setContentHandler(handler)
-    try:
-        parser.parse(infile)
-    except xml.sax.SAXParseException, e:
-        raise errors.InvalidHttpResponse(
-            url, msg='Malformed xml response: %s' % e)
-    # We receive already url-encoded strings so down-casting is safe. And bzr
-    # insists on getting strings not unicode strings.
-    return map(str, handler.get_dir_content())
+    handler = DavListDirHandler()
+    handler.set_url(url)
+    parser.setContentHandler(handler)
+    try:
+        parser.parse(infile)
+    except xml.sax.SAXParseException, e:
+        raise errors.InvalidHttpResponse(
+            url, msg='Malformed xml response: %s' % e)
+    # Reformat for bzr needs
+    dir_content = handler.dir_content
+    dir = dir_content[0]
+    dir_len = len(dir)
+    elements = []
+    for href in dir_content[1:]: # Ignore first element
+        if href.startswith(dir):
+            name = href[dir_len:]
+            if name.endswith('/'):
+                # Get rid of final '/'
+                name = name[0:-1]
+            # We receive already url-encoded strings so down-casting is
+            # safe. And bzr insists on getting strings not unicode strings.
+            elements.append(str(name))
+    return elements
+
+
+class DavStatHandler(DavResponseHandler):
+    """Handle a PROPPFIND depth 0 DAV response for a file or directory.
+
+    The expected content is:
+    - a multi-status element containing
+      - a single response element containing
+        - a href element
+        - a propstat element containing
+          - a status element (ignored)
+          - a prop element containing at least (other are ignored)
+            - a getcontentlength element (for files only)
+            - an executable element (for files only)
+    """
+
+    def __init__(self):
+        DavResponseHandler.__init__(self)
+        self.href = None
+        self.length = None
+        self.executable = None
+        # Flags defining the context for the actions
+        self._response_seen = False
+
+    def _validate_handling(self):
+        if self.href is not None:
+            self.expected_content_handled = True
+
+    def startElement(self, name, attrs):
+        sname = self._strip_ns(name)
+        self.chars_wanted = sname in ('href', 'getcontentlength', 'executable')
+        DavResponseHandler.startElement(self, name, attrs)
+
+    def endElement(self, name):
+        sname = self._strip_ns(name)
+        if self._response_seen:
+            if sname != 'multistatus':
+                raise errors.InvalidHttpResponse(
+                    self.url, msg='Unexpected %s element' % name)
+        else:
+            # We process only the first response (just in case)
+            if self._href_end():
+                self.href = self.chars
+            elif self._getcontentlength_end():
+                self.length = self.chars
+            elif self._executable_end():
+                self.executable = self.chars
+        if sname == 'response':
+            self._response_seen = True
+        DavResponseHandler.endElement(self, name)
+
+    def _href_end(self):
+        stack = self.elt_stack
+        return (len(stack) == 3
+                and stack[0] == 'multistatus'
+                and stack[1] == 'response'
+                and stack[2] == 'href')
+
+    def _getcontentlength_end(self):
+        stack = self.elt_stack
+        return (len(stack) == 5
+                and stack[0] == 'multistatus'
+                and stack[1] == 'response'
+                and stack[2] == 'propstat'
+                and stack[3] == 'prop'
+                and stack[4] == 'getcontentlength')
+
+    def _executable_end(self):
+        stack = self.elt_stack
+        return (len(stack) == 5
+                and stack[0] == 'multistatus'
+                and stack[1] == 'response'
+                and stack[2] == 'propstat'
+                and stack[3] == 'prop'
+                and stack[4] == 'executable')
+
+
+class _DAVStat(object):
+    """The stat info as it can be acquired with DAV."""
+
+    def __init__(self, size, is_dir, is_exec):
+        self.st_size = size
+        # We build a mode considering that:
+
+        # - we have no idea about group or other chmod bits so we use a sane
+        #   default (bzr should not care anyway)
+
+        # - we suppose that the user can write
+        if is_dir:
+            self.st_mode = 0040644
+        else:
+            self.st_mode = 0100644
+        if is_exec:
+            self.st_mode = self.st_mode | 0755
+
+
+def _extract_stat_info(url, infile):
+    """Extract the stat-like information from a DAV PROPFIND response.
+
+    :param url: The url used for the PROPFIND request.
+    :param infile: A file-like object pointing at the start of the response.
+    """
+    parser = xml.sax.make_parser()
+
+    handler = DavStatHandler()
+    handler.set_url(url)
+    parser.setContentHandler(handler)
+    try:
+        parser.parse(infile)
+    except xml.sax.SAXParseException, e:
+        raise errors.InvalidHttpResponse(
+            url, msg='Malformed xml response: %s' % e)
+    is_dir = (handler.href is not None
+              and handler.length is None
+              and handler.executable is None)
+    if is_dir:
+        size = None # directory sizes are meaningless for bzr
+        is_exec = True
+    else:
+        size = int(handler.length)
+        is_exec = (handler.executable == 'T')
+    return _DAVStat(size, is_dir, is_exec)
+
 
 class PUTRequest(_urllib2_wrappers.Request):
 
@@ -601,7 +738,7 @@
         abspath = self._remote_path(relpath)
         propfind = """<?xml version="1.0" encoding="utf-8" ?>
    <D:propfind xmlns:D="DAV:">
-     <D:allprop/>
+     <D:prop/>
    </D:propfind>
 """
         request = _urllib2_wrappers.Request('PROPFIND', abspath, propfind,
@@ -616,11 +753,6 @@
             # More precisely some intermediate directories are missing
             raise errors.NoSuchFile(abspath)
         if code != 207:
-            # As we don't want  to accept overwriting abs_to, 204
-            # (meaning  abs_to  was   existing  (but  empty,  the
-            # non-empty case is 412))  will be an error, a server
-            # bug  even,  since  we  require explicitely  to  not
-            # overwrite.
             self._raise_http_error(abspath, response,
                                    'unable to list  %r directory' % (abspath))
         return _extract_dir_content(abspath, response)
@@ -642,6 +774,33 @@
         """See Transport.rmdir."""
         self.delete(relpath) # That was easy thanks DAV
 
+    def stat(self, relpath):
+        """See Transport.stat.
+
+        We provide a limited implementation for bzr needs.
+        """
+        abspath = self._remote_path(relpath)
+        propfind = """<?xml version="1.0" encoding="utf-8" ?>
+   <D:propfind xmlns:D="DAV:">
+     <D:allprop/>
+   </D:propfind>
+"""
+        request = _urllib2_wrappers.Request('PROPFIND', abspath, propfind,
+                                            {'Depth': 0},
+                                            accepted_errors=[207, 404, 409,])
+        response = self._perform(request)
+
+        code = response.code
+        if code == 404:
+            raise errors.NoSuchFile(abspath)
+        if code == 409:
+            # More precisely some intermediate directories are missing
+            raise errors.NoSuchFile(abspath)
+        if code != 207:
+            self._raise_http_error(abspath, response,
+                                   'unable to list  %r directory' % (abspath))
+        return _extract_stat_info(abspath, response)
+
     # TODO: Before
     # www.ietf.org/internet-drafts/draft-suma-append-patch-00.txt
     # becomes  a real  RFC and  gets implemented,  we can  try to



More information about the bazaar-commits mailing list