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 |