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