bug fixing client lib
[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, public=False, **meta):
369         path = '/%s/%s' % (dst_container, dst_object)
370         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         self._set_public_header(headers, public)
378         self.headers = headers if headers else None
379         headers['content-length'] = 0
380         return self.put(path, headers=headers)
381     
382     def copy_object(self, src_container, src_object, dst_container,
383                              dst_object, public=False, **meta):
384         return self._change_obj_location(src_container, src_object,
385                                    dst_container, dst_object, False,
386                                    public, **meta)
387     
388     def move_object(self, src_container, src_object, dst_container,
389                              dst_object, public=False, **meta):
390         return self._change_obj_location(src_container, src_object,
391                                    dst_container, dst_object, True,
392                                    public, **meta)
393     
394     def delete_object(self, container, object, params={}):
395         return self.delete('/%s/%s' % (container, object), params=params)
396     
397     def retrieve_object_metadata(self, container, object, restricted=False,
398                                  version=None):
399         """
400         set restricted to True to get only user defined metadata
401         """
402         path = '/%s/%s' % (container, object)
403         prefix = 'x-object-meta-' if restricted else None
404         params = {'version':version} if version else {}
405         return self._get_metadata(path, prefix, params=params)
406     
407     def update_object_metadata(self, container, object, **meta):
408         path = '/%s/%s' % (container, object)
409         return self._update_metadata(path, 'object', **meta)
410     
411     def delete_object_metadata(self, container, object, meta=[]):
412         path = '/%s/%s' % (container, object)
413         return self._delete_metadata(path, 'object', meta)
414     
415 class Pithos_Client(OOS_Client):
416     """Pithos Storage Client. Extends OOS_Client"""
417     
418     def _chunked_transfer(self, path, method='PUT', f=stdin, headers=None,
419                           blocksize=1024):
420         """perfomrs a chunked request"""
421         http = HTTPConnection(self.host)
422         
423         # write header
424         path = '/%s/%s%s' % (self.api, self.account, path)
425         http.putrequest(method, path)
426         http.putheader('x-auth-token', self.token)
427         http.putheader('content-type', 'application/octet-stream')
428         http.putheader('transfer-encoding', 'chunked')
429         if headers:
430             for header,value in headers.items():
431                 http.putheader(header, value)
432         http.endheaders()
433         
434         # write body
435         data = ''
436         while True:
437             if f.closed:
438                 break
439             block = f.read(blocksize)
440             if block == '':
441                 break
442             data = '%s\r\n%s\r\n' % (hex(len(block)), block)
443             try:
444                 http.send(data)
445             except:
446                 #retry
447                 http.send(data)
448         data = '0x0\r\n'
449         try:
450             http.send(data)
451         except:
452             #retry
453             http.send(data)
454         
455         # get response
456         resp = http.getresponse()
457         
458         headers = dict(resp.getheaders())
459         
460         if self.verbose:
461             print '%d %s' % (resp.status, resp.reason)
462             for key, val in headers.items():
463                 print '%s: %s' % (key.capitalize(), val)
464             print
465         
466         length = resp.getheader('Content-length', None)
467         data = resp.read(length)
468         if self.debug:
469             print data
470             print
471         
472         if int(resp.status) in ERROR_CODES.keys():
473             raise Fault(data, int(resp.status))
474         
475         #print '*',  resp.status, headers, data
476         return resp.status, headers, data
477     
478     def _update_metadata(self, path, entity, **meta):
479         """
480         adds new and updates the values of previously set metadata
481         """
482         params = {'update':None}
483         headers = {}
484         prefix = 'x-%s-meta-' % entity
485         for k,v in meta.items():
486             k = '%s%s' % (prefix, k)
487             headers[k] = v
488         return self.post(path, headers=headers, params=params)
489     
490     def _delete_metadata(self, path, entity, meta=[]):
491         """
492         delete previously set metadata
493         """
494         params = {'update':None}
495         headers = {}
496         prefix = 'x-%s-meta-' % entity
497         for m in meta:
498             headers['%s%s' % (prefix, m)] = ''
499         return self.post(path, headers=headers, params=params)
500     
501     # Storage Account Services
502     
503     def list_containers(self, format='text', if_modified_since=None,
504                         if_unmodified_since=None, limit=1000, marker=None,
505                         until=None):
506         """returns a list with the account containers"""
507         params = {'until':until} if until else None
508         headers = {'if-modified-since':if_modified_since,
509                    'if-unmodified-since':if_unmodified_since}
510         return OOS_Client.list_containers(self, format=format, limit=limit,
511                                           marker=marker, params=params,
512                                           **headers)
513     
514     def retrieve_account_metadata(self, restricted=False, until=None):
515         """returns the account metadata"""
516         params = {'until':until} if until else {}
517         return OOS_Client.retrieve_account_metadata(self, restricted=restricted,
518                                                    **params)
519     
520     def set_account_groups(self, **groups):
521         """create account groups"""
522         headers = {}
523         for key, val in groups.items():
524             headers['x-account-group-%s' % key] = val
525         params = {'update':None}
526         return self.post('', headers=headers, params=params)
527     
528     def retrieve_account_groups(self):
529         """returns the account groups"""
530         meta = self.retrieve_account_metadata()
531         prefix = 'x-account-group-'
532         prefixlen = len(prefix)
533         groups = {}
534         for key, val in meta.items():
535             if prefix and not key.startswith(prefix):
536                 continue
537             elif prefix and key.startswith(prefix):
538                 key = key[prefixlen:]
539             groups[key] = val
540         return groups
541     
542     def unset_account_groups(self, groups=[]):
543         """delete account groups"""
544         headers = {}
545         for elem in groups:
546             headers['x-account-group-%s' % elem] = ''
547         params = {'update':None}
548         return self.post('', headers=headers, params=params)
549     
550     def reset_account_groups(self, **groups):
551         """overrides account groups"""
552         headers = {}
553         for key, val in groups.items():
554             headers['x-account-group-%s' % key] = val
555         meta = self.retrieve_account_metadata()
556         headers.update(meta)
557         return self.post('', headers=headers)
558     
559     # Storage Container Services
560     
561     def list_objects(self, container, format='text', limit=10000, marker=None,
562                      prefix=None, delimiter=None, path=None,
563                      include_trashed=False, params={}, if_modified_since=None,
564                      if_unmodified_since=None, meta={}, until=None):
565         """returns a list with the container objects"""
566         params = {'until':until, 'meta':meta}
567         args = locals()
568         for elem in ['self', 'container', 'params', 'until', 'meta']:
569             args.pop(elem)
570         return OOS_Client.list_objects(self, container, params=params, 
571                                        **args)
572     
573     def retrieve_container_metadata(self, container, restricted=False,
574                                     until=None):
575         """returns container's metadata"""
576         params = {'until':until} if until else {}
577         return OOS_Client.retrieve_container_metadata(self, container,
578                                                       restricted=restricted,
579                                                       **params)
580     
581     def set_container_policies(self, container, **policies):
582         """sets containers policies"""
583         path = '/%s' % (container)
584         headers = {}
585         print ''
586         for key, val in policies.items():
587             headers['x-container-policy-%s' % key] = val
588         return self.post(path, headers=headers)
589     
590     def delete_container(self, container, until=None):
591         """deletes a container or the container history until the date provided"""
592         params = {'until':until} if until else {}
593         return OOS_Client.delete_container(self, container, params)
594     
595     # Storage Object Services
596     
597     def retrieve_object(self, container, object, params={}, format='text', range=None,
598                         if_range=None, if_match=None, if_none_match=None,
599                         if_modified_since=None, if_unmodified_since=None,
600                         **headers):
601         """returns an object"""
602         headers={}
603         l = ['range', 'if_range', 'if_match', 'if_none_match',
604              'if_modified_since', 'if_unmodified_since']
605         l = [elem for elem in l if eval(elem)]
606         for elem in l:
607             headers.update({elem:eval(elem)})
608         return OOS_Client.retrieve_object(self, container, object, format=format,
609                                           params=params, **headers)
610     
611     def retrieve_object_version(self, container, object, version, detail=False,
612                                 range=None, if_range=None, if_match=None,
613                                 if_none_match=None, if_modified_since=None,
614                                 if_unmodified_since=None):
615         """returns a specific object version"""
616         args = locals()
617         l = ['self', 'container', 'object']
618         for elem in l:
619             args.pop(elem)
620         params = {'version':version}
621         return self.retrieve_object(container, object, params, **args)
622     
623     def retrieve_object_versionlist(self, container, object, range=None,
624                                     if_range=None, if_match=None,
625                                     if_none_match=None, if_modified_since=None,
626                                     if_unmodified_since=None):
627         """returns the object version list"""
628         args = locals()
629         l = ['self', 'container', 'object']
630         for elem in l:
631             args.pop(elem)
632             
633         return self.retrieve_object_version(container, object, version='list',
634                                             detail=True, **args)
635     
636     def create_zero_length_object(self, container, object, meta={},
637                       etag=None, content_type=None, content_encoding=None,
638                       content_disposition=None, x_object_manifest=None,
639                       x_object_sharing=None, x_object_public=None):
640         """createas a zero length object"""
641         args = locals()
642         for elem in ['self', 'container', 'object']:
643             args.pop(elem)
644         return OOS_Client.create_zero_length_object(self, container, object,
645                                                     **args)
646     
647     def create_object(self, container, object, f=stdin, meta={},
648                       etag=None, content_type=None, content_encoding=None,
649                       content_disposition=None, x_object_manifest=None,
650                       x_object_sharing=None, x_object_public=None):
651         """creates an object"""
652         args = locals()
653         for elem in ['self', 'container', 'object']:
654             args.pop(elem)
655         return OOS_Client.create_object(self, container, object, **args)
656         
657     def create_object_using_chunks(self, container, object, f=stdin,
658                                     blocksize=1024, meta={}, etag=None,
659                                     content_type=None, content_encoding=None,
660                                     content_disposition=None, 
661                                     x_object_sharing=None,
662                                     x_object_manifest=None, 
663                                     x_object_public=None):
664         """creates an object (incremental upload)"""
665         path = '/%s/%s' % (container, object)
666         headers = {}
667         l = ['etag', 'content_type', 'content_encoding', 'content_disposition', 
668              'x_object_sharing', 'x_object_manifest', 'x_object_public']
669         l = [elem for elem in l if eval(elem)]
670         for elem in l:
671             headers.update({elem:eval(elem)})
672         
673         for k,v in meta.items():
674             headers['x-object-meta-%s' %k.strip()] = v.strip()
675         
676         return self._chunked_transfer(path, 'PUT', f, headers=headers,
677                                       blocksize=blocksize)
678     
679     def create_object_by_hashmap(container, object, f=stdin, format='json',
680                                  meta={}, etag=None, content_encoding=None,
681                                  content_disposition=None, content_type=None,
682                                  x_object_sharing=None, x_object_manifest=None,
683                                  x_object_public = None):
684         """creates an object by uploading hashes representing data instead of data"""
685         args = locals()
686         for elem in ['self', 'container', 'object']:
687             args.pop(elem)
688             
689         data = f.read() if f else None
690         if data and format == 'json':
691             try:
692                 data = eval(data)
693                 data = json.dumps(data)
694             except SyntaxError:
695                 raise Fault('Invalid formatting')
696         
697         #TODO check with xml
698         return self.create_object(container, object, **args)
699     
700     def create_manifestation(self, container, object, manifest):
701         """creates a manifestation"""
702         headers={'x_object_manifest':manifest}
703         return self.create_object(container, object, f=None, **headers)
704     
705     def update_object(self, container, object, f=stdin, offset=None, meta={},
706                       content_length=None, content_type=None, content_range=None,
707                       content_encoding=None, content_disposition=None,
708                       x_object_bytes=None, x_object_manifest=None,
709                       x_object_sharing=None, x_object_public=None):
710         """updates an object"""
711         spath = '/%s/%s' % (container, object)
712         args = locals()
713         for elem in ['self', 'container', 'object']:
714             args.pop(elem)
715         
716         return OOS_Client.update_object(self, container, object, **args)
717         
718     def update_object_using_chunks(self, container, object, f=stdin,
719                                     blocksize=1024, offset=None, meta={},
720                                     content_type=None, content_encoding=None,
721                                     content_disposition=None, x_object_bytes=None,
722                                     x_object_manifest=None, x_object_sharing=None,
723                                     x_object_public=None):
724         """updates an object (incremental upload)"""
725         path = '/%s/%s' % (container, object)
726         headers = {}
727         l = ['content_type', 'content_encoding', 'content_disposition',
728              'x_object_bytes', 'x_object_manifest', 'x_object_sharing',
729              'x_object_public']
730         l = [elem for elem in l if eval(elem)]
731         for elem in l:
732             headers.update({elem:eval(elem)})
733         
734         if offset != None:
735             headers['content_range'] = 'bytes %s-/*' % offset
736         else:
737             headers['content_range'] = 'bytes */*'
738         
739         for k,v in meta.items():
740             headers['x-object-meta-%s' %k.strip()] = v.strip()
741         
742         return self._chunked_transfer(path, 'POST', f, headers=headers,
743                                       blocksize=blocksize)
744     
745     def delete_object(self, container, object, until=None):
746         """deletes an object or the object history until the date provided"""
747         params = {'until':until} if until else {}
748         return OOS_Client.delete_object(self, container, object, params)
749     
750     def trash_object(self, container, object):
751         """trashes an object"""
752         path = '/%s/%s' % (container, object)
753         meta = {'trash':'true'}
754         return self._update_metadata(path, 'object', **meta)
755     
756     def restore_object(self, container, object):
757         """restores a trashed object"""
758         return self.delete_object_metadata(container, object, ['trash'])
759     
760     def _set_public_header(self, headers, public=False):
761         """sets the public header"""
762         if not headers:
763             headers = {}
764         if public == None:
765             return
766         else:
767             headers['x-object-public'] = public if public else ''
768     
769     def publish_object(self, container, object):
770         """sets a previously created object publicly accessible"""
771         path = '/%s/%s' % (container, object)
772         headers = {'content_range':'bytes */*'}
773         self._set_public_header(headers, public=True)
774         return self.post(path, headers=headers)
775     
776     def unpublish_object(self, container, object):
777         """unpublish an object"""
778         path = '/%s/%s' % (container, object)
779         headers = {'content_range':'bytes */*'}
780         self._set_public_header(headers, public=False)
781         return self.post(path, headers=headers)