1 # Copyright (c) 2006-2007 Open Source Applications Foundation
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 import urlparse
, httplib
, copy
, base64
, StringIO
19 from xml
.etree
import ElementTree
21 from elementtree
import ElementTree
23 __all__
= ['DAVClient']
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"""
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
31 if obj
.startswith('{') is False:
32 ElementTree
.SubElement(parent
, '{%s}%s' % (namespace
, obj
))
34 ElementTree
.SubElement(parent
, obj
)
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
)
43 key_etree
= ElementTree
.SubElement(parent
, key
)
44 object_to_etree(key_etree
, value
, namespace
=namespace
)
46 elif type(obj
) is list:
47 # If the object is a list parse it and send it back recursively
49 object_to_etree(parent
, item
, namespace
=namespace
)
52 # If it's none of previous types then raise
53 raise TypeError, '%s is an unsupported type' % type(obj
)
56 class DAVClient(object):
58 def __init__(self
, url
='http://localhost:8080'):
61 self
._url
= urlparse
.urlparse(url
)
63 self
.headers
= {'Host':self
._url
[1],
64 'User-Agent': 'python.davclient.DAVClient/0.1'}
67 def _request(self
, method
, path
='', body
=None, headers
=None):
68 """Internal request method"""
72 headers
= copy
.copy(self
.headers
)
74 new_headers
= copy
.copy(self
.headers
)
75 new_headers
.update(headers
)
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])
83 raise Exception, 'Unsupported scheme'
85 self
._connection
.request(method
, path
, body
, headers
)
87 self
.response
= self
._connection
.getresponse()
89 self
.response
.body
= self
.response
.read()
91 # Try to parse and get an etree
93 self
._get
_response
_tree
()
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
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
110 ## HTTP DAV methods ##
112 def get(self
, path
, headers
=None):
113 """Simple get request"""
114 self
._request
('GET', path
, headers
=headers
)
115 return self
.response
.body
117 def head(self
, path
, headers
=None):
118 """Basic HEAD request"""
119 self
._request
('HEAD', path
, headers
=headers
)
121 def put(self
, path
, body
=None, f
=None, headers
=None):
122 """Put resource with body"""
126 self
._request
('PUT', path
, body
=body
, headers
=headers
)
128 def post(self
, path
, body
=None, headers
=None):
129 """POST resource with body"""
131 self
._request
('POST', path
, body
=body
, headers
=headers
)
133 def mkcol(self
, path
, headers
=None):
134 """Make DAV collection"""
135 self
._request
('MKCOL', path
=path
, headers
=headers
)
137 make_collection
= mkcol
139 def delete(self
, path
, headers
=None):
140 """Delete DAV resource"""
141 self
._request
('DELETE', path
=path
, headers
=headers
)
143 def copy(self
, source
, destination
, body
=None, depth
='infinity', overwrite
=True, headers
=None):
144 """Copy DAV resource"""
145 # Set all proper headers
147 headers
= {'Destination':destination
}
149 headers
['Destination'] = self
._url
.geturl() + destination
150 if overwrite
is False:
151 headers
['Overwrite'] = 'F'
152 headers
['Depth'] = depth
154 self
._request
('COPY', source
, body
=body
, headers
=headers
)
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>'
164 headers
['Content-Type'] = 'text/xml; charset="utf-8"'
166 self
.copy(source
, destination
, body
=unicode(body
, 'utf-8'), depth
=depth
, overwrite
=overwrite
, headers
=headers
)
169 def move(self
, source
, destination
, body
=None, depth
='infinity', overwrite
=True, headers
=None):
170 """Move DAV resource"""
171 # Set all proper headers
173 headers
= {'Destination':destination
}
175 headers
['Destination'] = self
._url
.geturl() + destination
176 if overwrite
is False:
177 headers
['Overwrite'] = 'F'
178 headers
['Depth'] = depth
180 self
._request
('MOVE', source
, body
=body
, headers
=headers
)
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>'
190 headers
['Content-Type'] = 'text/xml; charset="utf-8"'
192 self
.move(source
, destination
, unicode(body
, 'utf-8'), depth
=depth
, overwrite
=overwrite
, headers
=headers
)
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'"""
198 root
= ElementTree
.Element('{DAV:}propfind')
199 if type(properties
) is str:
200 ElementTree
.SubElement(root
, '{DAV:}%s' % properties
)
202 props
= ElementTree
.SubElement(root
, '{DAV:}prop')
203 object_to_etree(props
, properties
, namespace
=namespace
)
204 tree
= ElementTree
.ElementTree(root
)
206 # Etree won't just return a normal string, so we have to do this
207 body
= StringIO
.StringIO()
209 body
= body
.getvalue()
214 if depth
is not None:
215 headers
['Depth'] = depth
216 headers
['Content-Type'] = 'text/xml; charset="utf-8"'
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
)
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')
227 def parse_props(props
):
230 if prop
.tag
.find('{DAV:}') is not -1:
231 name
= prop
.tag
.split('}')[-1]
234 if len(prop
._children
) is not 0:
235 property_dict
[name
] = parse_props(prop
._children
)
237 property_dict
[name
] = prop
.text
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
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')
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
)
256 tree
= ElementTree
.ElementTree(root
)
261 headers
['Content-Type'] = 'text/xml; charset="utf-8"'
263 self
._request
('PROPPATCH', path
, body
=unicode('<?xml version="1.0" encoding="utf-8" ?>\n'+body
, 'utf-8'), headers
=headers
)
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
)
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'
280 self
._request
('LOCK', path
, body
=unicode('<?xml version="1.0" encoding="utf-8" ?>\n'+body
, 'utf-8'), headers
=headers
)
282 locks
= self
.response
.etree
.finall('.//{DAV:}locktoken')
285 lock_list
.append(lock
.getchildren()[0].text
.strip().strip('\n'))
289 def refresh_lock(self
, path
, token
, headers
=None):
290 """Refresh lock with token"""
294 headers
['If'] = '(<%s>)' % token
295 headers
['Timeout'] = 'Infinite, Second-4100000000'
297 self
._request
('LOCK', path
, body
=None, headers
=headers
)
300 def unlock(self
, path
, token
, headers
=None):
301 """Unlock DAV resource with token"""
304 headers
['Lock-Tocken'] = '<%s>' % token
306 self
._request
('UNLOCK', path
, body
=None, headers
=headers
)