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