Launchpad automatic translations update.
[odoo/odoo.git] / addons / document_webdav / test_davclient.py
1 #!/usr/bin/env python
2 # -*- encoding: utf-8 -*-
3 #
4 # Copyright P. Christeas <p_christ@hol.gr> 2008,2009
5 # Copyright OpenERP SA. (http://www.openerp.com) 2010
6 #
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 ###############################################################################
29
30 """ A trivial HTTP/WebDAV client, used for testing the server
31 """
32 # code taken from the 'http-client.py' script:
33 # http://git.hellug.gr/?p=xrg/openerp;a=history;f=tests/http-client.py;hb=refs/heads/xrg-60
34
35 import gzip
36 import logging
37 import xml.dom.minidom
38
39 import httplib
40
41 from openerp.tools import config
42 from xmlrpclib import Transport, ProtocolError
43 import StringIO
44 import base64
45 from openerp import SUPERUSER_ID
46
47 _logger = logging.getLogger(__name__)
48
49 class HTTP11(httplib.HTTP):
50         _http_vsn = 11
51         _http_vsn_str = 'HTTP/1.1'
52
53 class PersistentTransport(Transport):
54     """Handles an HTTP transaction to an XML-RPC server, persistently."""
55
56     def __init__(self, use_datetime=0):
57         self._use_datetime = use_datetime
58         self._http = {}
59         log.debug("Using persistent transport")
60
61     def make_connection(self, host):
62         # create a HTTP connection object from a host descriptor
63         if not self._http.has_key(host):
64                 host, extra_headers, x509 = self.get_host_info(host)
65                 self._http[host] = HTTP11(host)
66                 _logger.debug("New connection to %s", host)
67         return self._http[host]
68
69     def get_host_info(self, host):
70         host, extra_headers, x509 = Transport.get_host_info(self,host)
71         if extra_headers == None:
72                 extra_headers = []
73
74         extra_headers.append( ( 'Connection', 'keep-alive' ))
75
76         return host, extra_headers, x509
77
78     def _parse_response(self, file, sock, response):
79         """ read response from input file/socket, and parse it
80             We are persistent, so it is important to only parse
81             the right amount of input
82         """
83
84         p, u = self.getparser()
85
86         if response.msg.get('content-encoding') == 'gzip':
87             gzdata = StringIO.StringIO()
88             while not response.isclosed():
89                 rdata = response.read(1024)
90                 if not rdata:
91                     break
92                 gzdata.write(rdata)
93             gzdata.seek(0)
94             rbuffer = gzip.GzipFile(mode='rb', fileobj=gzdata)
95             while True:
96                 respdata = rbuffer.read()
97                 if not respdata:
98                     break
99                 p.feed(respdata)
100         else:
101             while not response.isclosed():
102                 rdata = response.read(1024)
103                 if not rdata:
104                         break
105                 p.feed(rdata)
106                 if len(rdata)<1024:
107                         break
108
109         p.close()
110         return u.close()
111
112     def request(self, host, handler, request_body, verbose=0):
113         # issue XML-RPC request
114
115         h = self.make_connection(host)
116         if verbose:
117             h.set_debuglevel(1)
118
119         self.send_request(h, handler, request_body)
120         self.send_host(h, host)
121         self.send_user_agent(h)
122         self.send_content(h, request_body)
123
124         resp = h._conn.getresponse()
125         # TODO: except BadStatusLine, e:
126
127         errcode, errmsg, headers = resp.status, resp.reason, resp.msg
128
129
130         if errcode != 200:
131             raise ProtocolError(
132                 host + handler,
133                 errcode, errmsg,
134                 headers
135                 )
136
137         self.verbose = verbose
138
139         try:
140             sock = h._conn.sock
141         except AttributeError:
142             sock = None
143
144         return self._parse_response(h.getfile(), sock, resp)
145
146 class CompressedTransport(PersistentTransport):
147     def send_content(self, connection, request_body):
148         connection.putheader("Content-Type", "text/xml")
149
150         if len(request_body) > 512 or True:
151             buffer = StringIO.StringIO()
152             output = gzip.GzipFile(mode='wb', fileobj=buffer)
153             output.write(request_body)
154             output.close()
155             buffer.seek(0)
156             request_body = buffer.getvalue()
157             connection.putheader('Content-Encoding', 'gzip')
158
159         connection.putheader("Content-Length", str(len(request_body)))
160         connection.putheader("Accept-Encoding",'gzip')
161         connection.endheaders()
162         if request_body:
163             connection.send(request_body)
164
165     def send_request(self, connection, handler, request_body):
166         connection.putrequest("POST", handler, skip_accept_encoding=1)
167
168 class SafePersistentTransport(PersistentTransport):
169     def make_connection(self, host):
170         # create a HTTP connection object from a host descriptor
171         if not self._http.has_key(host):
172                 host, extra_headers, x509 = self.get_host_info(host)
173                 self._http[host] = httplib.HTTPS(host, None, **(x509 or {}))
174                 _logger.debug("New connection to %s", host)
175         return self._http[host]
176
177 class AuthClient(object):
178     def getAuth(self, atype, realm):
179         raise NotImplementedError("Cannot authenticate for %s" % atype)
180
181     def resolveFailedRealm(self, realm):
182         """ Called when, using a known auth type, the realm is not in cache
183         """
184         raise NotImplementedError("Cannot authenticate for realm %s" % realm)
185
186 class BasicAuthClient(AuthClient):
187     def __init__(self):
188         self._realm_dict = {}
189
190     def getAuth(self, atype, realm):
191         if atype != 'Basic' :
192             return super(BasicAuthClient,self).getAuth(atype, realm)
193
194         if not self._realm_dict.has_key(realm):
195             _logger.debug("realm dict: %r", self._realm_dict)
196             _logger.debug("missing key: \"%s\"" % realm)
197             self.resolveFailedRealm(realm)
198         return 'Basic '+ self._realm_dict[realm]
199
200     def addLogin(self, realm, username, passwd):
201         """ Add some known username/password for a specific login.
202             This function should be called once, for each realm
203             that we want to authenticate against
204         """
205         assert realm
206         auths = base64.encodestring(username + ':' + passwd)
207         if auths[-1] == "\n":
208             auths = auths[:-1]
209         self._realm_dict[realm] = auths
210
211 class addAuthTransport:
212     """ Intermediate class that authentication algorithm to http transport
213     """
214
215     def setAuthClient(self, authobj):
216         """ Set the authentication client object.
217             This method must be called before any request is issued, that
218             would require http authentication
219         """
220         assert isinstance(authobj, AuthClient)
221         self._auth_client = authobj
222
223
224     def request(self, host, handler, request_body, verbose=0):
225         # issue XML-RPC request
226
227         h = self.make_connection(host)
228         if verbose:
229             h.set_debuglevel(1)
230
231         tries = 0
232         atype = None
233         realm = None
234
235         while(tries < 3):
236             self.send_request(h, handler, request_body)
237             self.send_host(h, host)
238             self.send_user_agent(h)
239             if atype:
240                 # This line will bork if self.setAuthClient has not
241                 # been issued. That is a programming error, fix your code!
242                 auths = self._auth_client.getAuth(atype, realm)
243                 _logger.debug("sending authorization: %s", auths)
244                 h.putheader('Authorization', auths)
245             self.send_content(h, request_body)
246
247             resp = h._conn.getresponse()
248             #  except BadStatusLine, e:
249             tries += 1
250
251             if resp.status == 401:
252                 if 'www-authenticate' in resp.msg:
253                     (atype,realm) = resp.msg.getheader('www-authenticate').split(' ',1)
254                     data1 = resp.read()
255                     if data1:
256                         log.warning("Why have data on a 401 auth. message?")
257                     if realm.startswith('realm="') and realm.endswith('"'):
258                         realm = realm[7:-1]
259                     _logger.debug("Resp: %r %r", resp.version,resp.isclosed(), resp.will_close)
260                     _logger.debug("Want to do auth %s for realm %s", atype, realm)
261                     if atype != 'Basic':
262                         raise ProtocolError(host+handler, 403,
263                                         "Unknown authentication method: %s" % atype, resp.msg)
264                     continue # with the outer while loop
265                 else:
266                     raise ProtocolError(host+handler, 403,
267                                 'Server-incomplete authentication', resp.msg)
268
269             if resp.status != 200:
270                 raise ProtocolError( host + handler,
271                     resp.status, resp.reason, resp.msg )
272
273             self.verbose = verbose
274
275             try:
276                 sock = h._conn.sock
277             except AttributeError:
278                 sock = None
279
280             return self._parse_response(h.getfile(), sock, resp)
281
282         raise ProtocolError(host+handler, 403, "No authentication.",'')
283
284 class PersistentAuthTransport(addAuthTransport,PersistentTransport):
285     pass
286
287 class PersistentAuthCTransport(addAuthTransport,CompressedTransport):
288     pass
289
290 class HTTPSConnection(httplib.HTTPSConnection):
291         certs_file = None
292         def connect(self):
293             "Connect to a host on a given (SSL) port. check the certificate"
294             import socket, ssl
295
296             if HTTPSConnection.certs_file:
297                 ca_certs = HTTPSConnection.certs_file
298                 cert_reqs = ssl.CERT_REQUIRED
299             else:
300                 ca_certs = None
301                 cert_reqs = ssl.CERT_NONE
302             sock = socket.create_connection((self.host, self.port), self.timeout)
303             self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
304                                 ca_certs=ca_certs,
305                                 cert_reqs=cert_reqs)
306
307
308         def getpeercert(self):
309                 import ssl
310                 cert = None
311                 if self.sock:
312                         cert =  self.sock.getpeercert()
313                 else:
314                         cert = ssl.get_server_certificate((self.host,self.port),
315                                 ssl_version=ssl.PROTOCOL_SSLv23 )
316                         lf = (len(ssl.PEM_FOOTER)+1)
317                         if cert[0-lf] != '\n':
318                                 cert = cert[:0-lf]+'\n'+cert[0-lf:]
319                         _logger.debug("len-footer: %s cert: %r", lf, cert[0-lf])
320
321                 return cert
322
323
324 class DAVClient(object):
325     """An instance of a WebDAV client, connected to the OpenERP server
326     """
327
328     def __init__(self, user=None, passwd=None, dbg=0, use_ssl=False, useragent=False, timeout=None):
329         if use_ssl:
330             self.host = config.get_misc('httpsd', 'interface', False)
331             self.port = config.get_misc('httpsd', 'port', 8071)
332             if not self.host:
333                 self.host = config.get('xmlrpcs_interface')
334                 self.port = config.get('xmlrpcs_port')
335         else:
336             self.host = config.get_misc('httpd', 'interface')
337             self.port = config.get_misc('httpd', 'port', 8069)
338             if not self.host:
339                 self.host = config.get('xmlrpc_interface')
340                 self.port = config.get('xmlrpc_port') or self.port
341         if self.host == '0.0.0.0' or not self.host:
342             self.host = '127.0.0.1'
343         self.port = int(self.port)
344         if not config.get_misc('webdav','enable',True):
345             raise Exception("WebDAV is disabled, cannot continue")
346         self.davpath = '/' + config.get_misc('webdav','vdir','webdav')
347         self.user = user
348         self.passwd = passwd
349         self.dbg = dbg
350         self.timeout = timeout or 5.0 # seconds, tests need to respond pretty fast!
351         self.hdrs = {}
352         if useragent:
353             self.set_useragent(useragent)
354
355     def get_creds(self, obj, cr, uid):
356         """Read back the user credentials from cr, uid
357
358         @param obj is any orm object, in order to use its pool
359         @param uid is the numeric id, which we will try to reverse resolve
360
361         note: this is a hackish way to get the credentials. It is expected
362         to break if "base_crypt" is used.
363         """
364         ruob = obj.pool.get('res.users')
365         res = ruob.read(cr, SUPERUSER_ID, [uid,], ['login', 'password'])
366         assert res, "uid %s not found" % uid
367         self.user = res[0]['login']
368         self.passwd = res[0]['password']
369         if self.passwd.startswith('$1$'):
370             # md5 by base crypt. We cannot decode, wild guess
371             # that passwd = login
372             self.passwd = self.user
373         return True
374
375     def set_useragent(self, uastr):
376         """ Set the user-agent header to something meaningful.
377         Some shorthand names will be replaced by stock strings.
378         """
379         if uastr in ('KDE4', 'Korganizer'):
380             self.hdrs['User-Agent'] = "Mozilla/5.0 (compatible; Konqueror/4.4; Linux) KHTML/4.4.3 (like Gecko)"
381         elif uastr == 'iPhone3':
382             self.hdrs['User-Agent'] = "DAVKit/5.0 (765); iCalendar/5.0 (79); iPhone/4.1 8B117"
383         elif uastr == "MacOS":
384             self.hdrs['User-Agent'] = "WebDAVFS/1.8 (01808000) Darwin/9.8.0 (i386)"
385         else:
386             self.hdrs['User-Agent'] = uastr
387
388     def _http_request(self, path, method='GET', hdrs=None, body=None):
389         if not hdrs:
390             hdrs = {}
391         import base64
392         dbg = self.dbg
393         hdrs.update(self.hdrs)
394         _logger.debug("Getting %s http://%s:%d/%s", method, self.host, self.port, path)
395         conn = httplib.HTTPConnection(self.host, port=self.port, timeout=self.timeout)
396         conn.set_debuglevel(dbg)
397         if not path:
398             path = "/index.html"
399         if not hdrs.has_key('Connection'):
400                 hdrs['Connection']= 'keep-alive'
401         conn.request(method, path, body, hdrs )
402         try:
403                 r1 = conn.getresponse()
404         except httplib.BadStatusLine, bsl:
405                 log.warning("Bad status line: %s", bsl.line)
406                 raise Exception('Bad status line.')
407         if r1.status == 401: # and r1.headers:
408                 if 'www-authenticate' in r1.msg:
409                         (atype,realm) = r1.msg.getheader('www-authenticate').split(' ',1)
410                         data1 = r1.read()
411                         if not self.user:
412                                 raise Exception('Must auth, have no user/pass!')
413                         _logger.debug("Ver: %s, closed: %s, will close: %s", r1.version,r1.isclosed(), r1.will_close)
414                         _logger.debug("Want to do auth %s for realm %s", atype, realm)
415                         if atype == 'Basic' :
416                                 auths = base64.encodestring(self.user + ':' + self.passwd)
417                                 if auths[-1] == "\n":
418                                         auths = auths[:-1]
419                                 hdrs['Authorization']= 'Basic '+ auths
420                                 #sleep(1)
421                                 conn.request(method, path, body, hdrs )
422                                 r1 = conn.getresponse()
423                         else:
424                                 raise Exception("Unknown auth type %s" %atype)
425                 else:
426                         _logger.warning("Got 401, cannot auth")
427                         raise Exception('No auth')
428
429         _logger.debug("Reponse: %s %s",r1.status, r1.reason)
430         data1 = r1.read()
431         if method != 'GET':
432             _logger.debug("Body:\n%s\nEnd of body", data1)
433             try:
434                 ctype = r1.msg.getheader('content-type')
435                 if ctype and ';' in ctype:
436                     ctype, encoding = ctype.split(';',1)
437                 if ctype == 'text/xml':
438                     doc = xml.dom.minidom.parseString(data1)
439                     _logger.debug("XML Body:\n %s", doc.toprettyxml(indent="\t"))
440             except Exception:
441                 _logger.warning("Cannot print XML.", exc_info=True)
442                 pass
443         conn.close()
444         return r1.status, r1.msg, data1
445
446     def _assert_headers(self, expect, msg):
447         """ Assert that the headers in msg contain the expect values
448         """
449         for k, v in expect.items():
450             hval = msg.getheader(k)
451             if not hval:
452                 raise AssertionError("Header %s not defined in http response" % k)
453             if isinstance(v, (list, tuple)):
454                 delim = ','
455                 hits = map(str.strip, hval.split(delim))
456                 mvits= []
457                 for vit in v:
458                     if vit not in hits:
459                         mvits.append(vit)
460                 if mvits:
461                     raise AssertionError("HTTP header \"%s\" is missing: %s" %(k, ', '.join(mvits)))
462             else:
463                 if hval.strip() != v.strip():
464                     raise AssertionError("HTTP header \"%s: %s\"" % (k, hval))
465
466     def gd_options(self, path='*', expect=None):
467         """ Test the http options functionality
468             If a dictionary is defined in expect, those options are
469             asserted.
470         """
471         if path != '*':
472             path = self.davpath + path
473         hdrs = { 'Content-Length': 0
474                 }
475         s, m, d = self._http_request(path, method='OPTIONS', hdrs=hdrs)
476         assert s == 200, "Status: %r" % s
477         assert 'OPTIONS' in m.getheader('Allow')
478         _logger.debug('Options: %r', m.getheader('Allow'))
479
480         if expect:
481             self._assert_headers(expect, m)
482
483     def _parse_prop_response(self, data):
484         """ Parse a propfind/propname response
485         """
486         def getText(node):
487             rc = []
488             for node in node.childNodes:
489                 if node.nodeType == node.TEXT_NODE:
490                     rc.append(node.data)
491             return ''.join(rc)
492
493         def getElements(node, namespaces=None, strict=False):
494             for cnod in node.childNodes:
495                 if cnod.nodeType != node.ELEMENT_NODE:
496                     if strict:
497                         _logger.debug("Found %r inside <%s>", cnod, node.tagName)
498                     continue
499                 if namespaces and (cnod.namespaceURI not in namespaces):
500                     _logger.debug("Ignoring <%s> in <%s>", cnod.tagName, node.localName)
501                     continue
502                 yield cnod
503
504         nod = xml.dom.minidom.parseString(data)
505         nod_r = nod.documentElement
506         res = {}
507         assert nod_r.localName == 'multistatus', nod_r.tagName
508         for resp in nod_r.getElementsByTagNameNS('DAV:', 'response'):
509             href = None
510             status = 200
511             res_nss = {}
512             for cno in getElements(resp, namespaces=['DAV:',]):
513                 if cno.localName == 'href':
514                     assert href is None, "Second href in same response"
515                     href = getText(cno)
516                 elif cno.localName == 'propstat':
517                     for pno in getElements(cno, namespaces=['DAV:',]):
518                         rstatus = None
519                         if pno.localName == 'prop':
520                             for prop in getElements(pno):
521                                 key = prop.localName
522                                 tval = getText(prop).strip()
523                                 val = tval or (True, rstatus or status)
524                                 if prop.namespaceURI == 'DAV:' and prop.localName == 'resourcetype':
525                                     val = 'plain'
526                                     for rte in getElements(prop, namespaces=['DAV:',]):
527                                         # Note: we only look at DAV:... elements, we
528                                         # actually expect only one DAV:collection child
529                                         val = rte.localName
530                                 res_nss.setdefault(prop.namespaceURI,{})[key] = val
531                         elif pno.localName == 'status':
532                             rstr = getText(pno)
533                             htver, sta, msg = rstr.split(' ', 3)
534                             assert htver == 'HTTP/1.1'
535                             rstatus = int(sta)
536                         else:
537                             _logger.debug("What is <%s> inside a <propstat>?", pno.tagName)
538
539                 else:
540                     _logger.debug("Unknown node: %s", cno.tagName)
541
542             res.setdefault(href,[]).append((status, res_nss))
543
544         return res
545
546     def gd_propfind(self, path, props=None, depth=0):
547         if not props:
548             propstr = '<allprop/>'
549         else:
550             propstr = '<prop>'
551             nscount = 0
552             for p in props:
553                 ns = None
554                 if isinstance(p, tuple):
555                     p, ns = p
556                 if ns is None or ns == 'DAV:':
557                     propstr += '<%s/>' % p
558                 else:
559                     propstr += '<ns%d:%s xmlns:ns%d="%s" />' %(nscount, p, nscount, ns)
560                     nscount += 1
561             propstr += '</prop>'
562
563         body="""<?xml version="1.0" encoding="utf-8"?>
564             <propfind xmlns="DAV:">%s</propfind>""" % propstr
565         hdrs = { 'Content-Type': 'text/xml; charset=utf-8',
566                 'Accept': 'text/xml',
567                 'Depth': depth,
568                 }
569
570         s, m, d = self._http_request(self.davpath + path, method='PROPFIND',
571                                     hdrs=hdrs, body=body)
572         assert s == 207, "Bad status: %s" % s
573         ctype = m.getheader('Content-Type').split(';',1)[0]
574         assert ctype == 'text/xml', m.getheader('Content-Type')
575         res = self._parse_prop_response(d)
576         if depth == 0:
577             assert len(res) == 1
578             res = res.values()[0]
579         else:
580             assert len(res) >= 1
581         return res
582
583
584     def gd_propname(self, path, depth=0):
585         body="""<?xml version="1.0" encoding="utf-8"?>
586             <propfind xmlns="DAV:"><propname/></propfind>"""
587         hdrs = { 'Content-Type': 'text/xml; charset=utf-8',
588                 'Accept': 'text/xml',
589                 'Depth': depth
590                 }
591         s, m, d = self._http_request(self.davpath + path, method='PROPFIND',
592                                     hdrs=hdrs, body=body)
593         assert s == 207, "Bad status: %s" % s
594         ctype = m.getheader('Content-Type').split(';',1)[0]
595         assert ctype == 'text/xml', m.getheader('Content-Type')
596         res = self._parse_prop_response(d)
597         if depth == 0:
598             assert len(res) == 1
599             res = res.values()[0]
600         else:
601             assert len(res) >= 1
602         return res
603
604     def gd_getetag(self, path, depth=0):
605         return self.gd_propfind(path, props=['getetag',], depth=depth)
606
607     def gd_lsl(self, path):
608         """ Return a list of 'ls -l' kind of data for a folder
609
610             This is based on propfind.
611         """
612
613         lspairs = [ ('name', 'displayname', 'n/a'), ('size', 'getcontentlength', '0'),
614                 ('type', 'resourcetype', '----------'), ('uid', 'owner', 'nobody'),
615                 ('gid', 'group', 'nogroup'), ('mtime', 'getlastmodified', 'n/a'),
616                 ('mime', 'getcontenttype', 'application/data'), ]
617
618         propnames = [ l[1] for l in lspairs]
619         propres = self.gd_propfind(path, props=propnames, depth=1)
620
621         res = []
622         for href, pr in propres.items():
623             lsline = {}
624             for st, nsdic in pr:
625                 davprops = nsdic['DAV:']
626                 if st == 200:
627                     for lsp in lspairs:
628                         if lsp[1] in davprops:
629                             if lsp[1] == 'resourcetype':
630                                 if davprops[lsp[1]] == 'collection':
631                                     lsline[lsp[0]] = 'dr-xr-x---'
632                                 else:
633                                     lsline[lsp[0]] = '-r-xr-x---'
634                             else:
635                                 lsline[lsp[0]] = davprops[lsp[1]]
636                 elif st in (404, 403):
637                     for lsp in lspairs:
638                         if lsp[1] in davprops:
639                             lsline[lsp[0]] = lsp[2]
640                 else:
641                     _logger.debug("Strange status: %s", st)
642
643             res.append(lsline)
644
645         return res
646
647     def gd_get(self, path, crange=None, mime=None, compare=None):
648         """ HTTP GET for path, supporting Partial ranges
649         """
650         hdrs = { 'Accept': mime or '*/*', }
651         if crange:
652             if isinstance(crange, tuple):
653                 crange = [crange,]
654             if not isinstance(crange, list):
655                 raise TypeError("Range must be a tuple or list of tuples.")
656             rs = []
657             for r in crange:
658                 rs.append('%d-%d' % r)
659             hdrs['Range'] = 'bytes='+ (','.join(rs))
660         s, m, d = self._http_request(self.davpath + path, method='GET', hdrs=hdrs)
661         assert s in (200, 206), "Bad status: %s" % s
662         ctype = m.getheader('Content-Type')
663         if ctype and ';' in ctype:
664             ctype = ctype.split(';',1)[0]
665         if mime:
666             assert ctype == mime, m.getheader('Content-Type')
667         rrange = None
668         rrh = m.getheader('Content-Range')
669         if rrh:
670             assert rrh.startswith('bytes '), rrh
671             rrh=rrh[6:].split('/',1)[0]
672             rrange = map(int, rrh.split('-',1))
673         if compare:
674             # we need to compare the returned data with that of compare
675             fd = open(compare, 'rb')
676             d2 = fd.read()
677             fd.close()
678             if crange:
679                 if len(crange) > 1:
680                     raise NotImplementedError
681                 r = crange[0]
682                 d2 = d2[r[0]:r[1]+1]
683             assert d2 == d, "Data does not match"
684         return ctype, rrange, d
685
686     def gd_put(self, path, body=None, srcpath=None, mime=None, noclobber=False):
687         """ HTTP PUT
688             @param noclobber will prevent overwritting a resource (If-None-Match)
689             @param mime will set the content-type
690         """
691         hdrs = { }
692         if not (body or srcpath):
693             raise ValueError("PUT must have something to send.")
694         if (not body) and srcpath:
695             fd = open(srcpath, 'rb')
696             body = fd.read()
697             fd.close()
698         if mime:
699             hdrs['Content-Type'] = mime
700         if noclobber:
701             hdrs['If-None-Match'] = '*'
702         s, m, d = self._http_request(self.davpath + path, method='PUT',
703                             hdrs=hdrs, body=body)
704         assert s == (201), "Bad status: %s" % s
705         etag = m.getheader('ETag')
706         return etag or True
707
708 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: