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