Home | History | Annotate | Download | only in davclient
      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 existing 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=True, 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=True, 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=True, 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=True, 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, overwrite=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 'allprop'"""
    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" encoding="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" encoding="utf-8" ?>\n'+body, 'utf-8'), headers=headers)
    264         
    265         
    266     def set_lock(self, path, owner, locktype='exclusive', lockscope='write', depth=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, 'owner':{'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 
    313