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 |