1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
39 from .storage import StorageClient, ClientError
40 from .utils import path4url, params4url, prefix_keys, filter_in, filter_out, list2str
43 def pithos_hash(block, blockhash):
44 h = hashlib.new(blockhash)
45 h.update(block.rstrip('\x00'))
48 class PithosClient(StorageClient):
49 """GRNet Pithos API client"""
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
61 path = path4url(self.account)
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)
67 success = kwargs.pop('success', 204)
68 return self.head(path, *args, success=success, **kwargs)
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
85 param_dict = {} if format is None else dict(format=format)
87 param_dict['limit'] = limit
88 if marker is not None:
89 param_dict['marker'] = marker
91 param_dict['shared'] = None
93 param_dict['until'] = until
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)
99 success = kwargs.pop('success', (200, 204))
100 return self.get(path, *args, success = success, **kwargs)
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', ...], ...
112 @metadata (dict): Optional user defined metadata in the form
114 'name2': 'value2', ...
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
120 self.assert_account()
121 path = path4url(self.account) + params4url({'update':None}) if update else ''
122 for group, usernames in groups.items():
125 for user in usernames:
126 userstr = userstr + dlm + user
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)
134 success = kwargs.pop('success', 202)
135 return self.post(path, *args, success=success, **kwargs)
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
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)
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
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
173 self.assert_container()
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
181 param_dict['path'] = path
183 if prefix is not None:
184 param_dict['prefix'] = prefix
185 if delimiter is not None:
186 param_dict['delimiter'] = delimiter
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)
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
206 'name2': 'value2', ...
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)
218 def container_post(self, update=True, format='json',
219 quota=None, versioning=None, metadata={}, content_type=None, content_length=None, transfer_encoding=None,
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
230 'name2': 'value2', ...
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
236 self.assert_container()
237 param_dict = {} if format is None else dict(format=format)
239 param_dict['update'] = None
240 path = path4url(self.account, self.container)+params4url(param_dict)
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)
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
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)
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
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)
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
303 self.assert_container()
304 param_dict = {} if format is None else dict(format=format)
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)
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', ...}
349 self.assert_container()
350 param_dict = {} if format is None else dict(format=format)
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)
368 for permition_type, permition_list in permitions.items():
370 perms = '' #Remove permitions
371 if len(permition_list) == 0:
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)
380 success = kwargs.pop('success', 201)
381 return self.put(path, *args, success=success, **kwargs)
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
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)
427 for permition_type, permition_list in permitions.items():
429 perms = '' #Remove permitions
430 if len(permition_list) == 0:
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)
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', ...}
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)
481 for permition_type, permition_list in permitions.items():
483 perms = '' #Remove permitions
484 if len(permition_list) == 0:
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)
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', ...}
527 self.assert_container()
528 param_dict = {} if format is None else dict(format=format)
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)
546 for permition_type, permition_list in permitions.items():
548 perms = '' #Remove permitions
549 if len(permition_list) == 0:
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)
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
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)
571 def purge_container(self):
572 self.container_delete(until=unicode(time()))
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'
579 def create_object(self, object, f, size=None, hash_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
585 upload_cb is a generator function with the same properties that is
586 called every time a block is uploaded.
588 self.assert_container()
590 meta = self.get_container_info(self.container)
591 blocksize = int(meta['x-container-block-size'])
592 blockhash = meta['x-container-block-hash']
594 size = size if size is not None else os.fstat(f.fileno()).st_size
595 nblocks = 1 + (size - 1) // blocksize
602 hash_gen = hash_cb(nblocks)
605 for i in range(nblocks):
606 block = f.read(min(blocksize, size - offset))
608 hash = pithos_hash(block, blockhash)
610 map[hash] = (offset, bytes)
615 assert offset == size
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))
622 if r.status_code == 201:
628 upload_gen = upload_cb(len(missing))
632 offset, bytes = map[hash]
635 self.put_block(data, hash)
639 r = self.object_put(object, format='json', hashmap=True,
640 content_type=content_type, json=hashmap, success=201)
642 def set_account_group(self, group, usernames):
643 self.account_post(update=True, groups = {group:usernames})
645 def del_account_group(self, group):
646 return self.account_post(update=True, groups={group:[]})
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")
656 def get_account_quota(self):
657 return filter_in(self.get_account_info(), 'X-Account-Policy-Quota', exactMatch = True)
659 def get_account_versioning(self):
660 return filter_in(self.get_account_info(), 'X-Account-Policy-Versioning', exactMatch = True)
662 def get_account_meta(self):
663 return filter_in(self.get_account_info(), 'X-Account-Meta-')
665 def get_account_group(self):
666 return filter_in(self.get_account_info(), 'X-Account-Group-')
668 def set_account_meta(self, metapairs):
669 assert(type(metapairs) is dict)
670 self.account_post(update=True, metadata=metapairs)
672 def del_account_meta(self, metakey):
673 self.account_post(update=True, metadata={metakey:''})
675 def set_account_quota(self, quota):
676 self.account_post(update=True, quota=quota)
678 def set_account_versioning(self, versioning):
679 self.account_post(update=True, versioning = versioning)
681 def list_containers(self):
682 r = self.account_get()
685 def get_container_versioning(self, container):
686 return filter_in(self.get_container_info(container), 'X-Container-Policy-Versioning')
688 def get_container_quota(self, container):
689 return filter_in(self.get_container_info(container), 'X-Container-Policy-Quota')
691 def get_container_meta(self, container):
692 return filter_in(self.get_container_info(container), 'X-Container-Meta-')
694 def get_container_object_meta(self, container):
695 return filter_in(self.get_container_info(container), 'X-Container-Object-Meta')
697 def set_container_meta(self, metapairs):
698 assert(type(metapairs) is dict)
699 self.container_post(update=True, metadata=metapairs)
701 def del_container_meta(self, metakey):
702 self.container_post(update=True, metadata={metakey:''})
704 def set_container_quota(self, quota):
705 self.container_post(update=True, quota=quota)
707 def set_container_versioning(self, versioning):
708 self.container_post(update=True, versioning=versioning)
710 def set_object_meta(self, object, metapairs):
711 assert(type(metapairs) is dict)
712 self.object_post(object, update=True, metadata=metapairs)
714 def del_object_meta(self, metakey, object):
715 self.object_post(object, update=True, metadata={metakey:''})
717 def publish_object(self, object):
718 self.object_post(object, update=True, public=True)
720 def unpublish_object(self, object):
721 self.object_post(object, update=True, public=False)
723 def get_object_sharing(self, object):
724 return filter_in(self.get_object_info(object), 'X-Object-Sharing', exactMatch = True)
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
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)
739 def del_object_sharing(self, object):
740 self.set_object_sharing(object)
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
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
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))
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:
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))
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
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
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))
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: