doc webdav: support for GET and PUT at yaml tests
[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 re
36 import gzip
37 import logging
38 import xml.dom.minidom
39
40 import httplib
41
42 from tools import config
43 from xmlrpclib import Transport, ProtocolError
44 import StringIO
45 import base64
46
47 log = logging.getLogger('http-client')
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                 log.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                 log.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             log.debug("realm dict: %r", self._realm_dict)
196             log.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                 log.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                     log.debug("Resp: %r %r", resp.version,resp.isclosed(), resp.will_close)
260                     log.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                         log.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):
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.hdrs = {}
351         if useragent:
352             self.set_useragent(useragent)
353
354     def get_creds(self, obj, cr, uid):
355         """Read back the user credentials from cr, uid
356         
357         @param obj is any orm object, in order to use its pool
358         @param uid is the numeric id, which we will try to reverse resolve
359         
360         note: this is a hackish way to get the credentials. It is expected
361         to break if "base_crypt" is used.
362         """
363         ruob = obj.pool.get('res.users')
364         res = ruob.read(cr, 1, [uid,], ['login', 'password'])
365         assert res, "uid %s not found" % uid
366         self.user = res[0]['login']
367         self.passwd = res[0]['password']
368         return True
369
370     def set_useragent(self, uastr):
371         """ Set the user-agent header to something meaningful.
372         Some shorthand names will be replaced by stock strings.
373         """
374         if uastr in ('KDE4', 'Korganizer'):
375             self.hdrs['User-Agent'] = "Mozilla/5.0 (compatible; Konqueror/4.4; Linux) KHTML/4.4.3 (like Gecko)"
376         elif uastr == 'iPhone3':
377             self.hdrs['User-Agent'] = "DAVKit/5.0 (765); iCalendar/5.0 (79); iPhone/4.1 8B117"
378         elif uastr == "MacOS":
379             self.hdrs['User-Agent'] = "WebDAVFS/1.8 (01808000) Darwin/9.8.0 (i386)"
380         else:
381             self.hdrs['User-Agent'] = uastr
382
383     def _http_request(self, path, method='GET', hdrs=None, body=None):
384         if not hdrs:
385             hdrs = {}
386         import base64
387         dbg = self.dbg
388         hdrs.update(self.hdrs)
389         log.debug("Getting %s http://%s:%d/%s", method, self.host, self.port, path)
390         conn = httplib.HTTPConnection(self.host, port=self.port)
391         conn.set_debuglevel(dbg)
392         if not path:
393             path = "/index.html"
394         if not hdrs.has_key('Connection'):
395                 hdrs['Connection']= 'keep-alive'
396         conn.request(method, path, body, hdrs )
397         try:
398                 r1 = conn.getresponse()
399         except httplib.BadStatusLine, bsl:
400                 log.warning("Bad status line: %s", bsl.line)
401                 raise Exception('Bad status line')
402         if r1.status == 401: # and r1.headers:
403                 if 'www-authenticate' in r1.msg:
404                         (atype,realm) = r1.msg.getheader('www-authenticate').split(' ',1)
405                         data1 = r1.read()
406                         if not self.user:
407                                 raise Exception('Must auth, have no user/pass!')
408                         log.debug("Ver: %s, closed: %s, will close: %s", r1.version,r1.isclosed(), r1.will_close)
409                         log.debug("Want to do auth %s for realm %s", atype, realm)
410                         if atype == 'Basic' :
411                                 auths = base64.encodestring(self.user + ':' + self.passwd)
412                                 if auths[-1] == "\n":
413                                         auths = auths[:-1]
414                                 hdrs['Authorization']= 'Basic '+ auths 
415                                 #sleep(1)
416                                 conn.request(method, path, body, hdrs )
417                                 r1 = conn.getresponse()
418                         else:
419                                 raise Exception("Unknown auth type %s" %atype)
420                 else:
421                         log.warning("Got 401, cannot auth")
422                         raise Exception('No auth')
423
424         log.debug("Reponse: %s %s",r1.status, r1.reason)
425         data1 = r1.read()
426         if method != 'GET':
427             log.debug("Body:\n%s\nEnd of body", data1)
428             try:
429                 ctype = r1.msg.getheader('content-type')
430                 if ctype and ';' in ctype:
431                     ctype, encoding = ctype.split(';',1)
432                 if ctype == 'text/xml':
433                     doc = xml.dom.minidom.parseString(data1)
434                     log.debug("XML Body:\n %s", doc.toprettyxml(indent="\t"))
435             except Exception:
436                 log.warning("could not print xml", exc_info=True)
437                 pass
438         conn.close()
439         return r1.status, r1.msg, data1
440
441     def _assert_headers(self, expect, msg):
442         """ Assert that the headers in msg contain the expect values
443         """
444         for k, v in expect.items():
445             hval = msg.getheader(k)
446             if not hval:
447                 raise AssertionError("Header %s not defined in http response" % k)
448             if isinstance(v, (list, tuple)):
449                 delim = ','
450                 hits = map(str.strip, hval.split(delim))
451                 mvits= []
452                 for vit in v:
453                     if vit not in hits:
454                         mvits.append(vit)
455                 if mvits:
456                     raise AssertionError("HTTP header \"%s\" is missing: %s" %(k, ', '.join(mvits)))
457             else:
458                 if hval.strip() != v.strip():
459                     raise AssertionError("HTTP header \"%s: %s\"" % (k, hval))
460
461     def gd_options(self, path='*', expect=None):
462         """ Test the http options functionality
463             If a dictionary is defined in expect, those options are
464             asserted.
465         """
466         if path != '*':
467             path = self.davpath + path
468         hdrs = { 'Content-Length': 0
469                 }
470         s, m, d = self._http_request(path, method='OPTIONS', hdrs=hdrs)
471         assert s == 200, "Status: %r" % s
472         assert 'OPTIONS' in m.getheader('Allow')
473         log.debug('Options: %r', m.getheader('Allow'))
474         
475         if expect:
476             self._assert_headers(expect, m)
477     
478     def _parse_prop_response(self, data):
479         """ Parse a propfind/propname response
480         """
481         def getText(node):
482             rc = []
483             for node in node.childNodes:
484                 if node.nodeType == node.TEXT_NODE:
485                     rc.append(node.data)
486             return ''.join(rc)
487         
488         def getElements(node, namespaces=None, strict=False):
489             for cnod in node.childNodes:
490                 if cnod.nodeType != node.ELEMENT_NODE:
491                     if strict:
492                         log.debug("Found %r inside <%s>", cnod, node.tagName)
493                     continue
494                 if namespaces and (cnod.namespaceURI not in namespaces):
495                     log.debug("Ignoring <%s> in <%s>", cnod.tagName, node.localName)
496                     continue
497                 yield cnod
498
499         nod = xml.dom.minidom.parseString(data)
500         nod_r = nod.documentElement
501         res = {}
502         assert nod_r.localName == 'multistatus', nod_r.tagName
503         for resp in nod_r.getElementsByTagNameNS('DAV:', 'response'):
504             href = None
505             status = 200
506             res_nss = {}
507             for cno in getElements(resp, namespaces=['DAV:',]):
508                 if cno.localName == 'href':
509                     assert href is None, "Second href in same response"
510                     href = getText(cno)
511                 elif cno.localName == 'propstat':
512                     for pno in getElements(cno, namespaces=['DAV:',]):
513                         rstatus = None
514                         if pno.localName == 'prop':
515                             for prop in getElements(pno):
516                                 key = prop.localName
517                                 tval = getText(prop).strip()
518                                 val = tval or (True, rstatus or status)
519                                 if prop.namespaceURI == 'DAV:' and prop.localName == 'resourcetype':
520                                     val = 'plain'
521                                     for rte in getElements(prop, namespaces=['DAV:',]):
522                                         # Note: we only look at DAV:... elements, we
523                                         # actually expect only one DAV:collection child
524                                         val = rte.localName
525                                 res_nss.setdefault(prop.namespaceURI,{})[key] = val
526                         elif pno.localName == 'status':
527                             rstr = getText(pno)
528                             htver, sta, msg = rstr.split(' ', 3)
529                             assert htver == 'HTTP/1.1'
530                             rstatus = int(sta)
531                         else:
532                             log.debug("What is <%s> inside a <propstat>?", pno.tagName)
533                     
534                 else:
535                     log.debug("Unknown node: %s", cno.tagName)
536                 
537             res.setdefault(href,[]).append((status, res_nss))
538
539         return res
540
541     def gd_propfind(self, path, props=None, depth=0):
542         if not props:
543             propstr = '<allprop/>'
544         else:
545             propstr = '<prop>'
546             nscount = 0
547             for p in props:
548                 ns = None
549                 if isinstance(p, tuple):
550                     p, ns = p
551                 if ns is None or ns == 'DAV:':
552                     propstr += '<%s/>' % p
553                 else:
554                     propstr += '<ns%d:%s xmlns:ns%d="%s" />' %(nscount, p, nscount, ns)
555                     nscount += 1
556             propstr += '</prop>'
557                 
558         body="""<?xml version="1.0" encoding="utf-8"?>
559             <propfind xmlns="DAV:">%s</propfind>""" % propstr
560         hdrs = { 'Content-Type': 'text/xml; charset=utf-8',
561                 'Accept': 'text/xml',
562                 'Depth': depth,
563                 }
564
565         s, m, d = self._http_request(self.davpath + path, method='PROPFIND', 
566                                     hdrs=hdrs, body=body)
567         assert s == 207, "Bad status: %s" % s
568         ctype = m.getheader('Content-Type').split(';',1)[0]
569         assert ctype == 'text/xml', m.getheader('Content-Type')
570         res = self._parse_prop_response(d)
571         if depth == 0:
572             assert len(res) == 1
573             res = res.values()[0]
574         else:
575             assert len(res) >= 1
576         return res
577         
578
579     def gd_propname(self, path, depth=0):
580         body="""<?xml version="1.0" encoding="utf-8"?>
581             <propfind xmlns="DAV:"><propname/></propfind>"""
582         hdrs = { 'Content-Type': 'text/xml; charset=utf-8',
583                 'Accept': 'text/xml',
584                 'Depth': depth
585                 }
586         s, m, d = self._http_request(self.davpath + path, method='PROPFIND', 
587                                     hdrs=hdrs, body=body)
588         assert s == 207, "Bad status: %s" % s
589         ctype = m.getheader('Content-Type').split(';',1)[0]
590         assert ctype == 'text/xml', m.getheader('Content-Type')
591         res = self._parse_prop_response(d)
592         if depth == 0:
593             assert len(res) == 1
594             res = res.values()[0]
595         else:
596             assert len(res) >= 1
597         return res
598
599     def gd_getetag(self, path, depth=0):
600         return self.gd_propfind(path, props=['getetag',], depth=depth)
601
602     def gd_lsl(self, path):
603         """ Return a list of 'ls -l' kind of data for a folder
604         
605             This is based on propfind.
606         """
607
608         lspairs = [ ('name', 'displayname', 'n/a'), ('size', 'getcontentlength', '0'),
609                 ('type', 'resourcetype', '----------'), ('uid', 'owner', 'nobody'),
610                 ('gid', 'group', 'nogroup'), ('mtime', 'getlastmodified', 'n/a'),
611                 ('mime', 'getcontenttype', 'application/data'), ]
612
613         propnames = [ l[1] for l in lspairs]
614         propres = self.gd_propfind(path, props=propnames, depth=1)
615         
616         res = []
617         for href, pr in propres.items():
618             lsline = {}
619             for st, nsdic in pr:
620                 davprops = nsdic['DAV:']
621                 if st == 200:
622                     for lsp in lspairs:
623                         if lsp[1] in davprops:
624                             if lsp[1] == 'resourcetype':
625                                 if davprops[lsp[1]] == 'collection':
626                                     lsline[lsp[0]] = 'dr-xr-x---'
627                                 else:
628                                     lsline[lsp[0]] = '-r-xr-x---'
629                             else:
630                                 lsline[lsp[0]] = davprops[lsp[1]]
631                 elif st in (404, 403):
632                     for lsp in lspairs:
633                         if lsp[1] in davprops:
634                             lsline[lsp[0]] = lsp[2]
635                 else:
636                     log.debug("Strange status: %s", st)
637             
638             res.append(lsline)
639             
640         return res
641
642     def gd_get(self, path, crange=None, mime=None, compare=None):
643         """ HTTP GET for path, supporting Partial ranges
644         """
645         hdrs = { 'Accept': mime or '*/*', }
646         if crange:
647             if isinstance(crange, tuple):
648                 crange = [crange,]
649             if not isinstance(crange, list):
650                 raise TypeError("Range must be a tuple or list of tuples")
651             rs = []
652             for r in crange:
653                 rs.append('%d-%d' % r)
654             hdrs['Range'] = 'bytes='+ (','.join(rs))
655         s, m, d = self._http_request(self.davpath + path, method='GET', hdrs=hdrs)
656         assert s in (200, 206), "Bad status: %s" % s
657         ctype = m.getheader('Content-Type').split(';',1)[0]
658         if mime:
659             assert ctype == mime, m.getheader('Content-Type')
660         rrange = None
661         rrh = m.getheader('Content-Range')
662         if rrh:
663             assert rrh.startswith('bytes '), rrh
664             rrh=rrh[6:].split('/',1)[0]
665             rrange = map(int, rrh.split('-',1))
666         if compare:
667             # we need to compare the returned data with that of compare
668             fd = open(compare, 'rb')
669             d2 = fd.read()
670             fd.close()
671             assert d2 == d, "Data does not match"
672         return ctype, rrange, d
673
674     def gd_put(self, path, body=None, srcpath=None, mime=None, noclobber=False, ):
675         """ HTTP PUT 
676             @param noclobber will prevent overwritting a resource (If-None-Match)
677             @param mime will set the content-type
678         """
679         hdrs = { }
680         if not (body or srcpath):
681             raise ValueError("PUT must have something to send")
682         if (not body) and srcpath:
683             fd = open(srcpath, 'rb')
684             body = fd.read()
685             fd.close()
686         if mime:
687             hdrs['Content-Type'] = mime
688         if noclobber:
689             hdrs['If-None-Match'] = '*'
690         s, m, d = self._http_request(self.davpath + path, method='PUT', 
691                             hdrs=hdrs, body=body)
692         assert s == (201), "Bad status: %s" % s
693         etag = m.getheader('ETag')
694         return etag or True
695
696 #eof