rest_api.py 35.2 KB
Newer Older
1
# Copyright 2012-2014 GRNET S.A. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

34
from kamaki.clients.storage import StorageClient
35
from kamaki.clients.utils import path4url
36

37

38
class PithosRestClient(StorageClient):
39
    service_type = 'object-store'
40

41 42 43 44 45 46
    def account_head(
            self,
            until=None,
            if_modified_since=None,
            if_unmodified_since=None,
            *args, **kwargs):
47
        """ Full Pithos+ HEAD at account level
48

49
        --- request parameters ---
50 51 52 53 54 55 56 57 58 59 60 61

        :param until: (string) optional timestamp

        --- request headers ---

        :param if_modified_since: (string) Retrieve if account has changed
            since provided timestamp

        :param if_unmodified_since: (string) Retrieve if account has not
            change since provided timestamp

        :returns: ConnectionResponse
62
        """
63 64
        self.response_headers = ['Last-Modified', ]
        self.response_header_prefices = ['X-Account-', ]
65

66
        self._assert_account()
67 68
        path = path4url(self.account)

69
        self.set_param('until', until, iff=until)
70 71 72 73
        self.set_header('If-Modified-Since', if_modified_since)
        self.set_header('If-Unmodified-Since', if_unmodified_since)

        success = kwargs.pop('success', 204)
74 75 76 77 78
        r = self.head(path, *args, success=success, **kwargs)
        self._unquote_header_keys(
            r.headers,
            ('x-account-group-', 'x-account-policy-', 'x-account-meta-'))
        return r
79

80 81 82 83 84 85
    def account_get(
            self,
            limit=None,
            marker=None,
            format='json',
            show_only_shared=False,
86
            public=False,
87 88 89 90
            until=None,
            if_modified_since=None,
            if_unmodified_since=None,
            *args, **kwargs):
91
        """  Full Pithos+ GET at account level
92

93
        --- request parameters ---
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

        :param limit: (integer) The amount of results requested
            (server will use default value if None)

        :param marker: string Return containers with name
            lexicographically after marker

        :param format: (string) reply format can be json or xml
            (default: json)

        :param shared: (bool) If true, only shared containers will be
            included in results

        :param until: (string) optional timestamp

        --- request headers ---

        :param if_modified_since: (string) Retrieve if account has changed
            since provided timestamp

        :param if_unmodified_since: (string) Retrieve if account has not
            changed since provided timestamp

        :returns: ConnectionResponse
118
        """
119
        self._assert_account()
120 121
        self.response_headers = ['Last-Modified', ]
        self.response_header_prefices = ['X-Account-', ]
122

123 124
        self.set_param('limit', limit, iff=limit)
        self.set_param('marker', marker, iff=marker)
125
        self.set_param('format', format, iff=format)
126
        self.set_param('shared', iff=show_only_shared)
127
        self.set_param('public', iff=public)
128
        self.set_param('until', until, iff=until)
129 130 131 132 133 134

        self.set_header('If-Modified-Since', if_modified_since)
        self.set_header('If-Unmodified-Since', if_unmodified_since)

        path = path4url(self.account)
        success = kwargs.pop('success', (200, 204))
135
        return self.get(path, *args, success=success, **kwargs)
136

137 138 139 140 141 142 143 144
    def account_post(
            self,
            update=True,
            groups={},
            metadata=None,
            quota=None,
            versioning=None,
            *args, **kwargs):
145
        """ Full Pithos+ POST at account level
146

147
        --- request parameters ---
148 149 150

        :param update: (bool) if True, Do not replace metadata/groups

151
        --- request headers ---
152 153 154 155 156 157 158 159 160 161 162 163 164 165

        :param groups: (dict) Optional user defined groups in the form
            { 'group1':['user1', 'user2', ...],
            'group2':['userA', 'userB', ...], }

        :param metadata: (dict) Optional user defined metadata in the form
            { 'name1': 'value1', 'name2': 'value2', ... }

        :param quota: (integer) If supported, sets the Account quota

        :param versioning: (string) If supported, sets the Account versioning
            to 'auto' or some other supported versioning string

        :returns: ConnectionResponse
166
        """
167
        self._assert_account()
168

169
        self.set_param('update', '', iff=update)
170 171
        self.request_header_prefices_to_quote = [
            'x-account-meta-', 'x-account-group-']
172

173 174 175 176 177 178 179 180
        if groups:
            for group, usernames in groups.items():
                userstr = ''
                dlm = ''
                for user in usernames:
                    userstr = userstr + dlm + user
                    dlm = ','
                self.set_header('X-Account-Group-' + group, userstr)
181
        if metadata:
182
            for metaname, metaval in metadata.items():
183
                self.set_header('X-Account-Meta-' + metaname, metaval)
184 185
        self.set_header('X-Account-Policy-Quota', quota)
        self.set_header('X-Account-Policy-Versioning', versioning)
186
        self._quote_header_keys(
187
            self.headers, ('x-account-group-', 'x-account-meta-'))
188 189 190 191 192

        path = path4url(self.account)
        success = kwargs.pop('success', 202)
        return self.post(path, *args, success=success, **kwargs)

193 194 195 196 197 198
    def container_head(
            self,
            until=None,
            if_modified_since=None,
            if_unmodified_since=None,
            *args, **kwargs):
199
        """ Full Pithos+ HEAD at container level
200

201
        --- request params ---
202 203 204 205 206 207 208 209 210 211 212 213

        :param until: (string) optional timestamp

        --- request headers ---

        :param if_modified_since: (string) Retrieve if account has changed
            since provided timestamp

        :param if_unmodified_since: (string) Retrieve if account has not
            changed since provided timestamp

        :returns: ConnectionResponse
214
        """
215
        self._assert_container()
216 217
        self.response_headers = ['Last-Modified', ]
        self.response_header_prefices = ['X-Container-', ]
218

219
        self.set_param('until', until, iff=until)
220 221 222 223 224 225

        self.set_header('If-Modified-Since', if_modified_since)
        self.set_header('If-Unmodified-Since', if_unmodified_since)

        path = path4url(self.account, self.container)
        success = kwargs.pop('success', 204)
226 227 228 229
        r = self.head(path, *args, success=success, **kwargs)
        self._unquote_header_keys(
            r.headers, ('x-container-policy-', 'x-container-meta-'))
        return r
230

231 232 233 234 235 236 237 238 239 240
    def container_get(
            self,
            limit=None,
            marker=None,
            prefix=None,
            delimiter=None,
            path=None,
            format='json',
            meta=[],
            show_only_shared=False,
241
            public=False,
242 243 244 245
            until=None,
            if_modified_since=None,
            if_unmodified_since=None,
            *args, **kwargs):
246
        """ Full Pithos+ GET at container level
247

248
        --- request parameters ---
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269

        :param limit: (integer) The amount of results requested
            (server will use default value if None)

        :param marker: (string) Return containers with name lexicographically
            after marker

        :param prefix: (string) Return objects starting with prefix

        :param delimiter: (string) Return objects up to the delimiter

        :param path: (string) assume prefix = path and delimiter = /
            (overwrites prefix and delimiter)

        :param format: (string) reply format can be json or xml (default:json)

        :param meta: (list) Return objects that satisfy the key queries in
            the specified comma separated list (use <key>, !<key> for
            existence queries, <key><op><value> for value queries, where <op>
            can be one of =, !=, <=, >=, <, >)

270 271
        :param show_only_shared: (bool) If true, only shared containers will
            be included in results
272 273 274 275 276 277 278 279 280 281 282 283

        :param until: (string) optional timestamp

        --- request headers ---

        :param if_modified_since: (string) Retrieve if account has changed
            since provided timestamp

        :param if_unmodified_since: (string) Retrieve if account has not
            changed since provided timestamp

        :returns: ConnectionResponse
284
        """
285

286
        self._assert_container()
287 288
        self.response_headers = ['Last-Modified', ]
        self.response_header_prefices = ['X-Container-', ]
289

290 291 292 293 294
        self.set_param('limit', limit, iff=limit)
        self.set_param('marker', marker, iff=marker)
        if not path:
            self.set_param('prefix', prefix, iff=prefix)
            self.set_param('delimiter', delimiter, iff=delimiter)
295 296
        else:
            self.set_param('path', path)
297
        self.set_param('format', format, iff=format)
298
        self.set_param('shared', iff=show_only_shared)
299
        self.set_param('public', iff=public)
300 301
        if meta:
            self.set_param('meta',  ','.join(meta))
302
        self.set_param('until', until, iff=until)
303 304 305 306 307 308 309 310

        self.set_header('If-Modified-Since', if_modified_since)
        self.set_header('If-Unmodified-Since', if_unmodified_since)

        path = path4url(self.account, self.container)
        success = kwargs.pop('success', 200)
        return self.get(path, *args, success=success, **kwargs)

311 312
    def container_put(
            self,
313
            quota=None, versioning=None, project_id=None, metadata=None,
314
            *args, **kwargs):
315
        """ Full Pithos+ PUT at container level
316

317
        --- request headers ---
318 319 320 321 322 323 324 325 326

        :param quota: (integer) Size limit in KB

        :param versioning: (string) 'auto' or other string supported by server

        :param metadata: (dict) Optional user defined metadata in the form
            { 'name1': 'value1', 'name2': 'value2', ... }

        :returns: ConnectionResponse
327
        """
328
        self._assert_container()
329
        self.request_header_prefices_to_quote = ['x-container-meta-', ]
330

331 332
        self.set_header('X-Container-Policy-Quota', quota)
        self.set_header('X-Container-Policy-Versioning', versioning)
333 334
        if project_id is not None:
            self.set_header('X-Container-Policy-Project', project_id)
335

336
        if metadata:
337
            for metaname, metaval in metadata.items():
338
                self.set_header('X-Container-Meta-' + metaname, metaval)
339 340
        self._quote_header_keys(
            self.headers, ('x-container-policy-', 'x-container-meta-'))
341 342

        path = path4url(self.account, self.container)
343
        success = kwargs.pop('success', (201, 202))
344 345
        return self.put(path, *args, success=success, **kwargs)

346 347 348 349 350 351
    def container_post(
            self,
            update=True,
            format='json',
            quota=None,
            versioning=None,
352
            project_id=None,
353 354 355 356 357
            metadata=None,
            content_type=None,
            content_length=None,
            transfer_encoding=None,
            *args, **kwargs):
358
        """ Full Pithos+ POST at container level
359

360
        --- request params ---
361 362 363 364 365

        :param update: (bool)  if True, Do not replace metadata/groups

        :param format: (string) json (default) or xml

366
        --- request headers ---
367 368 369 370 371 372 373 374 375 376 377 378

        :param quota: (integer) Size limit in KB

        :param versioning: (string) 'auto' or other string supported by server

        :param metadata: (dict) Optional user defined metadata in the form
            { 'name1': 'value1', 'name2': 'value2', ... }

        :param content_type: (string) set a custom content type

        :param content_length: (string) set a custrom content length

379
        :param transfer_encoding: (string) set a custom transfer encoding
380 381

        :returns: ConnectionResponse
382
        """
383
        self._assert_container()
384
        self.request_header_prefices_to_quote = ['x-container-meta-', ]
385

386
        self.set_param('update', '', iff=update)
387
        self.set_param('format', format, iff=format)
388

389 390
        self.set_header('X-Container-Policy-Quota', quota)
        self.set_header('X-Container-Policy-Versioning', versioning)
391 392
        if project_id is not None:
            self.set_header('X-Container-Policy-Project', project_id)
393

394
        if metadata:
395
            for metaname, metaval in metadata.items():
396
                self.set_header('X-Container-Meta-' + metaname, metaval)
397 398 399
        self.set_header('Content-Type', content_type)
        self.set_header('Content-Length', content_length)
        self.set_header('Transfer-Encoding', transfer_encoding)
400 401
        self._quote_header_keys(
            self.headers, ('x-container-policy-', 'x-container-meta-'))
402 403 404 405 406 407 408

        path = path4url(self.account, self.container)
        success = kwargs.pop('success', 202)
        return self.post(path, *args, success=success, **kwargs)

    def container_delete(self, until=None, delimiter=None, *args, **kwargs):
        """ Full Pithos+ DELETE at container level
409

410
        --- request parameters ---
411 412 413 414 415

        :param until: (timestamp string) if defined, container is purged up to
            that time

        :returns: ConnectionResponse
416
        """
417

418
        self._assert_container()
419

420 421
        self.set_param('until', until, iff=until)
        self.set_param('delimiter', delimiter, iff=delimiter)
422

423
        path = path4url(self.account, self.container)
424
        success = kwargs.pop('success', 204)
425
        return self.delete(path, *args, success=success, **kwargs)
426

427
    def object_head(
428
            self, obj,
429 430 431 432 433 434
            version=None,
            if_etag_match=None,
            if_etag_not_match=None,
            if_modified_since=None,
            if_unmodified_since=None,
            *args, **kwargs):
435
        """ Full Pithos+ HEAD at object level
436

437
        --- request parameters ---
438 439 440

        :param version: (string) optional version identified

441
        --- request headers ---
442 443 444 445 446 447 448 449 450 451 452 453 454 455

        :param if_etag_match: (string) if provided, return only results
            with etag matching with this

        :param if_etag_not_match: (string) if provided, return only results
            with etag not matching with this

        :param if_modified_since: (string) Retrieve if account has changed
            since provided timestamp

        :param if_unmodified_since: (string) Retrieve if account has not
            changed since provided timestamp

        :returns: ConnectionResponse
456
        """
457
        self._assert_container()
458 459 460 461 462 463 464 465 466
        self.response_headers = [
            'ETag',
            'Content-Length',
            'Content-Type',
            'Last-Modified',
            'Content-Encoding',
            'Content-Disposition',
        ]
        self.response_header_prefices = ['X-Object-', ]
467

468
        self.set_param('version', version, iff=version)
469 470 471 472 473 474

        self.set_header('If-Match', if_etag_match)
        self.set_header('If-None-Match', if_etag_not_match)
        self.set_header('If-Modified-Since', if_modified_since)
        self.set_header('If-Unmodified-Since', if_unmodified_since)

475
        path = path4url(self.account, self.container, obj)
476
        success = kwargs.pop('success', 200)
477 478 479
        r = self.head(path, *args, success=success, **kwargs)
        self._unquote_header_keys(r.headers, 'x-object-meta-')
        return r
480

481
    def object_get(
482
            self, obj,
483 484 485 486 487 488 489 490 491 492
            format='json',
            hashmap=False,
            version=None,
            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):
493
        """ Full Pithos+ GET at object level
494

495
        --- request parameters ---
496 497 498 499 500 501 502

        :param format: (string) json (default) or xml

        :param hashmap: (bool) Optional request for hashmap

        :param version: (string) optional version identified

503
        --- request headers ---
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521

        :param data_range: (string) Optional range of data to retrieve

        :param if_range: (bool)

        :param if_etag_match: (string) if provided, return only results
            with etag matching with this

        :param if_etag_not_match: (string) if provided, return only results
            with etag not matching with this

        :param if_modified_since: (string) Retrieve if account has changed
            since provided timestamp

        :param if_unmodified_since: (string) Retrieve if account has not
            changed since provided timestamp

        :returns: ConnectionResponse
522
        """
523
        self._assert_container()
524 525 526 527 528 529 530 531 532 533
        self.response_headers = [
            'ETag',
            'Content-Length',
            'Content-Type',
            'Last-Modified',
            'Content-Encoding',
            'Content-Disposition',
            'Content-Range',
        ]
        self.response_header_prefices = ['X-Object-', ]
534

535
        self.set_param('format', format, iff=format)
536
        self.set_param('hashmap', hashmap, iff=hashmap)
537
        self.set_param('version', version, iff=version)
538 539

        self.set_header('Range', data_range)
540
        self.set_header('If-Range', '', if_range and data_range)
541 542 543 544 545
        self.set_header('If-Match', if_etag_match, )
        self.set_header('If-None-Match', if_etag_not_match)
        self.set_header('If-Modified-Since', if_modified_since)
        self.set_header('If-Unmodified-Since', if_unmodified_since)

546
        path = path4url(self.account, self.container, obj)
547
        success = kwargs.pop('success', 200)
548 549 550
        r = self.get(path, *args, success=success, **kwargs)
        self._unquote_header_keys(r.headers, ('x-object-meta-'))
        return r
551

552
    def object_put(
553
            self, obj,
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
            format='json',
            hashmap=False,
            delimiter=None,
            if_etag_match=None,
            if_etag_not_match=None,
            etag=None,
            content_length=None,
            content_type=None,
            transfer_encoding=None,
            copy_from=None,
            move_from=None,
            source_account=None,
            source_version=None,
            content_encoding=None,
            content_disposition=None,
            manifest=None,
            permissions=None,
571
            public=None,
572 573
            metadata=None,
            *args, **kwargs):
574
        """ Full Pithos+ PUT at object level
575

576
        --- request parameters ---
577 578 579 580 581

        :param format: (string) json (default) or xml

        :param hashmap: (bool) Optional hashmap provided instead of data

582
        --- request headers ---
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621

        :param if_etag_match: (string) if provided, return only results
            with etag matching with this

        :param if_etag_not_match: (string) if provided, return only results
            with etag not matching with this

        :param etag: (string) The MD5 hash of the object (optional to check
            written data)

        :param content_length: (integer) The size of the data written

        :param content_type: (string) The MIME content type of the object

        :param transfer_encoding: (string) Set to chunked to specify
            incremental uploading (if used, Content-Length is ignored)

        :param copy_from: (string) The source path in the form
            /<container>/<object>

        :param move_from: (string) The source path in the form
            /<container>/<object>

        :param source_account: (string) The source account to copy/move from

        :param source_version: (string) The source version to copy from

        :param conent_encoding: (string) The encoding of the object

        :param content_disposition: (string) Presentation style of the object

        :param manifest: (string) Object parts prefix in
            /<container>/<object> form

        :param permissions: (dict) Object permissions in the form (all fields
            are optional)
            { 'read':[user1, group1, user2, ...],
            'write':['user3, group2, group3, ...] }

622
        :param public: (bool) If true, Object is published, False, unpublished
623 624 625 626 627

        :param metadata: (dict) Optional user defined metadata in the form
            {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}

        :returns: ConnectionResponse
628
        """
629
        self._assert_container()
630 631 632
        self.response_headers = ['ETag', 'X-Object-Version', ]
        self.request_headers_to_quote = ['x-copy-from', 'x-move-from', ]
        self.request_header_prefices_to_quote = ['x-object-meta-', ]
633

634
        self.set_param('format', format, iff=format)
635
        self.set_param('hashmap', hashmap, iff=hashmap)
636
        self.set_param('delimiter', delimiter, iff=delimiter)
637 638 639 640 641 642 643 644 645 646 647 648 649 650

        self.set_header('If-Match', if_etag_match)
        self.set_header('If-None-Match', if_etag_not_match)
        self.set_header('ETag', etag)
        self.set_header('Content-Length', content_length)
        self.set_header('Content-Type', content_type)
        self.set_header('Transfer-Encoding', transfer_encoding)
        self.set_header('X-Copy-From', copy_from)
        self.set_header('X-Move-From', move_from)
        self.set_header('X-Source-Account', source_account)
        self.set_header('X-Source-Version', source_version)
        self.set_header('Content-Encoding', content_encoding)
        self.set_header('Content-Disposition', content_disposition)
        self.set_header('X-Object-Manifest', manifest)
651
        if permissions:
652 653 654 655 656 657 658 659 660
            perms = None
            if permissions:
                for perm_type, perm_list in permissions.items():
                    if not perms:
                        perms = ''  # Remove permissions
                    if perm_list:
                        perms += ';' if perms else ''
                        perms += '%s=%s' % (perm_type, ','.join(perm_list))
            self.set_header('X-Object-Sharing', perms)
661
        self.set_header('X-Object-Public', public, public is not None)
662
        if metadata:
663
            for key, val in metadata.items():
664
                self.set_header('X-Object-Meta-' + key, val)
665
        self._quote_header_keys(self.headers, ('x-object-meta-', ))
666

667
        path = path4url(self.account, self.container, obj)
668 669 670
        success = kwargs.pop('success', 201)
        return self.put(path, *args, success=success, **kwargs)

671
    def object_copy(
672
            self, obj, destination,
673 674 675 676 677 678 679 680 681 682
            format='json',
            ignore_content_type=False,
            if_etag_match=None,
            if_etag_not_match=None,
            destination_account=None,
            content_type=None,
            content_encoding=None,
            content_disposition=None,
            source_version=None,
            permissions=None,
683
            public=None,
684 685
            metadata=None,
            *args, **kwargs):
686
        """ Full Pithos+ COPY at object level
687

688
        --- request parameters ---
689 690 691 692 693

        :param format: (string) json (default) or xml

        :param ignore_content_type: (bool) Ignore the supplied Content-Type

694
        --- request headers ---
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719

        :param if_etag_match: (string) if provided, copy only results
            with etag matching with this

        :param if_etag_not_match: (string) if provided, copy only results
            with etag not matching with this

        :param destination: (string) The destination path in the form
            /<container>/<object>

        :param destination_account: (string) The destination account to copy to

        :param content_type: (string) The MIME content type of the object

        :param content_encoding: (string) The encoding of the object

        :param content_disposition: (string) Object resentation style

        :param source_version: (string) The source version to copy from

        :param permissions: (dict) Object permissions in the form
            (all fields are optional)
            { 'read':[user1, group1, user2, ...],
            'write':['user3, group2, group3, ...] }

720
        :param public: (bool) If true, Object is published, False, unpublished
721 722 723 724 725 726 727

        :param metadata: (dict) Optional user defined metadata in the form
            {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}
            Metadata are appended to the source metadata. In case of same
            keys, they replace the old metadata

        :returns: ConnectionResponse
728
        """
729
        self._assert_container()
730 731 732 733 734 735 736 737 738 739 740
        self.response_headers = [
            'If-Match',
            'If-None-Match',
            'Destination',
            'Destination-Account',
            'Content-Type',
            'Content-Encoding',
            'Content-Disposition',
            'X-Source-Version',
        ]
        self.response_header_prefices = ['X-Object-', ]
741 742
        self.request_header_prefices_to_quote = [
            'x-object-meta-', 'Destination']
743

744
        self.set_param('format', format, iff=format)
745 746 747 748 749 750 751 752 753 754
        self.set_param('ignore_content_type', iff=ignore_content_type)

        self.set_header('If-Match', if_etag_match)
        self.set_header('If-None-Match', if_etag_not_match)
        self.set_header('Destination', destination)
        self.set_header('Destination-Account', destination_account)
        self.set_header('Content-Type', content_type)
        self.set_header('Content-Encoding', content_encoding)
        self.set_header('Content-Disposition', content_disposition)
        self.set_header('X-Source-Version', source_version)
755
        if permissions:
756 757
            perms = ''
            for perm_type, perm_list in permissions.items():
758
                if not perms:
759
                    perms = ''  # Remove permissions
760 761 762 763
                if perm_list:
                    perms += ';' if perms else ''
                    perms += '%s=%s' % (perm_type, ','.join(perm_list))
            self.set_header('X-Object-Sharing', perms)
764
        self.set_header('X-Object-Public', public, public is not None)
765
        if metadata:
766
            for key, val in metadata.items():
767
                self.set_header('X-Object-Meta-' + key, val)
768
        self._unquote_header_keys(self.headers, 'x-object-meta-')
769

770
        path = path4url(self.account, self.container, obj)
771 772 773
        success = kwargs.pop('success', 201)
        return self.copy(path, *args, success=success, **kwargs)

774 775 776 777 778 779 780 781 782 783 784 785
    def object_move(
            self, object,
            format='json',
            ignore_content_type=False,
            if_etag_match=None,
            if_etag_not_match=None,
            destination=None,
            destination_account=None,
            content_type=None,
            content_encoding=None,
            content_disposition=None,
            permissions={},
786
            public=None,
787 788
            metadata={},
            *args, **kwargs):
789
        """ Full Pithos+ COPY at object level
790

791
        --- request parameters ---
792 793 794 795 796

        :param format: (string) json (default) or xml

        :param ignore_content_type: (bool) Ignore the supplied Content-Type

797
        --- request headers ---
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822

        :param if_etag_match: (string) if provided, return only results
            with etag matching with this

        :param if_etag_not_match: (string) if provided, return only results
            with etag not matching with this

        :param destination: (string) The destination path in the form
            /<container>/<object>

        :param destination_account: (string) The destination account to copy to

        :param content_type: (string) The MIME content type of the object

        :param content_encoding: (string) The encoding of the object

        :param content_disposition: (string) Object presentation style

        :param source_version: (string) The source version to copy from

        :param permissions: (dict) Object permissions in the form
            (all fields are optional)
            { 'read':[user1, group1, user2, ...],
            'write':['user3, group2, group3, ...] }

823
        :param public: (bool) If true, Object is published, False, unpublished
824 825 826 827 828

        :param metadata: (dict) Optional user defined metadata in the form
            {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}

        :returns: ConnectionResponse
829
        """
830
        self._assert_container()
831 832 833 834 835 836 837 838 839 840 841
        self.response_headers = [
            'If-Match',
            'If-None-Match',
            'Destination',
            'Destination-Account',
            'Content-Type',
            'Content-Encoding',
            'Content-Disposition',
            'X-Source-Version',
        ]
        self.response_header_prefices = ['X-Object-', ]
842 843
        self.request_header_prefices_to_quote = [
            'x-object-meta-', 'Destination']
844

845
        self.set_param('format', format, iff=format)
846 847 848 849 850 851 852 853 854
        self.set_param('ignore_content_type', iff=ignore_content_type)

        self.set_header('If-Match', if_etag_match)
        self.set_header('If-None-Match', if_etag_not_match)
        self.set_header('Destination', destination)
        self.set_header('Destination-Account', destination_account)
        self.set_header('Content-Type', content_type)
        self.set_header('Content-Encoding', content_encoding)
        self.set_header('Content-Disposition', content_disposition)
855 856 857 858
        perms = ';'.join(
            ['%s=%s' % (k, ','.join(v)) for k, v in permissions.items() if (
                v)]) if (permissions) else ''
        self.set_header('X-Object-Sharing', perms, iff=permissions)
859
        self.set_header('X-Object-Public', public, public is not None)
860 861 862
        if metadata:
            for key, val in metadata.items():
                self.set_header('X-Object-Meta-' + key, val)
863
        self._unquote_header_keys(self.headers, 'x-object-meta-')
864 865 866 867 868

        path = path4url(self.account, self.container, object)
        success = kwargs.pop('success', 201)
        return self.move(path, *args, success=success, **kwargs)

869
    def object_post(
870
            self, obj,
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886
            format='json',
            update=True,
            if_etag_match=None,
            if_etag_not_match=None,
            content_length=None,
            content_type=None,
            content_range=None,
            transfer_encoding=None,
            content_encoding=None,
            content_disposition=None,
            source_object=None,
            source_account=None,
            source_version=None,
            object_bytes=None,
            manifest=None,
            permissions={},
887
            public=None,
888 889
            metadata={},
            *args, **kwargs):
890
        """ Full Pithos+ POST at object level
891

892
        --- request parameters ---
893 894 895 896 897

        :param format: (string) json (default) or xml

        :param update: (bool) Do not replace metadata

898
        --- request headers ---
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934

        :param if_etag_match: (string) if provided, return only results
            with etag matching with this

        :param if_etag_not_match: (string) if provided, return only results
            with etag not matching with this

        :param content_length: (string) The size of the data written

        :param content_type: (string) The MIME content type of the object

        :param content_range: (string) The range of data supplied

        :param transfer_encoding: (string) Set to chunked to specify
            incremental uploading (if used, Content-Length is ignored)

        :param content_encoding: (string) The encoding of the object

        :param content_disposition: (string) Object presentation style

        :param source_object: (string) Update with data from the object at
            path /<container>/<object>

        :param source_account: (string) The source account to update from

        :param source_version: (string) The source version to copy from

        :param object_bytes: (integer) The updated objects final size

        :param manifest: (string) Object parts prefix as /<container>/<object>

        :param permissions: (dict) Object permissions in the form (all fields
            are optional)
            { 'read':[user1, group1, user2, ...],
            'write':['user3, group2, group3, ...] }

935
        :param public: (bool) If true, Object is published, False, unpublished
936 937 938 939 940

        :param metadata: (dict) Optional user defined metadata in the form
            {'meta-key-1':'meta-value-1', 'meta-key-2':'meta-value-2', ...}

        :returns: ConnectionResponse
941
        """
942
        self._assert_container()
943
        self.response_headers = ['ETag', 'X-Object-Version']
944
        self.request_header_prefices_to_quote = ['x-object-meta-', ]
945

946
        self.set_param('format', format, iff=format)
947
        self.set_param('update', '', iff=update)
948 949 950

        self.set_header('If-Match', if_etag_match)
        self.set_header('If-None-Match', if_etag_not_match)
951
        self.set_header(
952
            'Content-Length', content_length, iff=not transfer_encoding)
953 954 955 956 957 958 959 960 961 962
        self.set_header('Content-Type', content_type)
        self.set_header('Content-Range', content_range)
        self.set_header('Transfer-Encoding', transfer_encoding)
        self.set_header('Content-Encoding', content_encoding)
        self.set_header('Content-Disposition', content_disposition)
        self.set_header('X-Source-Object', source_object)
        self.set_header('X-Source-Account', source_account)
        self.set_header('X-Source-Version', source_version)
        self.set_header('X-Object-Bytes', object_bytes)
        self.set_header('X-Object-Manifest', manifest)
963 964 965 966
        perms = ';'.join(
            ['%s=%s' % (k, ','.join(v)) for k, v in permissions.items() if (
                v)]) if (permissions) else ''
        self.set_header('X-Object-Sharing', perms, iff=permissions)
967
        self.set_header('X-Object-Public', public, public is not None)
968
        for key, val in metadata.items():
969
            self.set_header('X-Object-Meta-' + key, val)
970
        self._quote_header_keys(self.headers, ('x-object-meta-', ))
971

972
        path = path4url(self.account, self.container, obj)
973
        success = kwargs.pop('success', (202, 204))
974
        return self.post(path, *args, success=success, **kwargs)
975

976 977 978 979
    def object_delete(
            self, object,
            until=None, delimiter=None,
            *args, **kwargs):
980
        """ Full Pithos+ DELETE at object level
981

982
        --- request parameters ---
983 984 985 986

        :param until: (string) Optional timestamp

        :returns: ConnectionResponse
987
        """
988
        self._assert_container()
989

990 991
        self.set_param('until', until, iff=until)
        self.set_param('delimiter', delimiter, iff=delimiter)
992 993 994

        path = path4url(self.account, self.container, object)
        success = kwargs.pop('success', 204)
995
        return self.delete(path, *args, success=success, **kwargs)