1
"""HTTP 1.1 / WebDAV client library."""
3
__version__='$Revision: 1.4 $'[11:-2]
5
import sys, os, string, time, types,re
6
import socket, httplib, mimetools
7
from types import FileType
8
from mimetypes import guess_type
9
from base64 import encodestring
10
from common import rfc1123_date
11
from cStringIO import StringIO
12
from random import random
13
from urllib import quote
18
class HTTP(httplib.HTTP):
19
# A revised version of the HTTP class that can do basic
20
# HTTP 1.1 connections, and also compensates for a bug
21
# that occurs on some platforms in 1.5 and 1.5.1 with
22
# socket.makefile().read()
24
read_bug=sys.version[:5] in ('1.5 (','1.5.1')
26
def putrequest(self, request, selector, ver='1.1'):
27
selector=selector or '/'
28
str = '%s %s HTTP/%s\r\n' % (request, selector, ver)
32
file=self.sock.makefile('rb')
33
data=string.join(file.readlines(), '')
35
self.file=StringIO(data)
36
line = self.file.readline()
38
[ver, code, msg] = string.split(line, None, 2)
41
[ver, code] = string.split(line, None, 1)
45
if ver[:5] != 'HTTP/':
47
code=string.atoi(code)
48
msg =string.strip(msg)
49
headers =mimetools.Message(self.file, 0)
50
return ver, code, msg, headers
54
"""An object representing a web resource."""
56
def __init__(self, url, username=None, password=None):
57
self.username=username
58
self.password=password
61
mo = urlreg.match(url)
63
host,port,uri=mo.group(1,2,3)
65
self.port=port and string.atoi(port[1:]) or 80
67
else: raise ValueError, url
69
def __getattr__(self, name):
70
url=os.path.join(self.url, name)
71
return self.__class__(url, username=self.username,
72
password=self.password)
74
def __get_headers(self, kw={}):
76
headers=self.__set_authtoken(headers)
77
headers['User-Agent']='WebDAV.client %s' % __version__
78
headers['Host']=self.host
79
headers['Connection']='close'
80
headers['Accept']='*/*'
81
if kw.has_key('headers'):
82
for name, val in kw['headers'].items():
87
def __set_authtoken(self, headers, atype='Basic'):
88
if not (self.username and self.password):
90
if headers.has_key('Authorization'):
93
headers['Authorization']=(
94
"Basic %s" % string.replace(encodestring('%s:%s' % (self.username,self.password)),
97
raise ValueError, 'Unknown authentication scheme: %s' % atype
99
def __enc_formdata(self, args={}):
101
for key, val in args.items():
102
n=string.rfind(key, '__')
107
func=varfuncs.get(tag, marshal_string)
108
formdata.append(func(key, val))
109
return string.join(formdata, '&')
111
def __enc_multipart(self, args={}):
112
return MultiPart(args).render()
114
def __snd_request(self, method, uri, headers={}, body='', eh=1):
117
h.connect(self.host, self.port)
118
h.putrequest(method, uri)
119
for n, v in headers.items():
121
if eh: h.endheaders()
122
if body: h.send(body)
123
ver, code, msg, hdrs=h.getreply()
124
data=h.getfile().read()
127
raise 'NotAvailable', sys.exc_value
128
return http_response(ver, code, msg, hdrs, data)
133
headers=self.__get_headers(kw)
134
query=self.__enc_formdata(kw)
135
uri=query and '%s?%s' % (self.uri, query) or self.uri
136
return self.__snd_request('GET', uri, headers)
138
def head(self, **kw):
139
headers=self.__get_headers(kw)
140
query=self.__enc_formdata(kw)
141
uri=query and '%s?%s' % (self.uri, query) or self.uri
142
return self.__snd_request('HEAD', uri, headers)
144
def post(self, **kw):
145
headers=self.__get_headers(kw)
147
for key, val in kw.items():
148
if (key[-6:]=='__file') or hasattr(val, 'read'):
149
content_type='multipart/form-data'
151
if content_type=='multipart/form-data':
152
body=self.__enc_multipart(kw)
153
return self.__snd_request('POST', self.uri, headers, body, eh=0)
155
body=self.__enc_formdata(kw)
156
headers['Content-Type']='application/x-www-form-urlencoded'
157
headers['Content-Length']=str(len(body))
158
return self.__snd_request('POST', self.uri, headers, body)
160
def put(self, file='', content_type='', content_enc='',
161
isbin=re.compile(r'[\000-\006\177-\277]').search,
163
headers=self.__get_headers(kw)
165
if filetype is type('') and (isbin(file) is None) and \
166
os.path.exists(file):
170
c_type, c_enc=guess_type(file)
171
elif filetype is FileType:
173
c_type, c_enc=guess_type(file.name)
174
elif filetype is type(''):
176
c_type, c_enc=guess_type(self.url)
178
raise ValueError, 'File must be a filename, file or string.'
179
content_type=content_type or c_type
180
content_enc =content_enc or c_enc
181
if content_type: headers['Content-Type']=content_type
182
if content_enc: headers['Content-Encoding']=content_enc
183
headers['Content-Length']=str(len(body))
184
return self.__snd_request('PUT', self.uri, headers, body)
186
def options(self, **kw):
187
headers=self.__get_headers(kw)
188
return self.__snd_request('OPTIONS', self.uri, headers)
190
def trace(self, **kw):
191
headers=self.__get_headers(kw)
192
return self.__snd_request('TRACE', self.uri, headers)
194
def delete(self, **kw):
195
headers=self.__get_headers(kw)
196
return self.__snd_request('DELETE', self.uri, headers)
198
def propfind(self, body='', depth=0, **kw):
199
headers=self.__get_headers(kw)
200
headers['Depth']=str(depth)
201
headers['Content-Type']='text/xml; charset="utf-8"'
202
headers['Content-Length']=str(len(body))
203
return self.__snd_request('PROPFIND', self.uri, headers, body)
205
def proppatch(self, body, **kw):
206
headers=self.__get_headers(kw)
207
if body: headers['Content-Type']='text/xml; charset="utf-8"'
208
headers['Content-Length']=str(len(body))
209
return self.__snd_request('PROPPATCH', self.uri, headers, body)
211
def copy(self, dest, depth='infinity', overwrite=0, **kw):
212
"""Copy a resource to the specified destination."""
213
headers=self.__get_headers(kw)
214
headers['Overwrite']=overwrite and 'T' or 'F'
215
headers['Destination']=dest
216
headers['Depth']=depth
217
return self.__snd_request('COPY', self.uri, headers)
219
def move(self, dest, depth='infinity', overwrite=0, **kw):
220
"""Move a resource to the specified destination."""
221
headers=self.__get_headers(kw)
222
headers['Overwrite']=overwrite and 'T' or 'F'
223
headers['Destination']=dest
224
headers['Depth']=depth
225
return self.__snd_request('MOVE', self.uri, headers)
227
def mkcol(self, **kw):
228
headers=self.__get_headers(kw)
229
return self.__snd_request('MKCOL', self.uri, headers)
233
def lock(self, scope='exclusive', type='write', owner='',
234
depth='infinity', timeout='Infinite', **kw):
235
"""Create a lock with the specified scope, type, owner, depth
236
and timeout on the resource. A locked resource prevents a principal
237
without the lock from executing a PUT, POST, PROPPATCH, LOCK, UNLOCK,
238
MOVE, DELETE, or MKCOL on the locked resource."""
239
if not scope in ('shared', 'exclusive'):
240
raise ValueError, 'Invalid lock scope.'
241
if not type in ('write',):
242
raise ValueError, 'Invalid lock type.'
243
if not depth in ('0', 'infinity'):
244
raise ValueError, 'Invalid depth.'
245
headers=self.__get_headers(kw)
246
body='<?xml version="1.0" encoding="utf-8"?>\n' \
247
'<d:lockinfo xmlns:d="DAV:">\n' \
248
' <d:lockscope><d:%s/></d:lockscope>\n' \
249
' <d:locktype><d:%s/></d:locktype>\n' \
250
' <d:depth>%s</d:depth>\n' \
252
' <d:href>%s</d:href>\n' \
254
'</d:lockinfo>' % (scope, type, depth, owner)
255
headers['Content-Type']='text/xml; charset="utf-8"'
256
headers['Content-Length']=str(len(body))
257
headers['Timeout']=timeout
258
headers['Depth']=depth
259
return self.__snd_request('LOCK', self.uri, headers, body)
261
def unlock(self, token, **kw):
262
"""Remove the lock identified by token from the resource and all
263
other resources included in the lock. If all resources which have
264
been locked under the submitted lock token can not be unlocked the
265
unlock method will fail."""
266
headers=self.__get_headers(kw)
267
token='<opaquelocktoken:%s>' % str(token)
268
headers['Lock-Token']=token
269
return self.__snd_request('UNLOCK', self.uri, headers)
271
def allprops(self, depth=0):
272
return self.propfind('', depth)
274
def propnames(self, depth=0):
275
body='<?xml version="1.0" encoding="utf-8"?>\n' \
276
'<d:propfind xmlns:d="DAV:">\n' \
279
return self.propfind(body, depth)
281
def getprops(self, *names):
282
if not names: return self.propfind()
283
tags=string.join(names, '/>\n <')
284
body='<?xml version="1.0" encoding="utf-8"?>\n' \
285
'<d:propfind xmlns:d="DAV:">\n' \
289
'</d:propfind>' % tags
290
return self.propfind(body, 0)
292
def setprops(self, **props):
294
raise ValueError, 'No properties specified.'
296
for key, val in props.items():
297
tags.append(' <%s>%s</%s>' % (key, val, key))
298
tags=string.join(tags, '\n')
299
body='<?xml version="1.0" encoding="utf-8"?>\n' \
300
'<d:propertyupdate xmlns:d="DAV:">\n' \
306
'</d:propertyupdate>' % tags
307
return self.proppatch(body)
309
def delprops(self, *names):
311
raise ValueError, 'No property names specified.'
312
tags=string.join(names, '/>\n <')
313
body='<?xml version="1.0" encoding="utf-8"?>\n' \
314
'<d:propertyupdate xmlns:d="DAV:">\n' \
320
'</d:propfind>' % tags
321
return self.proppatch(body)
324
return '<HTTP resource %s>' % self.url
336
def __init__(self, ver, code, msg, headers, body):
343
def get_status(self):
344
return '%s %s' % (self.code, self.msg)
346
def get_header(self, name, val=None):
347
return self.headers.dict.get(string.lower(name), val)
349
def get_headers(self):
350
return self.headers.dict
357
data.append('%s %s %s\r\n' % (self.version, self.code, self.msg))
358
map(data.append, self.headers.headers)
360
data.append(self.body)
361
return string.join(data, '')
364
set_xml="""<?xml version="1.0" encoding="utf-8"?>
365
<d:propertyupdate xmlns:d="DAV:"
366
xmlns:z="http://www.zope.org/propsets/default">
369
<z:author>Brian Lloyd</z:author>
370
<z:title>My New Title</z:title>
376
funny="""<?xml version="1.0" encoding="utf-8"?>
377
<d:propertyupdate xmlns:d="DAV:"
378
xmlns:z="http://www.zope.org/propsets/default"
379
xmlns:q="http://www.something.com/foo/bar">
382
<z:author>Brian Lloyd</z:author>
383
<z:color>blue</z:color>
384
<z:count>72</z:count>
385
<q:Authors q:type="authorthing" z:type="string" xmlns:k="FOO:" xml:lang="en">
387
<q:Person k:thing="Im a thing!">
388
<q:Name>Brian Lloyd</q:Name>
401
rem_xml="""<?xml version="1.0" encoding="utf-8"?>
402
<d:propertyupdate xmlns:d="DAV:"
403
xmlns:z="http://www.zope.org/propsets/default">
413
find_xml="""<?xml version="1.0" encoding="utf-8" ?>
414
<D:propfind xmlns:D="DAV:">
415
<D:prop xmlns:z="http://www.zope.org/propsets/default">
425
##############################################################################
426
# Implementation details below here
429
urlreg=re.compile(r'http://([^:/]+)(:[0-9]+)?(/.+)?', re.I)
431
def marshal_string(name, val):
432
return '%s=%s' % (name, quote(str(val)))
434
def marshal_float(name, val):
435
return '%s:float=%s' % (name, val)
437
def marshal_int(name, val):
438
return '%s:int=%s' % (name, val)
440
def marshal_long(name, val):
441
value = '%s:long=%s' % (name, val)
446
def marshal_list(name, seq, tname='list', lt=type([]), tt=type(())):
451
raise TypeError, 'Invalid recursion in data to be marshaled.'
452
result.append(marshal_var("%s:%s" % (name, tname), v))
453
return string.join(result, '&')
455
def marshal_tuple(name, seq):
456
return marshal_list(name, seq, 'tuple')
459
vartypes=(('int', type(1), marshal_int),
460
('float', type(1.0), marshal_float),
461
('long', type(1L), marshal_long),
462
('list', type([]), marshal_list),
463
('tuple', type(()), marshal_tuple),
464
('string', type(''), marshal_string),
465
('file', types.FileType, None),
467
for name, tp, func in vartypes:
471
def marshal_var(name, val):
472
return varfuncs.get(type(val), marshal_string)(name, val)
477
def __init__(self,*args):
479
if c==1: name,val=None,args[0]
480
elif c==2: name,val=args[0],args[1]
481
else: raise ValueError, 'Invalid arguments'
483
h={'Content-Type': {'_v':''},
484
'Content-Transfer-Encoding': {'_v':''},
485
'Content-Disposition': {'_v':''},
490
if dt==types.DictType:
494
h['Content-Type']['_v']='multipart/form-data; boundary=%s' % b
495
for n,v in val.items():
496
d.append(MultiPart(n,v))
498
elif (dt==types.ListType) or (dt==types.TupleType):
499
raise ValueError, 'Sorry, nested multipart is not done yet!'
501
elif dt==types.FileType or hasattr(val,'read'):
502
if hasattr(val,'name'):
503
ct, enc=guess_type(val.name)
504
if not ct: ct='application/octet-stream'
505
fn=string.replace(val.name,'\\','/')
506
fn=fn[(string.rfind(fn,'/')+1):]
508
ct='application/octet-stream'
512
enc=enc or (ct[:6] in ('image/', 'applic') and 'binary' or '')
514
h['Content-Disposition']['_v'] ='form-data'
515
h['Content-Disposition']['name'] ='"%s"' % name
516
h['Content-Disposition']['filename']='"%s"' % fn
517
h['Content-Transfer-Encoding']['_v']=enc
518
h['Content-Type']['_v'] =ct
525
n=string.rfind(name, '__')
526
if n > 0: name='%s:%s' % (name[:n], name[n+2:])
527
h['Content-Disposition']['_v']='form-data'
528
h['Content-Disposition']['name']='"%s"' % name
538
return '%s_%s_%s' % (int(time.time()), os.getpid(), random())
546
for n,v in h.items():
548
s.append('%s: %s' % (n,v['_v']))
550
if k != '_v': s.append('; %s=%s' % (k, v[k]))
557
t.append('--%s\n' % b)
558
t.append(join(p,'\n--%s\n' % b))
559
t.append('\n--%s--\n' % b)
561
s.append('Content-Length: %s\n\n' % len(t))
566
for n,v in h.items():
568
s.append('%s: %s' % (n,v['_v']))
570
if k != '_v': s.append('; %s=%s' % (k, v[k]))
579
s.append('--%s\n' % b)
580
s.append(join(p,'\n--%s\n' % b))
581
s.append('\n--%s--\n' % b)
584
return join(s+self._data,'')
587
_extmap={'': 'text/plain',
595
'exe': 'application/octet-stream',
596
None : 'application/octet-stream',
599
_encmap={'image/gif': 'binary',
600
'image/jpg': 'binary',
601
'application/octet-stream': 'binary',
605
bri =Resource('http://tarzan.digicool.com/dev/brian3/',
608
abri=Resource('http://tarzan.digicool.com/dev/brian3/')
610
dav =Resource('http://tarzan.digicool.com/dev/dav/',
613
adav=Resource('http://tarzan.digicool.com/dev/dav/')