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