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