| OLD | NEW | 
 | (Empty) | 
|    1 #   Copyright (c) 2006-2007 Open Source Applications Foundation |  | 
|    2 # |  | 
|    3 #   Licensed under the Apache License, Version 2.0 (the "License"); |  | 
|    4 #   you may not use this file except in compliance with the License. |  | 
|    5 #   You may obtain a copy of the License at |  | 
|    6 # |  | 
|    7 #       http://www.apache.org/licenses/LICENSE-2.0 |  | 
|    8 # |  | 
|    9 #   Unless required by applicable law or agreed to in writing, software |  | 
|   10 #   distributed under the License is distributed on an "AS IS" BASIS, |  | 
|   11 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | 
|   12 #   See the License for the specific language governing permissions and |  | 
|   13 #   limitations under the License. |  | 
|   14  |  | 
|   15 import urlparse, httplib, copy, base64, StringIO |  | 
|   16 import urllib |  | 
|   17  |  | 
|   18 try: |  | 
|   19     from xml.etree import ElementTree |  | 
|   20 except: |  | 
|   21     from elementtree import ElementTree |  | 
|   22  |  | 
|   23 __all__ = ['DAVClient'] |  | 
|   24  |  | 
|   25 def object_to_etree(parent, obj, namespace=''): |  | 
|   26     """This function takes in a python object, traverses it, and adds it to an e
     xisting etree object""" |  | 
|   27      |  | 
|   28     if type(obj) is int or type(obj) is float or type(obj) is str: |  | 
|   29         # If object is a string, int, or float just add it |  | 
|   30         obj = str(obj) |  | 
|   31         if obj.startswith('{') is False: |  | 
|   32             ElementTree.SubElement(parent, '{%s}%s' % (namespace, obj)) |  | 
|   33         else: |  | 
|   34             ElementTree.SubElement(parent, obj) |  | 
|   35          |  | 
|   36     elif type(obj) is dict: |  | 
|   37         # If the object is a dictionary we'll need to parse it and send it back 
     recusively |  | 
|   38         for key, value in obj.items(): |  | 
|   39             if key.startswith('{') is False: |  | 
|   40                 key_etree = ElementTree.SubElement(parent, '{%s}%s' % (namespace
     , key)) |  | 
|   41                 object_to_etree(key_etree, value, namespace=namespace) |  | 
|   42             else: |  | 
|   43                 key_etree = ElementTree.SubElement(parent, key) |  | 
|   44                 object_to_etree(key_etree, value, namespace=namespace) |  | 
|   45              |  | 
|   46     elif type(obj) is list: |  | 
|   47         # If the object is a list parse it and send it back recursively |  | 
|   48         for item in obj: |  | 
|   49             object_to_etree(parent, item, namespace=namespace) |  | 
|   50              |  | 
|   51     else: |  | 
|   52         # If it's none of previous types then raise |  | 
|   53         raise TypeError, '%s is an unsupported type' % type(obj) |  | 
|   54          |  | 
|   55  |  | 
|   56 class DAVClient(object): |  | 
|   57      |  | 
|   58     def __init__(self, url='http://localhost:8080'): |  | 
|   59         """Initialization""" |  | 
|   60          |  | 
|   61         self._url = urlparse.urlparse(url) |  | 
|   62          |  | 
|   63         self.headers = {'Host':self._url[1],  |  | 
|   64                         'User-Agent': 'python.davclient.DAVClient/0.1'}  |  | 
|   65          |  | 
|   66          |  | 
|   67     def _request(self, method, path='', body=None, headers=None): |  | 
|   68         """Internal request method""" |  | 
|   69         self.response = None |  | 
|   70          |  | 
|   71         if headers is None: |  | 
|   72             headers = copy.copy(self.headers) |  | 
|   73         else: |  | 
|   74             new_headers = copy.copy(self.headers) |  | 
|   75             new_headers.update(headers) |  | 
|   76             headers = new_headers |  | 
|   77          |  | 
|   78         if self._url.scheme == 'http': |  | 
|   79             self._connection = httplib.HTTPConnection(self._url[1]) |  | 
|   80         elif self._url.scheme == 'https': |  | 
|   81             self._connection = httplib.HTTPSConnection(self._url[1]) |  | 
|   82         else: |  | 
|   83             raise Exception, 'Unsupported scheme' |  | 
|   84          |  | 
|   85         self._connection.request(method, path, body, headers) |  | 
|   86              |  | 
|   87         self.response = self._connection.getresponse() |  | 
|   88          |  | 
|   89         self.response.body = self.response.read() |  | 
|   90          |  | 
|   91         # Try to parse and get an etree |  | 
|   92         try: |  | 
|   93             self._get_response_tree() |  | 
|   94         except: |  | 
|   95             pass |  | 
|   96          |  | 
|   97              |  | 
|   98     def _get_response_tree(self): |  | 
|   99         """Parse the response body into an elementree object""" |  | 
|  100         self.response.tree = ElementTree.fromstring(self.response.body) |  | 
|  101         return self.response.tree |  | 
|  102          |  | 
|  103     def set_basic_auth(self, username, password): |  | 
|  104         """Set basic authentication""" |  | 
|  105         auth = 'Basic %s' % base64.encodestring('%s:%s' % (username, password)).
     strip() |  | 
|  106         self._username = username |  | 
|  107         self._password = password |  | 
|  108         self.headers['Authorization'] = auth |  | 
|  109          |  | 
|  110     ## HTTP DAV methods ## |  | 
|  111          |  | 
|  112     def get(self, path, headers=None): |  | 
|  113         """Simple get request""" |  | 
|  114         self._request('GET', path, headers=headers) |  | 
|  115         return self.response.body |  | 
|  116          |  | 
|  117     def head(self, path, headers=None): |  | 
|  118         """Basic HEAD request""" |  | 
|  119         self._request('HEAD', path, headers=headers) |  | 
|  120          |  | 
|  121     def put(self, path, body=None, f=None, headers=None): |  | 
|  122         """Put resource with body""" |  | 
|  123         if f is not None: |  | 
|  124             body = f.read() |  | 
|  125              |  | 
|  126         self._request('PUT', path, body=body, headers=headers) |  | 
|  127          |  | 
|  128     def post(self, path, body=None, headers=None): |  | 
|  129         """POST resource with body""" |  | 
|  130  |  | 
|  131         self._request('POST', path, body=body, headers=headers) |  | 
|  132          |  | 
|  133     def mkcol(self, path, headers=None): |  | 
|  134         """Make DAV collection""" |  | 
|  135         self._request('MKCOL', path=path, headers=headers) |  | 
|  136          |  | 
|  137     make_collection = mkcol |  | 
|  138          |  | 
|  139     def delete(self, path, headers=None): |  | 
|  140         """Delete DAV resource""" |  | 
|  141         self._request('DELETE', path=path, headers=headers) |  | 
|  142          |  | 
|  143     def copy(self, source, destination, body=None, depth='infinity', overwrite=T
     rue, headers=None): |  | 
|  144         """Copy DAV resource""" |  | 
|  145         # Set all proper headers |  | 
|  146         if headers is None: |  | 
|  147             headers = {'Destination':destination} |  | 
|  148         else: |  | 
|  149             headers['Destination'] = self._url.geturl() + destination |  | 
|  150         if overwrite is False: |  | 
|  151             headers['Overwrite'] = 'F' |  | 
|  152         headers['Depth'] = depth |  | 
|  153              |  | 
|  154         self._request('COPY', source, body=body, headers=headers) |  | 
|  155          |  | 
|  156          |  | 
|  157     def copy_collection(self, source, destination, depth='infinity', overwrite=T
     rue, headers=None): |  | 
|  158         """Copy DAV collection""" |  | 
|  159         body = '<?xml version="1.0" encoding="utf-8" ?><d:propertybehavior xmlns
     :d="DAV:"><d:keepalive>*</d:keepalive></d:propertybehavior>' |  | 
|  160          |  | 
|  161         # Add proper headers |  | 
|  162         if headers is None: |  | 
|  163             headers = {} |  | 
|  164         headers['Content-Type'] = 'text/xml; charset="utf-8"' |  | 
|  165          |  | 
|  166         self.copy(source, destination, body=unicode(body, 'utf-8'), depth=depth,
      overwrite=overwrite, headers=headers) |  | 
|  167          |  | 
|  168          |  | 
|  169     def move(self, source, destination, body=None, depth='infinity', overwrite=T
     rue, headers=None): |  | 
|  170         """Move DAV resource""" |  | 
|  171         # Set all proper headers |  | 
|  172         if headers is None: |  | 
|  173             headers = {'Destination':destination} |  | 
|  174         else: |  | 
|  175             headers['Destination'] = self._url.geturl() + destination |  | 
|  176         if overwrite is False: |  | 
|  177             headers['Overwrite'] = 'F' |  | 
|  178         headers['Depth'] = depth |  | 
|  179              |  | 
|  180         self._request('MOVE', source, body=body, headers=headers) |  | 
|  181          |  | 
|  182          |  | 
|  183     def move_collection(self, source, destination, depth='infinity', overwrite=T
     rue, headers=None): |  | 
|  184         """Move DAV collection and copy all properties""" |  | 
|  185         body = '<?xml version="1.0" encoding="utf-8" ?><d:propertybehavior xmlns
     :d="DAV:"><d:keepalive>*</d:keepalive></d:propertybehavior>' |  | 
|  186          |  | 
|  187         # Add proper headers |  | 
|  188         if headers is None: |  | 
|  189             headers = {} |  | 
|  190         headers['Content-Type'] = 'text/xml; charset="utf-8"' |  | 
|  191  |  | 
|  192         self.move(source, destination, unicode(body, 'utf-8'), depth=depth, over
     write=overwrite, headers=headers) |  | 
|  193          |  | 
|  194          |  | 
|  195     def propfind(self, path, properties='allprop', namespace='DAV:', depth=None,
      headers=None): |  | 
|  196         """Property find. If properties arg is unspecified it defaults to 'allpr
     op'""" |  | 
|  197         # Build propfind xml |  | 
|  198         root = ElementTree.Element('{DAV:}propfind') |  | 
|  199         if type(properties) is str: |  | 
|  200             ElementTree.SubElement(root, '{DAV:}%s' % properties) |  | 
|  201         else: |  | 
|  202             props = ElementTree.SubElement(root, '{DAV:}prop') |  | 
|  203             object_to_etree(props, properties, namespace=namespace) |  | 
|  204         tree = ElementTree.ElementTree(root) |  | 
|  205          |  | 
|  206         # Etree won't just return a normal string, so we have to do this |  | 
|  207         body = StringIO.StringIO() |  | 
|  208         tree.write(body) |  | 
|  209         body = body.getvalue() |  | 
|  210                  |  | 
|  211         # Add proper headers |  | 
|  212         if headers is None: |  | 
|  213             headers = {} |  | 
|  214         if depth is not None: |  | 
|  215             headers['Depth'] = depth |  | 
|  216         headers['Content-Type'] = 'text/xml; charset="utf-8"' |  | 
|  217          |  | 
|  218         # Body encoding must be utf-8, 207 is proper response |  | 
|  219         self._request('PROPFIND', path, body=unicode('<?xml version="1.0" encodi
     ng="utf-8" ?>\n'+body, 'utf-8'), headers=headers) |  | 
|  220          |  | 
|  221         if self.response is not None and hasattr(self.response, 'tree') is True: |  | 
|  222             property_responses = {} |  | 
|  223             for response in self.response.tree._children: |  | 
|  224                 property_href = response.find('{DAV:}href') |  | 
|  225                 property_stat = response.find('{DAV:}propstat') |  | 
|  226                  |  | 
|  227                 def parse_props(props): |  | 
|  228                     property_dict = {} |  | 
|  229                     for prop in props: |  | 
|  230                         if prop.tag.find('{DAV:}') is not -1: |  | 
|  231                             name = prop.tag.split('}')[-1] |  | 
|  232                         else: |  | 
|  233                             name = prop.tag |  | 
|  234                         if len(prop._children) is not 0: |  | 
|  235                             property_dict[name] = parse_props(prop._children) |  | 
|  236                         else: |  | 
|  237                             property_dict[name] = prop.text |  | 
|  238                     return property_dict |  | 
|  239                  |  | 
|  240                 if property_href is not None and property_stat is not None: |  | 
|  241                     property_dict = parse_props(property_stat.find('{DAV:}prop')
     ._children) |  | 
|  242                     property_responses[property_href.text] = property_dict |  | 
|  243             return property_responses |  | 
|  244          |  | 
|  245     def proppatch(self, path, set_props=None, remove_props=None, namespace='DAV:
     ', headers=None): |  | 
|  246         """Patch properties on a DAV resource. If namespace is not specified the
      DAV namespace is used for all properties""" |  | 
|  247         root = ElementTree.Element('{DAV:}propertyupdate') |  | 
|  248          |  | 
|  249         if set_props is not None: |  | 
|  250             prop_set = ElementTree.SubElement(root, '{DAV:}set') |  | 
|  251             object_to_etree(prop_set, set_props, namespace=namespace) |  | 
|  252         if remove_props is not None: |  | 
|  253             prop_remove = ElementTree.SubElement(root, '{DAV:}remove') |  | 
|  254             object_to_etree(prop_remove, remove_props, namespace=namespace) |  | 
|  255          |  | 
|  256         tree = ElementTree.ElementTree(root) |  | 
|  257          |  | 
|  258         # Add proper headers |  | 
|  259         if headers is None: |  | 
|  260             headers = {} |  | 
|  261         headers['Content-Type'] = 'text/xml; charset="utf-8"' |  | 
|  262          |  | 
|  263         self._request('PROPPATCH', path, body=unicode('<?xml version="1.0" encod
     ing="utf-8" ?>\n'+body, 'utf-8'), headers=headers) |  | 
|  264          |  | 
|  265          |  | 
|  266     def set_lock(self, path, owner, locktype='exclusive', lockscope='write', dep
     th=None, headers=None): |  | 
|  267         """Set a lock on a dav resource""" |  | 
|  268         root = ElementTree.Element('{DAV:}lockinfo') |  | 
|  269         object_to_etree(root, {'locktype':locktype, 'lockscope':lockscope, 'owne
     r':{'href':owner}}, namespace='DAV:') |  | 
|  270         tree = ElementTree.ElementTree(root) |  | 
|  271          |  | 
|  272         # Add proper headers |  | 
|  273         if headers is None: |  | 
|  274             headers = {} |  | 
|  275         if depth is not None: |  | 
|  276             headers['Depth'] = depth |  | 
|  277         headers['Content-Type'] = 'text/xml; charset="utf-8"' |  | 
|  278         headers['Timeout'] = 'Infinite, Second-4100000000' |  | 
|  279          |  | 
|  280         self._request('LOCK', path, body=unicode('<?xml version="1.0" encoding="
     utf-8" ?>\n'+body, 'utf-8'), headers=headers) |  | 
|  281          |  | 
|  282         locks = self.response.etree.finall('.//{DAV:}locktoken') |  | 
|  283         lock_list = [] |  | 
|  284         for lock in locks: |  | 
|  285             lock_list.append(lock.getchildren()[0].text.strip().strip('\n')) |  | 
|  286         return lock_list |  | 
|  287          |  | 
|  288  |  | 
|  289     def refresh_lock(self, path, token, headers=None): |  | 
|  290         """Refresh lock with token""" |  | 
|  291          |  | 
|  292         if headers is None: |  | 
|  293             headers = {} |  | 
|  294         headers['If'] = '(<%s>)' % token |  | 
|  295         headers['Timeout'] = 'Infinite, Second-4100000000' |  | 
|  296          |  | 
|  297         self._request('LOCK', path, body=None, headers=headers) |  | 
|  298          |  | 
|  299          |  | 
|  300     def unlock(self, path, token, headers=None): |  | 
|  301         """Unlock DAV resource with token""" |  | 
|  302         if headers is None: |  | 
|  303             headers = {} |  | 
|  304         headers['Lock-Tocken'] = '<%s>' % token |  | 
|  305          |  | 
|  306         self._request('UNLOCK', path, body=None, headers=headers) |  | 
|  307          |  | 
|  308  |  | 
|  309  |  | 
|  310  |  | 
|  311  |  | 
|  312  |  | 
| OLD | NEW |