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