Added objectcopy/move/post _ minor bugfixes
[kamaki] / kamaki / clients / pithos.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 import hashlib
35 import os
36
37 from time import time
38
39 from .storage import StorageClient, ClientError
40 from .utils import path4url, params4url, prefix_keys, filter_in, filter_out, list2str
41
42
43 def pithos_hash(block, blockhash):
44     h = hashlib.new(blockhash)
45     h.update(block.rstrip('\x00'))
46     return h.hexdigest()
47
48 class PithosClient(StorageClient):
49     """GRNet Pithos API client"""
50
51     def account_head(self, until = None,
52         if_modified_since=None, if_unmodified_since=None, *args, **kwargs):
53         """ Full Pithos+ HEAD at account level
54         --- request parameters ---
55         @param until (string): optional timestamp
56         --- --- optional request headers ---
57         @param if_modified_since (string): Retrieve if account has changed since provided timestamp
58         @param if_unmodified_since (string): Retrieve if account has not changed since provided timestamp
59         """
60         self.assert_account()
61         path = path4url(self.account)
62
63         path += '' if until is None else params4url({'until':until})
64         self.set_header('If-Modified-Since', if_modified_since)
65         self.set_header('If-Unmodified-Since', if_unmodified_since)
66
67         success = kwargs.pop('success', 204)
68         return self.head(path, *args, success=success, **kwargs)
69
70     def account_get(self, limit=None, marker=None, format='json', show_only_shared=False, until=None,
71         if_modified_since=None, if_unmodified_since=None, *args, **kwargs):
72         """  Full Pithos+ GET at account level
73         --- request parameters ---
74         @param limit (integer): The amount of results requested (server will use default value if None)
75         @param marker (string): Return containers with name lexicographically after marker
76         @param format (string): reply format can be json or xml (default: json)
77         @param shared (bool): If true, only shared containers will be included in results
78         @param until (string): optional timestamp
79         --- --- optional request headers ---
80         @param if_modified_since (string): Retrieve if account has changed since provided timestamp
81         @param if_unmodified_since (string): Retrieve if account has not changed since provided timestamp
82         """
83         self.assert_account()
84
85         param_dict = {} if format is None else dict(format=format)
86         if limit is not None:
87             param_dict['limit'] = limit
88         if marker is not None:
89             param_dict['marker'] = marker
90         if show_only_shared:
91             param_dict['shared'] = None
92         if until is not None:
93             param_dict['until'] = until
94
95         path = path4url(self.account)+params4url(param_dict)
96         self.set_header('If-Modified-Since', if_modified_since)
97         self.set_header('If-Unmodified-Since', if_unmodified_since)
98
99         success = kwargs.pop('success', (200, 204))
100         return self.get(path, *args, success = success, **kwargs)
101
102     def account_post(self, update=True,
103         groups={}, metadata={}, quota=None, versioning=None, *args, **kwargs):
104         """ Full Pithos+ POST at account level
105         --- request parameters ---
106         @param update (bool): if True, Do not replace metadata/groups
107         --- request headers ---
108         @groups (dict): Optional user defined groups in the form
109                     {   'group1':['user1', 'user2', ...], 
110                         'group2':['userA', 'userB', ...], ...
111                     }
112         @metadata (dict): Optional user defined metadata in the form
113                     {   'name1': 'value1',
114                         'name2': 'value2', ...
115                     }
116         @param quota(integer): If supported, sets the Account quota
117         @param versioning(string): If supported, sets the Account versioning
118                     to 'auto' or some other supported versioning string
119         """
120         self.assert_account()
121         path = path4url(self.account) + params4url({'update':None}) if update else ''
122         for group, usernames in groups.items():
123             userstr = ''
124             dlm = ''
125             for user in usernames:
126                 userstr = userstr + dlm + user
127                 dlm = ','
128             self.set_header('X-Account-Group-'+group, userstr)
129         for metaname, metaval in metadata.items():
130             self.set_header('X-Account-Meta-'+metaname, metaval)
131         self.set_header('X-Account-Policy-Quota', quota)
132         self.set_header('X-Account-Policy-Versioning', versioning)
133
134         success = kwargs.pop('success', 202)
135         return self.post(path, *args, success=success, **kwargs)
136
137     def container_head(self, until=None,
138         if_modified_since=None, if_unmodified_since=None, *args, **kwargs):
139         """ Full Pithos+ HEAD at container level
140         --- request params ---
141         @param until (string): optional timestamp
142         --- optional request headers --- 
143         @param if_modified_since (string): Retrieve if account has changed since provided timestamp
144         @param if_unmodified_since (string): Retrieve if account has not changed since provided timestamp
145         """
146         self.assert_container()
147         path = path4url(self.account, self.container) + '' if until is None else params4url(dict(until=until))
148         self.set_header('If-Modified-Since', if_modified_since)
149         self.set_header('If-Unmodified-Since', if_unmodified_since)
150         success = kwargs.pop('success', 204)
151         return self.head(path, *args, success=success, **kwargs)
152
153     def container_get(self, limit = None, marker = None, prefix=None, delimiter=None, path = None, format='json', meta=[], show_only_shared=False, until=None,
154         if_modified_since=None, if_unmodified_since=None, *args, **kwargs):
155         """ Full Pithos+ GET at container level
156         --- request parameters ---
157         @param limit (integer): The amount of results requested (server qill use default value if None)
158         @param marker (string): Return containers with name lexicographically after marker
159         @param prefix (string): Return objects starting with prefix
160         @param delimiter (string): Return objects up to the delimiter
161         @param path (string): assume prefix = path and delimiter = / (overwrites prefix
162         and delimiter)
163         @param format (string): reply format can be json or xml (default: json)
164         @param meta (list): Return objects that satisfy the key queries in the specified
165         comma separated list (use <key>, !<key> for existence queries, <key><op><value>
166         for value queries, where <op> can be one of =, !=, <=, >=, <, >)
167         @param shared (bool): If true, only shared containers will be included in results
168         @param until (string): optional timestamp
169         --- --- optional request headers ---
170         @param if_modified_since (string): Retrieve if account has changed since provided timestamp
171         @param if_unmodified_since (string): Retrieve if account has not changed since provided timestamp
172         """
173         self.assert_container()
174
175         param_dict = {} if format is None else dict(format=format)
176         if limit is not None:
177             param_dict['limit'] = limit
178         if marker is not None:
179             param_dict['marker'] = marker
180         if path is not None:
181                 param_dict['path'] = path
182         else:
183             if prefix is not None:
184                 param_dict['prefix'] = prefix
185             if delimiter is not None:
186                 param_dict['delimiter'] = delimiter
187         if show_only_shared:
188             param_dict['shared'] = None
189         if meta is not None and len(meta) > 0:
190             param_dict['meta'] = list2str(meta)
191         if until is not None:
192             param_dict['until'] = until
193         path = path4url(self.account, self.container)+params4url(param_dict)
194         self.set_header('If-Modified-Since', if_modified_since)
195         self.set_header('If-Unmodified-Since', if_unmodified_since)
196         success = kwargs.pop('success', 200)
197         return self.get(path, *args, success=success, **kwargs)
198
199     def container_put(self, quota=None, versioning=None, metadata={}, *args, **kwargs):
200         """ Full Pithos+ PUT at container level
201         --- request headers ---
202         @param quota (integer): Size limit in KB
203         @param versioning (string): 'auto' or other string supported by server
204         @metadata (dict): Optional user defined metadata in the form
205         {   'name1': 'value1',
206         'name2': 'value2', ...
207         }
208         """
209         self.assert_container()
210         path = path4url(self.account, self.container)
211         for metaname, metaval in metadata.items():
212             self.set_header('X-Container-Meta-'+metaname, metaval)
213         self.set_header('X-Container-Policy-Quota', quota)
214         self.set_header('X-Container-Policy-Versioning', versioning)
215         success = kwargs.pop('success',(201, 202))
216         return self.put(path, *args, success=success, **kwargs)
217
218     def container_post(self, update=True, format='json',
219         quota=None, versioning=None, metadata={}, content_type=None, content_length=None, transfer_encoding=None,
220         *args, **kwargs):
221         """ Full Pithos+ POST at container level
222         --- request params ---
223         @param update (bool):  if True, Do not replace metadata/groups
224         @param format(string): json (default) or xml
225         --- request headers ---
226         @param quota (integer): Size limit in KB
227         @param versioning (string): 'auto' or other string supported by server
228         @metadata (dict): Optional user defined metadata in the form
229         {   'name1': 'value1',
230         'name2': 'value2', ...
231         }
232         @param content_type (string): set a custom content type
233         @param content_length (string): set a custrom content length
234         @param transfer_encoding (string): set a custrom transfer encoding
235         """
236         self.assert_container()
237         param_dict = {} if format is None else dict(format=format)
238         if update:
239             param_dict['update'] = None
240         path = path4url(self.account, self.container)+params4url(param_dict)
241
242         for metaname, metaval in metadata.items():
243             self.set_header('X-Container-Meta-'+metaname, metaval)
244         self.set_header('X-Container-Policy-Quota', quota)
245         self.set_header('X-Container-Policy-Versioning', versioning)
246         self.set_header('Content-Type', content_type)
247         self.set_header('Content-Length', content_length)
248         self.set_header('Transfer-Encoding', transfer_encoding)
249         success = kwargs.pop('success', 202)
250         return self.post(path, *args, success=success, **kwargs)
251
252     def container_delete(self, until=None, *args, **kwargs):
253         """ Full Pithos+ DELETE at container level
254         --- request parameters ---
255         @param until (timestamp string): if defined, container is purged up to that time
256         """
257         self.assert_container()
258         path=path4url(self.account, self.container)
259         path += '' if until is None else params4url(dict(until=until))
260         success = kwargs.pop('success', 204)
261         return self.delete(path, success=success)
262
263     def object_head(self, object, version=None,
264         if_etag_match=None, if_etag_not_match = None, if_modified_since = None, if_unmodified_since = None, *args, **kwargs):
265         """ Full Pithos+ HEAD at object level
266         --- request parameters ---
267         @param version (string): optional version identified
268         --- request headers ---
269         @param if_etag_match (string): if provided, return only results
270                 with etag matching with this
271         @param if_etag_not_match (string): if provided, return only results
272                 with etag not matching with this
273         @param if_modified_since (string): Retrieve if account has changed since provided timestamp
274         @param if_unmodified_since (string): Retrieve if account has not changed since provided timestamp
275         """
276         self.assert_container()
277         path=path4url(self.account, self.container, object)
278         path += '' if version is None else params4url(dict(version=version))
279         self.set_header('If-Match', if_etag_match)
280         self.set_header('If-None-Match', if_etag_not_match)
281         self.set_header('If-Modified-Since', if_modified_since)
282         self.set_header('If-Unmodified-Since', if_unmodified_since)
283         success = kwargs.pop('success', 200)
284         return self.head(path, *args, success=success, **kwargs)
285
286     def object_get(self, object, format='json', hashmap=False, version=None,
287         data_range=None, if_range=False, if_etag_match=None, if_etag_not_match = None, if_modified_since = None, if_unmodified_since = None, *args, **kwargs):
288         """ Full Pithos+ GET at object level
289         --- request parameters ---
290         @param format (string): json (default) or xml
291         @param hashmap (bool): Optional request for hashmap
292         @param version (string): optional version identified
293         --- request headers ---
294         @param data_range (string): Optional range of data to retrieve
295         @param if_range (bool): 
296         @param if_etag_match (string): if provided, return only results
297                 with etag matching with this
298         @param if_etag_not_match (string): if provided, return only results
299                 with etag not matching with this
300         @param if_modified_since (string): Retrieve if account has changed since provided timestamp
301         @param if_unmodified_since (string): Retrieve if account has not changed since provided timestamp
302         """
303         self.assert_container()
304         param_dict = {} if format is None else dict(format=format)
305         if hashmap:
306             param_dict['hashmap']=None
307         if version is not None:
308             param_dict['version']=version
309         path=path4url(self.account, self.container, object)+params4url(param_dict)
310         self.set_header('Range', data_range)
311         self.set_header('If-Range', '', if_range is True and data_range is not None)
312         self.set_header('If-Match', if_etag_match, )
313         self.set_header('If-None-Match', if_etag_not_match)
314         self.set_header('If-Modified-Since', if_modified_since)
315         self.set_header('If-Unmodified-Since', if_unmodified_since)
316         success = kwargs.pop('success', 200)
317         return self.get(path, *args, success=success, **kwargs)
318
319     def object_put(self, object, format='json', hashmap=False,
320         if_etag_match=None, if_etag_not_match = None, etag=None, content_length = None, content_type=None, transfer_encoding=None,
321         copy_from=None, move_from=None, source_account=None, source_version=None, content_encoding = None, content_disposition=None,
322         manifest = None, permitions = {}, public = None, metadata={}, *args, **kwargs):
323         """ Full Pithos+ PUT at object level
324         --- request parameters ---
325         @param format (string): json (default) or xml
326         @param hashmap (bool): Optional hashmap provided instead of data
327         --- request headers ---
328         @param if_etag_match (string): if provided, return only results
329                 with etag matching with this
330         @param if_etag_not_match (string): if provided, return only results
331                 with etag not matching with this
332         @param etag (string): The MD5 hash of the object (optional to check written data)
333         @param content_length (integer): The size of the data written
334         @param content_type (string): The MIME content type of the object
335         @param transfer_encoding (string): Set to chunked to specify incremental uploading (if used, Content-Length is ignored)
336         @param copy_from (string): The source path in the form /<container>/<object>
337         @param move_from (string): The source path in the form /<container>/<object>
338         @param source_account (string): The source account to copy/move from
339         @param source_version (string): The source version to copy from
340         @param conent_encoding (string): The encoding of the object
341         @param content_disposition (string): The presentation style of the object
342         @param manifest (string): Object parts prefix in /<container>/<object> form
343         @param permitions (dict): Object permissions in the form (all fields are optional)
344                 {'read':[user1, group1, user2, ...], 'write':['user3, group2, group3, ...]}
345         @param public (bool): If true, Object is publicly accessible, if false, not
346         @param metadata (dict): Optional user defined metadata in the form
347                 {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
348         """
349         self.assert_container()
350         param_dict = {} if format is None else dict(format=format)
351         if hashmap:
352             param_dict['hashmap'] = None
353         path=path4url(self.account, self.container, object)+params4url(param_dict)
354         self.set_header('If-Match', if_etag_match)
355         self.set_header('If-None-Match', if_etag_not_match)
356         self.set_header('ETag', etag)
357         self.set_header('Content-Length', content_length)
358         self.set_header('Content-Type', content_type)
359         self.set_header('Transfer-Encoding', transfer_encoding)
360         self.set_header('X-Copy-From', copy_from)
361         self.set_header('X-Move-From', move_from)
362         self.set_header('X-Source-Account', source_account)
363         self.set_header('X-Source-Version', source_version)
364         self.set_header('Content-Encoding', content_encoding)
365         self.set_header('Content-Disposition', content_disposition)
366         self.set_header('X-Object-Manifest', manifest)
367         perms = None
368         for permition_type, permition_list in permitions.items():
369             if perms is None:
370                 perms = '' #Remove permitions
371             if len(permition_list) == 0:
372                 continue
373             perms += ';'+permition_type if len(perms) > 0 else permition_type
374             perms += '='+list2str(permition_list, seperator=',')
375         self.set_header('X-Object-Sharing', perms)
376         self.set_header('X-Object-Public', public)
377         for key, val in metadata.items():
378             self.set_header('X-Object-Meta-'+key, val)
379
380         success = kwargs.pop('success', 201)
381         return self.put(path, *args, success=success, **kwargs)
382
383     def object_copy(self, object, destination, format='json', ignore_content_type=False,
384         if_etag_match=None, if_etag_not_match=None, destination_account=None,
385         content_type=None, content_encoding=None, content_disposition=None, source_version=None,
386         manifest=None, permitions={}, public=False, metadata={}, *args, **kwargs):
387         """ Full Pithos+ COPY at object level
388         --- request parameters ---
389         @param format (string): json (default) or xml
390         @param ignore_content_type (bool): Ignore the supplied Content-Type
391         --- request headers ---
392          @param if_etag_match (string): if provided, copy only results
393                 with etag matching with this
394         @param if_etag_not_match (string): if provided, copy only results
395                 with etag not matching with this
396         @param destination (string): The destination path in the form /<container>/<object>
397         @param destination_account (string): The destination account to copy to
398         @param content_type (string): The MIME content type of the object
399         @param content_encoding (string): The encoding of the object
400         @param content_disposition (string): The presentation style of the object
401         @param source_version (string): The source version to copy from
402         @param manifest (string): Object parts prefix in /<container>/<object> form
403         @param permitions (dict): Object permissions in the form (all fields are optional)
404                 {'read':[user1, group1, user2, ...], 'write':['user3, group2, group3, ...]}
405                 permitions override source permitions, removing any old permitions
406         @param public (bool): If true, Object is publicly accessible, if else, not
407         @param metadata (dict): Optional user defined metadata in the form
408                 {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
409                 Metadata are appended to the source metadata. In case of same keys, they
410                 replace the old metadata
411         """
412         self.assert_container()
413         param_dict = {} if format is None else dict(format=format)
414         if ignore_content_type:
415             param_dict['ignore_content_type'] = None
416         path = path4url(self.account, self.container, object)+params4url(param_dict)
417         self.set_header('If-Match', if_etag_match)
418         self.set_header('If-None-Match', if_etag_not_match)
419         self.set_header('Destination', destination)
420         self.set_header('Destination-Account', destination_account)
421         self.set_header('Content-Type', content_type)
422         self.set_header('Content-Encoding', content_encoding)
423         self.set_header('Content-Disposition', content_disposition)
424         self.set_header('X-Source-Version', source_version)
425         self.set_header('X-Object-Manifest', manifest)
426         perms = None
427         for permition_type, permition_list in permitions.items():
428             if perms is None:
429                 perms = '' #Remove permitions
430             if len(permition_list) == 0:
431                 continue
432             perms += ';'+permition_type if len(perms) > 0 else permition_type
433             perms += '='+list2str(permition_list, seperator=',')
434         self.set_header('X-Object-Sharing', perms)
435         self.set_header('X-Object-Public', public)
436         for key, val in metadata.items():
437             self.set_header('X-Object-Meta-'+key, val)
438         success = kwargs.pop('success', 201)
439         return self.copy(path, *args, success=success, **kwargs)
440
441     def object_move(self, object, format='json', ignore_content_type=False,
442         if_etag_match=None, if_etag_not_match=None, destination=None, destination_account=None,
443         content_type=None, content_encoding=None, content_disposition=None, manifest=None,
444         permitions={}, public=False, metadata={}, *args, **kwargs):
445         """ Full Pithos+ COPY at object level
446         --- request parameters ---
447         @param format (string): json (default) or xml
448         @param ignore_content_type (bool): Ignore the supplied Content-Type
449         --- request headers ---
450          @param if_etag_match (string): if provided, return only results
451                 with etag matching with this
452         @param if_etag_not_match (string): if provided, return only results
453                 with etag not matching with this
454         @param destination (string): The destination path in the form /<container>/<object>
455         @param destination_account (string): The destination account to copy to
456         @param content_type (string): The MIME content type of the object
457         @param content_encoding (string): The encoding of the object
458         @param content_disposition (string): The presentation style of the object
459         @param source_version (string): The source version to copy from
460         @param manifest (string): Object parts prefix in /<container>/<object> form
461         @param permitions (dict): Object permissions in the form (all fields are optional)
462                 {'read':[user1, group1, user2, ...], 'write':['user3, group2, group3, ...]}
463         @param public (bool): If true, Object is publicly accessible, if false, not
464         @param metadata (dict): Optional user defined metadata in the form
465                 {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
466         """
467         self.assert_container()
468         param_dict = {} if format is None else dict(format=format)
469         if ignore_content_type:
470             param_dict['ignore_content_type']=None
471         path = path4url(self.account, self.container, object)+params4url(param_dict)
472         self.set_header('If-Match', if_etag_match)
473         self.set_header('If-None-Match', if_etag_not_match)
474         self.set_header('Destination', destination)
475         self.set_header('Destination-Account', destination_account)
476         self.set_header('Content-Type', content_type)
477         self.set_header('Content-Encoding', content_encoding)
478         self.set_header('Content-Disposition', content_disposition)
479         self.set_header('X-Object-Manifest', manifest)
480         perms = None
481         for permition_type, permition_list in permitions.items():
482             if perms is None:
483                 perms = '' #Remove permitions
484             if len(permition_list) == 0:
485                 continue
486             perms += ';'+permition_type if len(perms) > 0 else permition_type
487             perms += '='+list2str(permition_list, seperator=',')
488         self.set_header('X-Object-Sharing', perms)
489         self.set_header('X-Object-Public', public)
490         for key, val in metadata.items():
491             self.set_header('X-Object-Meta-'+key, val)
492         success = kwargs.pop('success', 201)
493         return self.move(path, *args, success=success, **kwargs)
494
495     def object_post(self, object, format='json', update=True,
496         if_etag_match=None, if_etag_not_match=None, content_length=None, content_type=None,
497         content_range=None, transfer_encoding=None, content_encoding=None, content_disposition=None,
498         source_object=None, source_account=None, source_version=None, object_bytes=None,
499         manifest=None, permitions={}, public=False, metadata={}, *args, **kwargs):
500         """ Full Pithos+ POST at object level
501         --- request parameters ---
502         @param format (string): json (default) or xml
503         @param update (bool): Do not replace metadata
504         --- request headers ---
505         @param if_etag_match (string): if provided, return only results
506                 with etag matching with this
507         @param if_etag_not_match (string): if provided, return only results
508                 with etag not matching with this
509         @param content_length (string): The size of the data written
510         @param content_type (string): The MIME content type of the object
511         @param content_range (string): The range of data supplied
512         @param transfer_encoding (string): Set to chunked to specify incremental uploading
513                 (if used, Content-Length is ignored)
514         @param content_encoding (string): The encoding of the object
515         @param content_disposition (string): The presentation style of the object
516         @param source_object (string): Update with data from the object at path /<container>/<object>
517         @param source_account (string): The source account to update from
518         @param source_version (string): The source version to copy from
519         @param object_bytes (integer): The updated objects final size
520         @param manifest (string): Object parts prefix in /<container>/<object> form
521         @param permitions (dict): Object permissions in the form (all fields are optional)
522                 {'read':[user1, group1, user2, ...], 'write':['user3, group2, group3, ...]}
523         @param public (bool): If true, Object is publicly accessible, if false, not
524         @param metadata (dict): Optional user defined metadata in the form
525                 {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
526         """
527         self.assert_container()
528         param_dict = {} if format is None else dict(format=format)
529         if update:
530             param_dict['update'] = None
531         path = path4url(self.account, self.container, object)+params4url(param_dict)
532         self.set_header('If-Match', if_etag_match)
533         self.set_header('If-None-Match', if_etag_not_match)
534         self.set_header('Content-Length', content_length, iff=transfer_encoding is None)
535         self.set_header('Content-Type', content_type)
536         self.set_header('Content-Range', content_range)
537         self.set_header('Transfer-Encoding', transfer_encoding)
538         self.set_header('Content-Encoding', content_encoding)
539         self.set_header('Content-Disposition', content_disposition)
540         self.set_header('X-Source-Object', source_object)
541         self.set_header('X-Source-Account', source_account)
542         self.set_header('X-Source-Version', source_version)
543         self.set_header('X-Object-Bytes', object_bytes)
544         self.set_header('X-Object-Manifest', manifest)
545         perms = None
546         for permition_type, permition_list in permitions.items():
547             if perms is None:
548                 perms = '' #Remove permitions
549             if len(permition_list) == 0:
550                 continue
551             perms += ';'+permition_type if len(perms) > 0 else permition_type
552             perms += '='+list2str(permition_list, seperator=',')
553         self.set_header('X-Object-Sharing', perms)
554         self.set_header('X-Object-Public', public)
555         for key, val in metadata.items():
556             self.set_header('X-Object-Meta-'+key, val)
557         success=kwargs.pop('success', (202, 204))
558         return self.post(path, *args, success=success, **kwargs)
559        
560     def object_delete(self, object, until=None, *args, **kwargs):
561         """ Full Pithos+ DELETE at object level
562         --- request parameters --- 
563         @param until (string): Optional timestamp
564         """
565         self.assert_container()
566         path = path4url(self.account, self.container, object)
567         path += '' if until is None else params4url(dict(until=until))
568         success = kwargs.pop('success', 204)
569         self.delete(path, *args, success=success, **kwargs)
570
571     def purge_container(self):
572         self.container_delete(until=unicode(time()))
573
574     def put_block(self, data, hash):
575         r = self.container_post(update=True, content_type='application/octet-stream',
576             content_length=len(data), data=data)
577         assert r.text.strip() == hash, 'Local hash does not match server'
578
579     def create_object(self, object, f, size=None, hash_cb=None,
580                       upload_cb=None):
581         """Create an object by uploading only the missing blocks
582         hash_cb is a generator function taking the total number of blocks to
583         be hashed as an argument. Its next() will be called every time a block
584         is hashed.
585         upload_cb is a generator function with the same properties that is
586         called every time a block is uploaded.
587         """
588         self.assert_container()
589
590         meta = self.get_container_info(self.container)
591         blocksize = int(meta['x-container-block-size'])
592         blockhash = meta['x-container-block-hash']
593
594         size = size if size is not None else os.fstat(f.fileno()).st_size
595         nblocks = 1 + (size - 1) // blocksize
596         hashes = []
597         map = {}
598
599         offset = 0
600
601         if hash_cb:
602             hash_gen = hash_cb(nblocks)
603             hash_gen.next()
604
605         for i in range(nblocks):
606             block = f.read(min(blocksize, size - offset))
607             bytes = len(block)
608             hash = pithos_hash(block, blockhash)
609             hashes.append(hash)
610             map[hash] = (offset, bytes)
611             offset += bytes
612             if hash_cb:
613                 hash_gen.next()
614
615         assert offset == size
616
617         hashmap = dict(bytes=size, hashes=hashes)
618         content_type = 'application/octet-stream'
619         r = self.object_put(object, format='json', hashmap=True,
620             content_type=content_type, json=hashmap, success=(201, 409))
621
622         if r.status_code == 201:
623             return
624
625         missing = r.json
626
627         if upload_cb:
628             upload_gen = upload_cb(len(missing))
629             upload_gen.next()
630
631         for hash in missing:
632             offset, bytes = map[hash]
633             f.seek(offset)
634             data = f.read(bytes)
635             self.put_block(data, hash)
636             if upload_cb:
637                 upload_gen.next()
638
639         r = self.object_put(object, format='json', hashmap=True,
640             content_type=content_type, json=hashmap, success=201)
641
642     def set_account_group(self, group, usernames):
643         self.account_post(update=True, groups = {group:usernames})
644
645     def del_account_group(self, group):
646         return self.account_post(update=True, groups={group:[]})
647
648     def get_account_info(self):
649         r = self.account_head()
650         from datetime import datetime
651         r = self.account_head(if_modified_since=datetime.now())
652         if r.status_code == 401:
653             raise ClientError("No authorization")
654         return r.headers
655
656     def get_account_quota(self):
657         return filter_in(self.get_account_info(), 'X-Account-Policy-Quota', exactMatch = True)
658
659     def get_account_versioning(self):
660         return filter_in(self.get_account_info(), 'X-Account-Policy-Versioning', exactMatch = True)
661
662     def get_account_meta(self):
663         return filter_in(self.get_account_info(), 'X-Account-Meta-')
664
665     def get_account_group(self):
666         return filter_in(self.get_account_info(), 'X-Account-Group-')
667
668     def set_account_meta(self, metapairs):
669         assert(type(metapairs) is dict)
670         self.account_post(update=True, metadata=metapairs)
671
672     def del_account_meta(self, metakey):
673         self.account_post(update=True, metadata={metakey:''})
674
675     def set_account_quota(self, quota):
676         self.account_post(update=True, quota=quota)
677
678     def set_account_versioning(self, versioning):
679         self.account_post(update=True, versioning = versioning)
680
681     def list_containers(self):
682         r = self.account_get()
683         return r.json
684
685     def get_container_versioning(self, container):
686         return filter_in(self.get_container_info(container), 'X-Container-Policy-Versioning')
687
688     def get_container_quota(self, container):
689         return filter_in(self.get_container_info(container), 'X-Container-Policy-Quota')
690
691     def get_container_meta(self, container):
692         return filter_in(self.get_container_info(container), 'X-Container-Meta-')
693
694     def get_container_object_meta(self, container):
695         return filter_in(self.get_container_info(container), 'X-Container-Object-Meta')
696
697     def set_container_meta(self, metapairs):
698         assert(type(metapairs) is dict)
699         self.container_post(update=True, metadata=metapairs)
700
701     def del_container_meta(self, metakey):
702         self.container_post(update=True, metadata={metakey:''})
703
704     def set_container_quota(self, quota):
705         self.container_post(update=True, quota=quota)
706
707     def set_container_versioning(self, versioning):
708         self.container_post(update=True, versioning=versioning)
709
710     def set_object_meta(self, object, metapairs):
711         assert(type(metapairs) is dict)
712         self.object_post(object, update=True, metadata=metapairs)
713
714     def del_object_meta(self, metakey, object):
715         self.object_post(object, update=True, metadata={metakey:''})
716
717     def publish_object(self, object):
718         self.object_post(object, update=True, public=True)
719
720     def unpublish_object(self, object):
721         self.object_post(object, update=True, public=False)
722
723     def get_object_sharing(self, object):
724         return filter_in(self.get_object_info(object), 'X-Object-Sharing', exactMatch = True)
725
726     def set_object_sharing(self, object, read_permition = False, write_permition = False):
727         """Give read/write permisions to an object.
728            @param object is the object to change sharing permitions onto
729            @param read_permition is a list of users and user groups that get read permition for this object
730                 False means all previous read permitions will be removed
731            @param write_perimition is a list of users and user groups to get write permition for this object
732                 False means all previous read permitions will be removed
733         """
734         perms = {}
735         perms['read'] = read_permition if isinstance(read_permition, list) else ''
736         perms['write'] = write_permition if isinstance(write_permition, list) else ''
737         self.object_post(object, update=True, permitions=perms)
738
739     def del_object_sharing(self, object):
740         self.set_object_sharing(object)
741
742     def append_object(self, object, source_file, upload_cb = None):
743         """@param upload_db is a generator for showing progress of upload
744             to caller application, e.g. a progress bar. Its next is called
745             whenever a block is uploaded
746         """
747         self.assert_container()
748         meta = self.get_container_info(self.container)
749         blocksize = int(meta['x-container-block-size'])
750         filesize = os.fstat(source_file.fileno()).st_size
751         nblocks = 1 + (filesize - 1)//blocksize
752         offset = 0
753         if upload_cb is not None:
754             upload_gen = upload_cb(nblocks)
755         for i in range(nblocks):
756             block = source_file.read(min(blocksize, filesize - offset))
757             offset += len(block)
758             self.object_post(object, update=True,
759                 content_range='bytes */*', content_type='application/octet-stream',
760                 content_length=len(block), data=block)
761             if upload_cb is not None:
762                 upload_gen.next()
763
764     def truncate_object(self, object, upto_bytes):
765         self.object_post(object, update=True, content_range='bytes 0-%s/*'%upto_bytes,
766             content_type='application/octet-stream', object_bytes=upto_bytes,
767             source_object=path4url(self.container, object))
768
769     def overwrite_object(self, object, start, end, source_file, upload_cb=None):
770         """Overwrite a part of an object with given source file
771            @start the part of the remote object to start overwriting from, in bytes
772            @end the part of the remote object to stop overwriting to, in bytes
773         """
774         self.assert_container()
775         meta = self.get_container_info(self.container)
776         blocksize = int(meta['x-container-block-size'])
777         filesize = os.fstat(source_file.fileno()).st_size
778         datasize = int(end) - int(start) + 1
779         nblocks = 1 + (datasize - 1)//blocksize
780         offset = 0
781         if upload_cb is not None:
782             upload_gen = upload_cb(nblocks)
783         for i in range(nblocks):
784             block = source_file.read(min(blocksize, filesize - offset, datasize - offset))
785             offset += len(block)
786             self.object_post(object, update=True, content_type='application/octet-stream', 
787                 content_length=len(block), content_range='bytes %s-%s/*'%(start,end), data=block)
788             if upload_cb is not None:
789                 upload_gen.next()
790