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