ba8c257aa16b5e7244751068f858064b5e0bdd6f
[pithos] / tools / lib / client.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from httplib import HTTPConnection, HTTP
35 from sys import stdin
36 from xml.dom import minidom
37
38 import json
39 import types
40 import socket
41 import urllib
42 import datetime
43
44 ERROR_CODES = {304:'Not Modified',
45                400:'Bad Request',
46                401:'Unauthorized',
47                404:'Not Found',
48                409:'Conflict',
49                411:'Length Required',
50                412:'Precondition Failed',
51                416:'Range Not Satisfiable',
52                422:'Unprocessable Entity',
53                503:'Service Unavailable'}
54
55 class Fault(Exception):
56     def __init__(self, data='', status=None):
57         if data == '' and status in ERROR_CODES.keys():
58             data = ERROR_CODES[status]
59         Exception.__init__(self, data)
60         self.data = data
61         self.status = status
62
63 class Client(object):
64     def __init__(self, host, token, account, api='v1', verbose=False, debug=False):
65         """`host` can also include a port, e.g '127.0.0.1:8000'."""
66         
67         self.host = host
68         self.account = account
69         self.api = api
70         self.verbose = verbose or debug
71         self.debug = debug
72         self.token = token
73     
74     def _req(self, method, path, body=None, headers={}, format='text', params={}):
75         full_path = '/%s%s?format=%s' % (self.api, path, format)
76         
77         for k,v in params.items():
78             if v:
79                 full_path = '%s&%s=%s' %(full_path, k, v)
80             else:
81                 full_path = '%s&%s=' %(full_path, k)
82         conn = HTTPConnection(self.host)
83         
84         #encode whitespace
85         full_path = full_path.replace(' ', '%20')
86         
87         kwargs = {}
88         for k,v in headers.items():
89             headers.pop(k)
90             k = k.replace('_', '-')
91             headers[k] = v
92         
93         kwargs['headers'] = headers
94         kwargs['headers']['X-Auth-Token'] = self.token
95         if body:
96             kwargs['body'] = body
97             kwargs['headers'].setdefault('content-type', 'application/octet-stream')
98         kwargs['headers'].setdefault('content-length', len(body) if body else 0)
99         
100         #print '#', method, full_path, kwargs
101         t1 = datetime.datetime.utcnow()
102         conn.request(method, full_path, **kwargs)
103         
104         resp = conn.getresponse()
105         t2 = datetime.datetime.utcnow()
106         #print 'response time:', str(t2-t1)
107         headers = dict(resp.getheaders())
108         
109         if self.verbose:
110             print '%d %s' % (resp.status, resp.reason)
111             for key, val in headers.items():
112                 print '%s: %s' % (key.capitalize(), val)
113             print
114         
115         length = resp.getheader('content-length', None)
116         data = resp.read(length)
117         if self.debug:
118             print data
119             print
120         
121         if int(resp.status) in ERROR_CODES.keys():
122             raise Fault(data, int(resp.status))
123         
124         #print '**',  resp.status, headers, data, '\n'
125         return resp.status, headers, data
126     
127     def delete(self, path, format='text', params={}):
128         return self._req('DELETE', path, format=format, params=params)
129     
130     def get(self, path, format='text', headers={}, params={}):
131         return self._req('GET', path, headers=headers, format=format,
132                         params=params)
133     
134     def head(self, path, format='text', params={}):
135          return self._req('HEAD', path, format=format, params=params)
136     
137     def post(self, path, body=None, format='text', headers=None, params={}):
138         return self._req('POST', path, body, headers=headers, format=format,
139                         params=params)
140     
141     def put(self, path, body=None, format='text', headers=None):
142         return self._req('PUT', path, body, headers=headers, format=format)
143     
144     def _list(self, path, format='text', params={}, **headers):
145         status, headers, data = self.get(path, format=format, headers=headers,
146                                          params=params)
147         if format == 'json':
148             data = json.loads(data) if data else ''
149         elif format == 'xml':
150             data = minidom.parseString(data)
151         else:
152             data = data.strip().split('\n') if data else ''
153         return data
154     
155     def _get_metadata(self, path, prefix=None, params={}):
156         status, headers, data = self.head(path, params=params)
157         prefixlen = len(prefix) if prefix else 0
158         meta = {}
159         for key, val in headers.items():
160             if prefix and not key.startswith(prefix):
161                 continue
162             elif prefix and key.startswith(prefix):
163                 key = key[prefixlen:]
164             meta[key] = val
165         return meta
166     
167     def _filter(self, l, d):
168         """
169         filter out from l elements having the metadata values provided
170         """
171         ll = l
172         for elem in l:
173             if type(elem) == types.DictionaryType:
174                 for key in d.keys():
175                     k = 'x_object_meta_%s' % key
176                     if k in elem.keys() and elem[k] == d[key]:
177                         ll.remove(elem)
178                         break
179         return ll
180     
181 class OOS_Client(Client):
182     """Openstack Object Storage Client"""
183     
184     def _update_metadata(self, path, entity, **meta):
185         """adds new and updates the values of previously set metadata"""
186         ex_meta = self.retrieve_account_metadata(restricted=True)
187         ex_meta.update(meta)
188         headers = {}
189         prefix = 'x-%s-meta-' % entity
190         for k,v in ex_meta.items():
191             k = '%s%s' % (prefix, k)
192             headers[k] = v
193         return self.post(path, headers=headers)
194     
195     def _reset_metadata(self, path, entity, **meta):
196         """
197         overwrites all user defined metadata
198         """
199         headers = {}
200         prefix = 'x-%s-meta-' % entity
201         for k,v in meta.items():
202             k = '%s%s' % (prefix, k)
203             headers[k] = v
204         return self.post(path, headers=headers)
205     
206     def _delete_metadata(self, path, entity, meta=[]):
207         """delete previously set metadata"""
208         ex_meta = self.retrieve_account_metadata(restricted=True)
209         headers = {}
210         prefix = 'x-%s-meta-' % entity
211         for k in ex_meta.keys():
212             if k in meta:
213                 headers['%s%s' % (prefix, k)] = ex_meta[k]
214         return self.post(path, headers=headers)
215     
216     # Storage Account Services
217     
218     def list_containers(self, format='text', limit=None,
219                         marker=None, params={}, account=None, **headers):
220         """lists containers"""
221         account = account or self.account
222         path = '/%s' % account
223         params.update({'limit':limit, 'marker':marker})
224         return self._list(path, format, params, **headers)
225     
226     def retrieve_account_metadata(self, restricted=False, account=None, **params):
227         """returns the account metadata"""
228         account = account or self.account
229         path = '/%s' % account
230         prefix = 'x-account-meta-' if restricted else None
231         return self._get_metadata(path, prefix, params)
232     
233     def update_account_metadata(self, account=None, **meta):
234         """updates the account metadata"""
235         account = account or self.account
236         path = '/%s' % account
237         return self._update_metadata(path, 'account', **meta)
238         
239     def delete_account_metadata(self, meta=[], account=None):
240         """deletes the account metadata"""
241         account = account or self.account
242         path = '/%s' % account
243         return self._delete_metadata(path, 'account', meta)
244     
245     def reset_account_metadata(self, account=None, **meta):
246         """resets account metadata"""
247         account = account or self.account
248         path = '/%s' % account
249         return self._reset_metadata(path, 'account', **meta)
250     
251     # Storage Container Services
252     
253     def _filter_trashed(self, l):
254         return self._filter(l, {'trash':'true'})
255     
256     def list_objects(self, container, format='text',
257                      limit=None, marker=None, prefix=None, delimiter=None,
258                      path=None, include_trashed=False, params={}, account=None,
259                      **headers):
260         """returns a list with the container objects"""
261         account = account or self.account
262         params.update({'limit':limit, 'marker':marker, 'prefix':prefix,
263                        'delimiter':delimiter, 'path':path})
264         l = self._list('/%s/%s' % (account, container), format, params,
265                        **headers)
266         #TODO support filter trashed with xml also
267         if format != 'xml' and not include_trashed:
268             l = self._filter_trashed(l)
269         return l
270     
271     def create_container(self, container, account=None, **meta):
272         """creates a container"""
273         account = account or self.account
274         headers = {}
275         for k,v in meta.items():
276             headers['x-container-meta-%s' %k.strip().upper()] = v.strip()
277         status, header, data = self.put('/%s/%s' % (account, container),
278                                         headers=headers)
279         if status == 202:
280             return False
281         elif status != 201:
282             raise Fault(data, int(status))
283         return True
284     
285     def delete_container(self, container, params={}, account=None):
286         """deletes a container"""
287         account = account or self.account
288         return self.delete('/%s/%s' % (account, container), params=params)
289     
290     def retrieve_container_metadata(self, container, restricted=False,
291                                     account=None, **params):
292         """returns the container metadata"""
293         account = account or self.account
294         prefix = 'x-container-meta-' if restricted else None
295         return self._get_metadata('/%s/%s' % (account, container), prefix,
296                                   params)
297     
298     def update_container_metadata(self, container, account=None, **meta):
299         """unpdates the container metadata"""
300         account = account or self.account
301         return self._update_metadata('/%s/%s' % (account, container),
302                                      'container', **meta)
303         
304     def delete_container_metadata(self, container, meta=[], account=None):
305         """deletes the container metadata"""
306         account = account or self.account
307         path = '/%s/%s' % (account, container)
308         return self._delete_metadata(path, 'container', meta)
309     
310     # Storage Object Services
311     
312     def request_object(self, container, object, format='text', params={},
313                        account=None, **headers):
314         """returns tuple containing the status, headers and data response for an object request"""
315         account = account or self.account
316         path = '/%s/%s/%s' % (account, container, object)
317         status, headers, data = self.get(path, format, headers, params)
318         return status, headers, data
319     
320     def retrieve_object(self, container, object, format='text', params={},
321                         account=None, **headers):
322         """returns an object's data"""
323         account = account or self.account
324         t = self.request_object(container, object, format, params, account,
325                                 **headers)
326         data = t[2]
327         if format == 'json':
328             data = json.loads(data) if data else ''
329         elif format == 'xml':
330             data = minidom.parseString(data)
331         return data
332     
333     def retrieve_object_hashmap(self, container, object, params={},
334                         account=None, **headers):
335         """returns the hashmap representing object's data"""
336         args = locals()
337         for elem in ['self', 'container', 'object']:
338             args.pop(elem)
339         data = self.retrieve_object(container, object, format='json', **args)
340         return data['hashes']
341     
342     def create_directory_marker(self, container, object, account=None):
343         """creates a dierectory marker"""
344         account = account or self.account
345         if not object:
346             raise Fault('Directory markers have to be nested in a container')
347         h = {'content_type':'application/directory'}
348         return self.create_zero_length_object(container, object, account=account,
349                                               **h)
350     
351     def create_object(self, container, object, f=stdin, format='text', meta={},
352                       etag=None, content_type=None, content_encoding=None,
353                       content_disposition=None, account=None, **headers):
354         """creates a zero-length object"""
355         account = account or self.account
356         path = '/%s/%s/%s' % (account, container, object)
357         for k, v  in headers.items():
358             if not v:
359                 headers.pop(k)
360         
361         l = ['etag', 'content_encoding', 'content_disposition', 'content_type']
362         l = [elem for elem in l if eval(elem)]
363         for elem in l:
364             headers.update({elem:eval(elem)})
365         headers.setdefault('content-type', 'application/octet-stream')
366         
367         for k,v in meta.items():
368             headers['x-object-meta-%s' %k.strip()] = v.strip()
369         data = f.read() if f else None
370         return self.put(path, data, format, headers=headers)
371     
372     def create_zero_length_object(self, container, object, meta={}, etag=None,
373                                   content_type=None, content_encoding=None,
374                                   content_disposition=None, account=None,
375                                   **headers):
376         account = account or self.account
377         args = locals()
378         for elem in ['self', 'container', 'headers', 'account']:
379             args.pop(elem)
380         args.update(headers)
381         return self.create_object(container, account=account, f=None, **args)
382     
383     def update_object(self, container, object, f=stdin,
384                       offset=None, meta={}, content_length=None,
385                       content_type=None, content_encoding=None,
386                       content_disposition=None,  account=None, **headers):
387         account = account or self.account
388         path = '/%s/%s/%s' % (account, container, object)
389         for k, v  in headers.items():
390             if not v:
391                 headers.pop(k)
392         
393         l = ['content_encoding', 'content_disposition', 'content_type',
394              'content_length']
395         l = [elem for elem in l if eval(elem)]
396         for elem in l:
397             headers.update({elem:eval(elem)})
398             
399         if 'content_range' not in headers.keys():
400             if offset != None:
401                 headers['content_range'] = 'bytes %s-/*' % offset
402             else:
403                 headers['content_range'] = 'bytes */*'
404             
405         for k,v in meta.items():
406             headers['x-object-meta-%s' %k.strip()] = v.strip()
407         data = f.read() if f else None
408         return self.post(path, data, headers=headers)
409     
410     def _change_obj_location(self, src_container, src_object, dst_container,
411                              dst_object, remove=False, meta={}, account=None,
412                              **headers):
413         account = account or self.account
414         path = '/%s/%s/%s' % (account, dst_container, dst_object)
415         headers = {} if not headers else headers
416         for k, v in meta.items():
417             headers['x-object-meta-%s' % k] = v
418         if remove:
419             headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
420         else:
421             headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
422         headers['content_length'] = 0
423         return self.put(path, headers=headers)
424     
425     def copy_object(self, src_container, src_object, dst_container, dst_object,
426                    meta={}, account=None, **headers):
427         """copies an object"""
428         account = account or self.account
429         return self._change_obj_location(src_container, src_object,
430                                    dst_container, dst_object, account=account,
431                                    remove=False, meta=meta, **headers)
432     
433     def move_object(self, src_container, src_object, dst_container,
434                              dst_object, meta={}, account=None,
435                              **headers):
436         """moves an object"""
437         account = account or self.account
438         return self._change_obj_location(src_container, src_object,
439                                          dst_container, dst_object,
440                                          account=account, remove=True,
441                                          meta=meta, **headers)
442     
443     def delete_object(self, container, object, params={}, account=None):
444         """deletes an object"""
445         account = account or self.account
446         return self.delete('/%s/%s/%s' % (account, container, object),
447                            params=params)
448     
449     def retrieve_object_metadata(self, container, object, restricted=False,
450                                  version=None, account=None):
451         """
452         set restricted to True to get only user defined metadata
453         """
454         account = account or self.account
455         path = '/%s/%s/%s' % (account, container, object)
456         prefix = 'x-object-meta-' if restricted else None
457         params = {'version':version} if version else {}
458         return self._get_metadata(path, prefix, params=params)
459     
460     def update_object_metadata(self, container, object, account=None,
461                                **meta):
462         """
463         updates object's metadata
464         """
465         account = account or self.account
466         path = '/%s/%s/%s' % (account, container, object)
467         return self._update_metadata(path, 'object', **meta)
468     
469     def delete_object_metadata(self, container, object, meta=[], account=None):
470         """
471         deletes object's metadata
472         """
473         account = account or self.account
474         path = '/%s/%s' % (account, container, object)
475         return self._delete_metadata(path, 'object', meta)
476     
477 class Pithos_Client(OOS_Client):
478     """Pithos Storage Client. Extends OOS_Client"""
479     
480     def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
481                           blocksize=1024):
482         """perfomrs a chunked request"""
483         http = HTTPConnection(self.host)
484         
485         # write header
486         path = '/%s%s' % (self.api, path)
487         http.putrequest(method, path)
488         http.putheader('x-auth-token', self.token)
489         http.putheader('content-type', 'application/octet-stream')
490         http.putheader('transfer-encoding', 'chunked')
491         if headers:
492             for header,value in headers.items():
493                 http.putheader(header, value)
494         http.endheaders()
495         
496         # write body
497         data = ''
498         while True:
499             if f.closed:
500                 break
501             block = f.read(blocksize)
502             if block == '':
503                 break
504             data = '%x\r\n%s\r\n' % (len(block), block)
505             try:
506                 http.send(data)
507             except:
508                 #retry
509                 http.send(data)
510         data = '0\r\n\r\n'
511         try:
512             http.send(data)
513         except:
514             #retry
515             http.send(data)
516         
517         # get response
518         resp = http.getresponse()
519         
520         headers = dict(resp.getheaders())
521         
522         if self.verbose:
523             print '%d %s' % (resp.status, resp.reason)
524             for key, val in headers.items():
525                 print '%s: %s' % (key.capitalize(), val)
526             print
527         
528         length = resp.getheader('Content-length', None)
529         data = resp.read(length)
530         if self.debug:
531             print data
532             print
533         
534         if int(resp.status) in ERROR_CODES.keys():
535             raise Fault(data, int(resp.status))
536         
537         #print '*',  resp.status, headers, data
538         return resp.status, headers, data
539     
540     def _update_metadata(self, path, entity, **meta):
541         """
542         adds new and updates the values of previously set metadata
543         """
544         params = {'update':None}
545         headers = {}
546         prefix = 'x-%s-meta-' % entity
547         for k,v in meta.items():
548             k = '%s%s' % (prefix, k)
549             headers[k] = v
550         return self.post(path, headers=headers, params=params)
551     
552     def _delete_metadata(self, path, entity, meta=[]):
553         """
554         delete previously set metadata
555         """
556         params = {'update':None}
557         headers = {}
558         prefix = 'x-%s-meta-' % entity
559         for m in meta:
560             headers['%s%s' % (prefix, m)] = ''
561         return self.post(path, headers=headers, params=params)
562     
563     # Storage Account Services
564     
565     def list_containers(self, format='text', if_modified_since=None,
566                         if_unmodified_since=None, limit=None, marker=None,
567                         shared=False, until=None, account=None):
568         """returns a list with the account containers"""
569         account = account or self.account
570         params = {'until':until} if until else {}
571         if shared:
572             params['shared'] = None
573         headers = {'if-modified-since':if_modified_since,
574                    'if-unmodified-since':if_unmodified_since}
575         return OOS_Client.list_containers(self, account=account, format=format,
576                                           limit=limit, marker=marker,
577                                           params=params, **headers)
578     
579     def retrieve_account_metadata(self, restricted=False, until=None,
580                                   account=None):
581         """returns the account metadata"""
582         account = account or self.account
583         params = {'until':until} if until else {}
584         return OOS_Client.retrieve_account_metadata(self, account=account,
585                                                     restricted=restricted,
586                                                     **params)
587     
588     def set_account_groups(self, account=None, **groups):
589         """create account groups"""
590         account = account or self.account
591         path = '/%s' % account
592         headers = {}
593         for k, v in groups.items():
594             headers['x-account-group-%s' % k] = v
595         params = {'update':None}
596         return self.post(path, headers=headers, params=params)
597     
598     def retrieve_account_groups(self, account=None):
599         """returns the account groups"""
600         account = account or self.account
601         meta = self.retrieve_account_metadata(account=account)
602         prefix = 'x-account-group-'
603         prefixlen = len(prefix)
604         groups = {}
605         for key, val in meta.items():
606             if prefix and not key.startswith(prefix):
607                 continue
608             elif prefix and key.startswith(prefix):
609                 key = key[prefixlen:]
610             groups[key] = val
611         return groups
612     
613     def unset_account_groups(self, groups=[], account=None):
614         """delete account groups"""
615         account = account or self.account
616         path = '/%s' % account
617         headers = {}
618         for elem in groups:
619             headers['x-account-group-%s' % elem] = ''
620         params = {'update':None}
621         return self.post(path, headers=headers, params=params)
622     
623     def reset_account_groups(self, account=None, **groups):
624         """overrides account groups"""
625         account = account or self.account
626         path = '/%s' % account
627         headers = {}
628         for k, v in groups.items():
629             v = v.strip()
630             headers['x-account-group-%s' % k] = v
631         meta = self.retrieve_account_metadata(restricted=True)
632         prefix = 'x-account-meta-'
633         for k,v in meta.items():
634             k = '%s%s' % (prefix, k)
635             headers[k] = v
636         return self.post(path, headers=headers)
637     
638     # Storage Container Services
639     
640     def list_objects(self, container, format='text',
641                      limit=None, marker=None, prefix=None, delimiter=None,
642                      path=None, shared=False, include_trashed=False, params={},
643                      if_modified_since=None, if_unmodified_since=None, meta='',
644                      until=None, account=None):
645         """returns a list with the container objects"""
646         account = account or self.account
647         params = {'until':until, 'meta':meta}
648         if shared:
649             params['shared'] = None
650         args = locals()
651         for elem in ['self', 'container', 'params', 'until', 'meta']:
652             args.pop(elem)
653         return OOS_Client.list_objects(self, container, params=params, **args)
654     
655     def retrieve_container_metadata(self, container, restricted=False,
656                                     until=None, account=None):
657         """returns container's metadata"""
658         account = account or self.account
659         params = {'until':until} if until else {}
660         return OOS_Client.retrieve_container_metadata(self, container,
661                                                       account=account,
662                                                       restricted=restricted,
663                                                       **params)
664     
665     def set_container_policies(self, container, account=None,
666                                **policies):
667         """sets containers policies"""
668         account = account or self.account
669         path = '/%s/%s' % (account, container)
670         headers = {}
671         for key, val in policies.items():
672             headers['x-container-policy-%s' % key] = val
673         return self.post(path, headers=headers)
674     
675     def delete_container(self, container, until=None, account=None):
676         """deletes a container or the container history until the date provided"""
677         account = account or self.account
678         params = {'until':until} if until else {}
679         return OOS_Client.delete_container(self, container, account=account,
680                                            params=params)
681     
682     # Storage Object Services
683     
684     def retrieve_object(self, container, object, params={}, format='text',
685                         range=None, if_range=None,
686                         if_match=None, if_none_match=None,
687                         if_modified_since=None, if_unmodified_since=None,
688                         account=None, **headers):
689         """returns an object"""
690         account = account or self.account
691         headers={}
692         l = ['range', 'if_range', 'if_match', 'if_none_match',
693              'if_modified_since', 'if_unmodified_since']
694         l = [elem for elem in l if eval(elem)]
695         for elem in l:
696             headers.update({elem:eval(elem)})
697         return OOS_Client.retrieve_object(self, container, object,
698                                           account=account, format=format,
699                                           params=params, **headers)
700     
701     def retrieve_object_version(self, container, object, version,
702                                 format='text', range=None, if_range=None,
703                                 if_match=None, if_none_match=None,
704                                 if_modified_since=None, if_unmodified_since=None,
705                                 account=None):
706         """returns a specific object version"""
707         account = account or self.account
708         args = locals()
709         l = ['self', 'container', 'object']
710         for elem in l:
711             args.pop(elem)
712         params = {'version':version}
713         return self.retrieve_object(container, object, params=params, **args)
714     
715     def retrieve_object_versionlist(self, container, object, range=None,
716                                     if_range=None, if_match=None,
717                                     if_none_match=None, if_modified_since=None,
718                                     if_unmodified_since=None, account=None):
719         """returns the object version list"""
720         account = account or self.account
721         args = locals()
722         l = ['self', 'container', 'object']
723         for elem in l:
724             args.pop(elem)
725         
726         return self.retrieve_object_version(container, object, version='list',
727                                             format='json', **args)
728     
729     def create_zero_length_object(self, container, object,
730                                   meta={}, etag=None, content_type=None,
731                                   content_encoding=None,
732                                   content_disposition=None,
733                                   x_object_manifest=None, x_object_sharing=None,
734                                   x_object_public=None, account=None):
735         """createas a zero length object"""
736         account = account or self.account
737         args = locals()
738         for elem in ['self', 'container', 'object']:
739             args.pop(elem)
740         return OOS_Client.create_zero_length_object(self, container, object,
741                                                     **args)
742     
743     def create_object(self, container, object, f=stdin, 
744                       meta={}, etag=None, content_type=None,
745                       content_encoding=None, content_disposition=None,
746                       x_object_manifest=None, x_object_sharing=None,
747                       x_object_public=None, account=None):
748         """creates an object"""
749         account = account or self.account
750         args = locals()
751         for elem in ['self', 'container', 'object']:
752             args.pop(elem)
753         return OOS_Client.create_object(self, container, object, **args)
754         
755     def create_object_using_chunks(self, container, object,
756                                    f=stdin, blocksize=1024, meta={}, etag=None,
757                                    content_type=None, content_encoding=None,
758                                    content_disposition=None,
759                                    x_object_sharing=None, x_object_manifest=None,
760                                    x_object_public=None, account=None):
761         """creates an object (incremental upload)"""
762         account = account or self.account
763         path = '/%s/%s/%s' % (account, container, object)
764         headers = {}
765         l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
766              'x_object_sharing', 'x_object_manifest', 'x_object_public']
767         l = [elem for elem in l if eval(elem)]
768         for elem in l:
769             headers.update({elem:eval(elem)})
770         headers.setdefault('content-type', 'application/octet-stream')
771         
772         for k,v in meta.items():
773             v = v.strip()
774             headers['x-object-meta-%s' %k.strip()] = v
775         
776         return self._chunked_transfer(path, 'PUT', f, headers=headers,
777                                       blocksize=blocksize)
778     
779     def create_object_by_hashmap(container, object, f=stdin, format='json',
780                                  meta={}, etag=None, content_encoding=None,
781                                  content_disposition=None, content_type=None,
782                                  x_object_sharing=None, x_object_manifest=None,
783                                  x_object_public = None, account=None):
784         """creates an object by uploading hashes representing data instead of data"""
785         account = account or self.account
786         args = locals()
787         for elem in ['self', 'container', 'object']:
788             args.pop(elem)
789             
790         data = f.read() if f else None
791         if data and format == 'json':
792             try:
793                 data = eval(data)
794                 data = json.dumps(data)
795             except SyntaxError:
796                 raise Fault('Invalid formatting')
797         
798         #TODO check with xml
799         return self.create_object(container, object, **args)
800     
801     def create_manifestation(self, container, object, manifest, account=None):
802         """creates a manifestation"""
803         account = account or self.account
804         headers={'x_object_manifest':manifest}
805         return self.create_object(container, object, f=None, account=account,
806                                   **headers)
807     
808     def update_object(self, container, object, f=stdin,
809                       offset=None, meta={}, content_length=None,
810                       content_type=None, content_range=None,
811                       content_encoding=None, content_disposition=None,
812                       x_object_bytes=None, x_object_manifest=None,
813                       x_object_sharing=None, x_object_public=None,
814                       x_source_object=None, account=None):
815         """updates an object"""
816         account = account or self.account
817         args = locals()
818         for elem in ['self', 'container', 'object']:
819             args.pop(elem)
820         return OOS_Client.update_object(self, container, object, **args)
821         
822     def update_object_using_chunks(self, container, object, f=stdin,
823                                    blocksize=1024, offset=None, meta={},
824                                    content_type=None, content_encoding=None,
825                                    content_disposition=None, x_object_bytes=None,
826                                    x_object_manifest=None, x_object_sharing=None,
827                                    x_object_public=None, account=None):
828         """updates an object (incremental upload)"""
829         account = account or self.account
830         path = '/%s/%s/%s' % (account, container, object)
831         headers = {}
832         l = ['content_type', 'content_encoding', 'content_disposition',
833              'x_object_bytes', 'x_object_manifest', 'x_object_sharing',
834              'x_object_public']
835         l = [elem for elem in l if eval(elem)]
836         for elem in l:
837             headers.update({elem:eval(elem)})
838         
839         if offset != None:
840             headers['content_range'] = 'bytes %s-/*' % offset
841         else:
842             headers['content_range'] = 'bytes */*'
843         
844         for k,v in meta.items():
845             v = v.strip()
846             headers['x-object-meta-%s' %k.strip()] = v
847         return self._chunked_transfer(path, 'POST', f, headers=headers,
848                                       blocksize=blocksize)
849     
850     def update_from_other_source(self, container, object, source,
851                       offset=None, meta={}, content_range=None,
852                       content_encoding=None, content_disposition=None,
853                       x_object_bytes=None, x_object_manifest=None,
854                       x_object_sharing=None, x_object_public=None, account=None):
855         """updates an object"""
856         account = account or self.account
857         args = locals()
858         for elem in ['self', 'container', 'object', 'source']:
859             args.pop(elem)
860         
861         args['x_source_object'] = source
862         return self.update_object(container, object, f=None, **args)
863     
864     def delete_object(self, container, object, until=None, account=None):
865         """deletes an object or the object history until the date provided"""
866         account = account or self.account
867         params = {'until':until} if until else {}
868         return OOS_Client.delete_object(self, container, object, params, account)
869     
870     def trash_object(self, container, object):
871         """trashes an object"""
872         account = account or self.account
873         path = '/%s/%s' % (container, object)
874         meta = {'trash':'true'}
875         return self._update_metadata(path, 'object', **meta)
876     
877     def restore_object(self, container, object, account=None):
878         """restores a trashed object"""
879         account = account or self.account
880         return self.delete_object_metadata(container, object, account, ['trash'])
881     
882     def publish_object(self, container, object, account=None):
883         """sets a previously created object publicly accessible"""
884         account = account or self.account
885         path = '/%s/%s/%s' % (account, container, object)
886         headers = {}
887         headers['x_object_public'] = True
888         params = {'update':None}
889         return self.post(path, headers=headers, params=params)
890     
891     def unpublish_object(self, container, object, account=None):
892         """unpublish an object"""
893         account = account or self.account
894         path = '/%s/%s/%s' % (account, container, object)
895         headers = {}
896         headers['x_object_public'] = False
897         params = {'update':None}
898         return self.post(path, headers=headers, params=params)
899     
900     def _change_obj_location(self, src_container, src_object, dst_container,
901                              dst_object, remove=False,
902                              meta={}, account=None, **headers):
903         account = account or self.account
904         path = '/%s/%s/%s' % (account, dst_container, dst_object)
905         headers = {} if not headers else headers
906         for k, v in meta.items():
907             headers['x-object-meta-%s' % k] = v
908         if remove:
909             headers['x-move-from'] = '/%s/%s' % (src_container, src_object)
910         else:
911             headers['x-copy-from'] = '/%s/%s' % (src_container, src_object)
912         headers['content_length'] = 0
913         return self.put(path, headers=headers)
914     
915     def copy_object(self, src_container, src_object, dst_container, dst_object,
916                     meta={}, public=False, version=None, account=None):
917         """copies an object"""
918         account = account or self.account
919         headers = {}
920         headers['x_object_public'] = public
921         if version:
922             headers['x_object_version'] = version
923         return OOS_Client.copy_object(self, src_container, src_object,
924                                       dst_container, dst_object, meta=meta,
925                                       account=account,**headers)
926     
927     def move_object(self, src_container, src_object, dst_container,
928                              dst_object, meta={}, public=False, version=None,
929                              account=None):
930         """moves an object"""
931         headers = {}
932         headers['x_object_public'] = public
933         if version:
934             headers['x_object_version'] = version
935         return OOS_Client.move_object(self, src_container, src_object,
936                                       dst_container, dst_object, meta=meta,
937                                       account=account, **headers)
938     
939     def list_shared_by_others(self, limit=None, marker=None, format='text'):
940         """lists other accounts that share objects to the user"""
941         l = ['limit', 'marker']
942         params = {}
943         for elem in [elem for elem in l if eval(elem)]:
944             params[elem] = eval(elem)
945         return self._list('', format, params)
946     
947     def share_object(self, container, object, l, read=True):
948         """gives access(read by default) to an object to a user/group list"""
949         action = 'read' if read else 'write'
950         sharing = '%s=%s' % (action, ','.join(l))
951         self.update_object(container, object, f=None, x_object_sharing=sharing)
952
953 def _encode_headers(headers):
954     h = {}
955     for k, v in headers.items():
956         k = urllib.quote(k)
957         if v and type(v) == types.StringType:
958             v = urllib.quote(v, '/=,-* :"')
959         h[k] = v
960     return h