# Copyright (c) 2006-2007 Open Source Applications Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import urlparse, httplib, copy, base64, StringIO import urllib try: from xml.etree import ElementTree except: from elementtree import ElementTree __all__ = ['DAVClient'] def object_to_etree(parent, obj, namespace=''): """This function takes in a python object, traverses it, and adds it to an existing etree object""" if type(obj) is int or type(obj) is float or type(obj) is str: # If object is a string, int, or float just add it obj = str(obj) if obj.startswith('{') is False: ElementTree.SubElement(parent, '{%s}%s' % (namespace, obj)) else: ElementTree.SubElement(parent, obj) elif type(obj) is dict: # If the object is a dictionary we'll need to parse it and send it back recusively for key, value in obj.items(): if key.startswith('{') is False: key_etree = ElementTree.SubElement(parent, '{%s}%s' % (namespace, key)) object_to_etree(key_etree, value, namespace=namespace) else: key_etree = ElementTree.SubElement(parent, key) object_to_etree(key_etree, value, namespace=namespace) elif type(obj) is list: # If the object is a list parse it and send it back recursively for item in obj: object_to_etree(parent, item, namespace=namespace) else: # If it's none of previous types then raise raise TypeError, '%s is an unsupported type' % type(obj) class DAVClient(object): def __init__(self, url='http://localhost:8080'): """Initialization""" self._url = urlparse.urlparse(url) self.headers = {'Host':self._url[1], 'User-Agent': 'python.davclient.DAVClient/0.1'} def _request(self, method, path='', body=None, headers=None): """Internal request method""" self.response = None if headers is None: headers = copy.copy(self.headers) else: new_headers = copy.copy(self.headers) new_headers.update(headers) headers = new_headers if self._url.scheme == 'http': self._connection = httplib.HTTPConnection(self._url[1]) elif self._url.scheme == 'https': self._connection = httplib.HTTPSConnection(self._url[1]) else: raise Exception, 'Unsupported scheme' self._connection.request(method, path, body, headers) self.response = self._connection.getresponse() self.response.body = self.response.read() # Try to parse and get an etree try: self._get_response_tree() except: pass def _get_response_tree(self): """Parse the response body into an elementree object""" self.response.tree = ElementTree.fromstring(self.response.body) return self.response.tree def set_basic_auth(self, username, password): """Set basic authentication""" auth = 'Basic %s' % base64.encodestring('%s:%s' % (username, password)).strip() self._username = username self._password = password self.headers['Authorization'] = auth ## HTTP DAV methods ## def get(self, path, headers=None): """Simple get request""" self._request('GET', path, headers=headers) return self.response.body def head(self, path, headers=None): """Basic HEAD request""" self._request('HEAD', path, headers=headers) def put(self, path, body=None, f=None, headers=None): """Put resource with body""" if f is not None: body = f.read() self._request('PUT', path, body=body, headers=headers) def post(self, path, body=None, headers=None): """POST resource with body""" self._request('POST', path, body=body, headers=headers) def mkcol(self, path, headers=None): """Make DAV collection""" self._request('MKCOL', path=path, headers=headers) make_collection = mkcol def delete(self, path, headers=None): """Delete DAV resource""" self._request('DELETE', path=path, headers=headers) def copy(self, source, destination, body=None, depth='infinity', overwrite=True, headers=None): """Copy DAV resource""" # Set all proper headers if headers is None: headers = {'Destination':destination} else: headers['Destination'] = self._url.geturl() + destination if overwrite is False: headers['Overwrite'] = 'F' headers['Depth'] = depth self._request('COPY', source, body=body, headers=headers) def copy_collection(self, source, destination, depth='infinity', overwrite=True, headers=None): """Copy DAV collection""" body = '*' # Add proper headers if headers is None: headers = {} headers['Content-Type'] = 'text/xml; charset="utf-8"' self.copy(source, destination, body=unicode(body, 'utf-8'), depth=depth, overwrite=overwrite, headers=headers) def move(self, source, destination, body=None, depth='infinity', overwrite=True, headers=None): """Move DAV resource""" # Set all proper headers if headers is None: headers = {'Destination':destination} else: headers['Destination'] = self._url.geturl() + destination if overwrite is False: headers['Overwrite'] = 'F' headers['Depth'] = depth self._request('MOVE', source, body=body, headers=headers) def move_collection(self, source, destination, depth='infinity', overwrite=True, headers=None): """Move DAV collection and copy all properties""" body = '*' # Add proper headers if headers is None: headers = {} headers['Content-Type'] = 'text/xml; charset="utf-8"' self.move(source, destination, unicode(body, 'utf-8'), depth=depth, overwrite=overwrite, headers=headers) def propfind(self, path, properties='allprop', namespace='DAV:', depth=None, headers=None): """Property find. If properties arg is unspecified it defaults to 'allprop'""" # Build propfind xml root = ElementTree.Element('{DAV:}propfind') if type(properties) is str: ElementTree.SubElement(root, '{DAV:}%s' % properties) else: props = ElementTree.SubElement(root, '{DAV:}prop') object_to_etree(props, properties, namespace=namespace) tree = ElementTree.ElementTree(root) # Etree won't just return a normal string, so we have to do this body = StringIO.StringIO() tree.write(body) body = body.getvalue() # Add proper headers if headers is None: headers = {} if depth is not None: headers['Depth'] = depth headers['Content-Type'] = 'text/xml; charset="utf-8"' # Body encoding must be utf-8, 207 is proper response self._request('PROPFIND', path, body=unicode('\n'+body, 'utf-8'), headers=headers) if self.response is not None and hasattr(self.response, 'tree') is True: property_responses = {} for response in self.response.tree._children: property_href = response.find('{DAV:}href') property_stat = response.find('{DAV:}propstat') def parse_props(props): property_dict = {} for prop in props: if prop.tag.find('{DAV:}') is not -1: name = prop.tag.split('}')[-1] else: name = prop.tag if len(prop._children) is not 0: property_dict[name] = parse_props(prop._children) else: property_dict[name] = prop.text return property_dict if property_href is not None and property_stat is not None: property_dict = parse_props(property_stat.find('{DAV:}prop')._children) property_responses[property_href.text] = property_dict return property_responses def proppatch(self, path, set_props=None, remove_props=None, namespace='DAV:', headers=None): """Patch properties on a DAV resource. If namespace is not specified the DAV namespace is used for all properties""" root = ElementTree.Element('{DAV:}propertyupdate') if set_props is not None: prop_set = ElementTree.SubElement(root, '{DAV:}set') object_to_etree(prop_set, set_props, namespace=namespace) if remove_props is not None: prop_remove = ElementTree.SubElement(root, '{DAV:}remove') object_to_etree(prop_remove, remove_props, namespace=namespace) tree = ElementTree.ElementTree(root) # Add proper headers if headers is None: headers = {} headers['Content-Type'] = 'text/xml; charset="utf-8"' self._request('PROPPATCH', path, body=unicode('\n'+body, 'utf-8'), headers=headers) def set_lock(self, path, owner, locktype='exclusive', lockscope='write', depth=None, headers=None): """Set a lock on a dav resource""" root = ElementTree.Element('{DAV:}lockinfo') object_to_etree(root, {'locktype':locktype, 'lockscope':lockscope, 'owner':{'href':owner}}, namespace='DAV:') tree = ElementTree.ElementTree(root) # Add proper headers if headers is None: headers = {} if depth is not None: headers['Depth'] = depth headers['Content-Type'] = 'text/xml; charset="utf-8"' headers['Timeout'] = 'Infinite, Second-4100000000' self._request('LOCK', path, body=unicode('\n'+body, 'utf-8'), headers=headers) locks = self.response.etree.finall('.//{DAV:}locktoken') lock_list = [] for lock in locks: lock_list.append(lock.getchildren()[0].text.strip().strip('\n')) return lock_list def refresh_lock(self, path, token, headers=None): """Refresh lock with token""" if headers is None: headers = {} headers['If'] = '(<%s>)' % token headers['Timeout'] = 'Infinite, Second-4100000000' self._request('LOCK', path, body=None, headers=headers) def unlock(self, path, token, headers=None): """Unlock DAV resource with token""" if headers is None: headers = {} headers['Lock-Tocken'] = '<%s>' % token self._request('UNLOCK', path, body=None, headers=headers)