diff env/lib/python3.9/site-packages/boto/s3/bucket.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.9/site-packages/boto/s3/bucket.py	Mon Mar 22 18:12:50 2021 +0000
@@ -0,0 +1,1879 @@
+# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2010, Eucalyptus Systems, Inc.
+# All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish, dis-
+# tribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the fol-
+# lowing conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+import boto
+from boto import handler
+from boto.resultset import ResultSet
+from boto.exception import BotoClientError
+from boto.s3.acl import Policy, CannedACLStrings, Grant
+from boto.s3.key import Key
+from boto.s3.prefix import Prefix
+from boto.s3.deletemarker import DeleteMarker
+from boto.s3.multipart import MultiPartUpload
+from boto.s3.multipart import CompleteMultiPartUpload
+from boto.s3.multidelete import MultiDeleteResult
+from boto.s3.multidelete import Error
+from boto.s3.bucketlistresultset import BucketListResultSet
+from boto.s3.bucketlistresultset import VersionedBucketListResultSet
+from boto.s3.bucketlistresultset import MultiPartUploadListResultSet
+from boto.s3.lifecycle import Lifecycle
+from boto.s3.tagging import Tags
+from boto.s3.cors import CORSConfiguration
+from boto.s3.bucketlogging import BucketLogging
+from boto.s3 import website
+import boto.jsonresponse
+import boto.utils
+import xml.sax
+import xml.sax.saxutils
+import re
+import base64
+from collections import defaultdict
+from boto.compat import BytesIO, six, StringIO, urllib
+
+# as per http://goo.gl/BDuud (02/19/2011)
+
+
+class S3WebsiteEndpointTranslate(object):
+
+    trans_region = defaultdict(lambda: 's3-website-us-east-1')
+    trans_region['eu-west-1'] = 's3-website-eu-west-1'
+    trans_region['eu-central-1'] = 's3-website.eu-central-1'
+    trans_region['us-west-1'] = 's3-website-us-west-1'
+    trans_region['us-west-2'] = 's3-website-us-west-2'
+    trans_region['sa-east-1'] = 's3-website-sa-east-1'
+    trans_region['ap-northeast-1'] = 's3-website-ap-northeast-1'
+    trans_region['ap-southeast-1'] = 's3-website-ap-southeast-1'
+    trans_region['ap-southeast-2'] = 's3-website-ap-southeast-2'
+    trans_region['cn-north-1'] = 's3-website.cn-north-1'
+
+    @classmethod
+    def translate_region(self, reg):
+        return self.trans_region[reg]
+
+S3Permissions = ['READ', 'WRITE', 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL']
+
+
+class Bucket(object):
+
+    LoggingGroup = 'http://acs.amazonaws.com/groups/s3/LogDelivery'
+
+    BucketPaymentBody = """<?xml version="1.0" encoding="UTF-8"?>
+       <RequestPaymentConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+         <Payer>%s</Payer>
+       </RequestPaymentConfiguration>"""
+
+    VersioningBody = """<?xml version="1.0" encoding="UTF-8"?>
+       <VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+         <Status>%s</Status>
+         <MfaDelete>%s</MfaDelete>
+       </VersioningConfiguration>"""
+
+    VersionRE = '<Status>([A-Za-z]+)</Status>'
+    MFADeleteRE = '<MfaDelete>([A-Za-z]+)</MfaDelete>'
+
+    def __init__(self, connection=None, name=None, key_class=Key):
+        self.name = name
+        self.connection = connection
+        self.key_class = key_class
+
+    def __repr__(self):
+        return '<Bucket: %s>' % self.name
+
+    def __iter__(self):
+        return iter(BucketListResultSet(self))
+
+    def __contains__(self, key_name):
+        return not (self.get_key(key_name) is None)
+
+    def startElement(self, name, attrs, connection):
+        return None
+
+    def endElement(self, name, value, connection):
+        if name == 'Name':
+            self.name = value
+        elif name == 'CreationDate':
+            self.creation_date = value
+        else:
+            setattr(self, name, value)
+
+    def set_key_class(self, key_class):
+        """
+        Set the Key class associated with this bucket.  By default, this
+        would be the boto.s3.key.Key class but if you want to subclass that
+        for some reason this allows you to associate your new class with a
+        bucket so that when you call bucket.new_key() or when you get a listing
+        of keys in the bucket you will get an instances of your key class
+        rather than the default.
+
+        :type key_class: class
+        :param key_class: A subclass of Key that can be more specific
+        """
+        self.key_class = key_class
+
+    def lookup(self, key_name, headers=None):
+        """
+        Deprecated: Please use get_key method.
+
+        :type key_name: string
+        :param key_name: The name of the key to retrieve
+
+        :rtype: :class:`boto.s3.key.Key`
+        :returns: A Key object from this bucket.
+        """
+        return self.get_key(key_name, headers=headers)
+
+    def get_key(self, key_name, headers=None, version_id=None,
+                response_headers=None, validate=True):
+        """
+        Check to see if a particular key exists within the bucket.  This
+        method uses a HEAD request to check for the existence of the key.
+        Returns: An instance of a Key object or None
+
+        :param key_name: The name of the key to retrieve
+        :type key_name: string
+
+        :param headers: The headers to send when retrieving the key
+        :type headers: dict
+
+        :param version_id:
+        :type version_id: string
+
+        :param response_headers: A dictionary containing HTTP
+            headers/values that will override any headers associated
+            with the stored object in the response.  See
+            http://goo.gl/EWOPb for details.
+        :type response_headers: dict
+
+        :param validate: Verifies whether the key exists. If ``False``, this
+            will not hit the service, constructing an in-memory object.
+            Default is ``True``.
+        :type validate: bool
+
+        :rtype: :class:`boto.s3.key.Key`
+        :returns: A Key object from this bucket.
+        """
+        if validate is False:
+            if headers or version_id or response_headers:
+                raise BotoClientError(
+                    "When providing 'validate=False', no other params " + \
+                    "are allowed."
+                )
+
+            # This leans on the default behavior of ``new_key`` (not hitting
+            # the service). If that changes, that behavior should migrate here.
+            return self.new_key(key_name)
+
+        query_args_l = []
+        if version_id:
+            query_args_l.append('versionId=%s' % version_id)
+        if response_headers:
+            for rk, rv in six.iteritems(response_headers):
+                query_args_l.append('%s=%s' % (rk, urllib.parse.quote(rv)))
+
+        key, resp = self._get_key_internal(key_name, headers, query_args_l)
+        return key
+
+    def _get_key_internal(self, key_name, headers, query_args_l):
+        query_args = '&'.join(query_args_l) or None
+        response = self.connection.make_request('HEAD', self.name, key_name,
+                                                headers=headers,
+                                                query_args=query_args)
+        response.read()
+        # Allow any success status (2xx) - for example this lets us
+        # support Range gets, which return status 206:
+        if response.status / 100 == 2:
+            k = self.key_class(self)
+            provider = self.connection.provider
+            k.metadata = boto.utils.get_aws_metadata(response.msg, provider)
+            for field in Key.base_fields:
+                k.__dict__[field.lower().replace('-', '_')] = \
+                    response.getheader(field)
+            # the following machinations are a workaround to the fact that
+            # apache/fastcgi omits the content-length header on HEAD
+            # requests when the content-length is zero.
+            # See http://goo.gl/0Tdax for more details.
+            clen = response.getheader('content-length')
+            if clen:
+                k.size = int(response.getheader('content-length'))
+            else:
+                k.size = 0
+            k.name = key_name
+            k.handle_version_headers(response)
+            k.handle_encryption_headers(response)
+            k.handle_restore_headers(response)
+            k.handle_storage_class_header(response)
+            k.handle_addl_headers(response.getheaders())
+            return k, response
+        else:
+            if response.status == 404:
+                return None, response
+            else:
+                raise self.connection.provider.storage_response_error(
+                    response.status, response.reason, '')
+
+    def list(self, prefix='', delimiter='', marker='', headers=None,
+             encoding_type=None):
+        """
+        List key objects within a bucket.  This returns an instance of an
+        BucketListResultSet that automatically handles all of the result
+        paging, etc. from S3.  You just need to keep iterating until
+        there are no more results.
+
+        Called with no arguments, this will return an iterator object across
+        all keys within the bucket.
+
+        The Key objects returned by the iterator are obtained by parsing
+        the results of a GET on the bucket, also known as the List Objects
+        request.  The XML returned by this request contains only a subset
+        of the information about each key.  Certain metadata fields such
+        as Content-Type and user metadata are not available in the XML.
+        Therefore, if you want these additional metadata fields you will
+        have to do a HEAD request on the Key in the bucket.
+
+        :type prefix: string
+        :param prefix: allows you to limit the listing to a particular
+            prefix.  For example, if you call the method with
+            prefix='/foo/' then the iterator will only cycle through
+            the keys that begin with the string '/foo/'.
+
+        :type delimiter: string
+        :param delimiter: can be used in conjunction with the prefix
+            to allow you to organize and browse your keys
+            hierarchically. See http://goo.gl/Xx63h for more details.
+
+        :type marker: string
+        :param marker: The "marker" of where you are in the result set
+
+        :param encoding_type: Requests Amazon S3 to encode the response and
+            specifies the encoding method to use.
+
+            An object key can contain any Unicode character; however, XML 1.0
+            parser cannot parse some characters, such as characters with an
+            ASCII value from 0 to 10. For characters that are not supported in
+            XML 1.0, you can add this parameter to request that Amazon S3
+            encode the keys in the response.
+
+            Valid options: ``url``
+        :type encoding_type: string
+
+        :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet`
+        :return: an instance of a BucketListResultSet that handles paging, etc
+        """
+        return BucketListResultSet(self, prefix, delimiter, marker, headers,
+                                   encoding_type=encoding_type)
+
+    def list_versions(self, prefix='', delimiter='', key_marker='',
+                      version_id_marker='', headers=None, encoding_type=None):
+        """
+        List version objects within a bucket.  This returns an
+        instance of an VersionedBucketListResultSet that automatically
+        handles all of the result paging, etc. from S3.  You just need
+        to keep iterating until there are no more results.  Called
+        with no arguments, this will return an iterator object across
+        all keys within the bucket.
+
+        :type prefix: string
+        :param prefix: allows you to limit the listing to a particular
+            prefix.  For example, if you call the method with
+            prefix='/foo/' then the iterator will only cycle through
+            the keys that begin with the string '/foo/'.
+
+        :type delimiter: string
+        :param delimiter: can be used in conjunction with the prefix
+            to allow you to organize and browse your keys
+            hierarchically. See:
+
+            http://aws.amazon.com/releasenotes/Amazon-S3/213
+
+            for more details.
+
+        :type key_marker: string
+        :param key_marker: The "marker" of where you are in the result set
+
+        :param encoding_type: Requests Amazon S3 to encode the response and
+            specifies the encoding method to use.
+
+            An object key can contain any Unicode character; however, XML 1.0
+            parser cannot parse some characters, such as characters with an
+            ASCII value from 0 to 10. For characters that are not supported in
+            XML 1.0, you can add this parameter to request that Amazon S3
+            encode the keys in the response.
+
+            Valid options: ``url``
+        :type encoding_type: string
+
+        :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet`
+        :return: an instance of a BucketListResultSet that handles paging, etc
+        """
+        return VersionedBucketListResultSet(self, prefix, delimiter,
+                                            key_marker, version_id_marker,
+                                            headers,
+                                            encoding_type=encoding_type)
+
+    def list_multipart_uploads(self, key_marker='',
+                               upload_id_marker='',
+                               headers=None, encoding_type=None):
+        """
+        List multipart upload objects within a bucket.  This returns an
+        instance of an MultiPartUploadListResultSet that automatically
+        handles all of the result paging, etc. from S3.  You just need
+        to keep iterating until there are no more results.
+
+        :type key_marker: string
+        :param key_marker: The "marker" of where you are in the result set
+
+        :type upload_id_marker: string
+        :param upload_id_marker: The upload identifier
+
+        :param encoding_type: Requests Amazon S3 to encode the response and
+            specifies the encoding method to use.
+
+            An object key can contain any Unicode character; however, XML 1.0
+            parser cannot parse some characters, such as characters with an
+            ASCII value from 0 to 10. For characters that are not supported in
+            XML 1.0, you can add this parameter to request that Amazon S3
+            encode the keys in the response.
+
+            Valid options: ``url``
+        :type encoding_type: string
+
+        :rtype: :class:`boto.s3.bucketlistresultset.MultiPartUploadListResultSet`
+        :return: an instance of a BucketListResultSet that handles paging, etc
+        """
+        return MultiPartUploadListResultSet(self, key_marker,
+                                            upload_id_marker,
+                                            headers,
+                                            encoding_type=encoding_type)
+
+    def _get_all_query_args(self, params, initial_query_string=''):
+        pairs = []
+
+        if initial_query_string:
+            pairs.append(initial_query_string)
+
+        for key, value in sorted(params.items(), key=lambda x: x[0]):
+            if value is None:
+                continue
+            key = key.replace('_', '-')
+            if key == 'maxkeys':
+                key = 'max-keys'
+            if not isinstance(value, six.string_types + (six.binary_type,)):
+                value = six.text_type(value)
+            if not isinstance(value, six.binary_type):
+                value = value.encode('utf-8')
+            if value:
+                pairs.append(u'%s=%s' % (
+                    urllib.parse.quote(key),
+                    urllib.parse.quote(value)
+                ))
+
+        return '&'.join(pairs)
+
+    def _get_all(self, element_map, initial_query_string='',
+                 headers=None, **params):
+        query_args = self._get_all_query_args(
+            params,
+            initial_query_string=initial_query_string
+        )
+        response = self.connection.make_request('GET', self.name,
+                                                headers=headers,
+                                                query_args=query_args)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 200:
+            rs = ResultSet(element_map)
+            h = handler.XmlHandler(rs, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            return rs
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def validate_kwarg_names(self, kwargs, names):
+        """
+        Checks that all named arguments are in the specified list of names.
+
+        :type kwargs: dict
+        :param kwargs: Dictionary of kwargs to validate.
+
+        :type names: list
+        :param names: List of possible named arguments.
+        """
+        for kwarg in kwargs:
+            if kwarg not in names:
+                raise TypeError('Invalid argument "%s"!' % kwarg)
+
+    def get_all_keys(self, headers=None, **params):
+        """
+        A lower-level method for listing contents of a bucket.  This
+        closely models the actual S3 API and requires you to manually
+        handle the paging of results.  For a higher-level method that
+        handles the details of paging for you, you can use the list
+        method.
+
+        :type max_keys: int
+        :param max_keys: The maximum number of keys to retrieve
+
+        :type prefix: string
+        :param prefix: The prefix of the keys you want to retrieve
+
+        :type marker: string
+        :param marker: The "marker" of where you are in the result set
+
+        :type delimiter: string
+        :param delimiter: If this optional, Unicode string parameter
+            is included with your request, then keys that contain the
+            same string between the prefix and the first occurrence of
+            the delimiter will be rolled up into a single result
+            element in the CommonPrefixes collection. These rolled-up
+            keys are not returned elsewhere in the response.
+
+        :param encoding_type: Requests Amazon S3 to encode the response and
+            specifies the encoding method to use.
+
+            An object key can contain any Unicode character; however, XML 1.0
+            parser cannot parse some characters, such as characters with an
+            ASCII value from 0 to 10. For characters that are not supported in
+            XML 1.0, you can add this parameter to request that Amazon S3
+            encode the keys in the response.
+
+            Valid options: ``url``
+        :type encoding_type: string
+
+        :rtype: ResultSet
+        :return: The result from S3 listing the keys requested
+
+        """
+        self.validate_kwarg_names(params, ['maxkeys', 'max_keys', 'prefix',
+                                           'marker', 'delimiter',
+                                           'encoding_type'])
+        return self._get_all([('Contents', self.key_class),
+                              ('CommonPrefixes', Prefix)],
+                             '', headers, **params)
+
+    def get_all_versions(self, headers=None, **params):
+        """
+        A lower-level, version-aware method for listing contents of a
+        bucket.  This closely models the actual S3 API and requires
+        you to manually handle the paging of results.  For a
+        higher-level method that handles the details of paging for
+        you, you can use the list method.
+
+        :type max_keys: int
+        :param max_keys: The maximum number of keys to retrieve
+
+        :type prefix: string
+        :param prefix: The prefix of the keys you want to retrieve
+
+        :type key_marker: string
+        :param key_marker: The "marker" of where you are in the result set
+            with respect to keys.
+
+        :type version_id_marker: string
+        :param version_id_marker: The "marker" of where you are in the result
+            set with respect to version-id's.
+
+        :type delimiter: string
+        :param delimiter: If this optional, Unicode string parameter
+            is included with your request, then keys that contain the
+            same string between the prefix and the first occurrence of
+            the delimiter will be rolled up into a single result
+            element in the CommonPrefixes collection. These rolled-up
+            keys are not returned elsewhere in the response.
+
+        :param encoding_type: Requests Amazon S3 to encode the response and
+            specifies the encoding method to use.
+
+            An object key can contain any Unicode character; however, XML 1.0
+            parser cannot parse some characters, such as characters with an
+            ASCII value from 0 to 10. For characters that are not supported in
+            XML 1.0, you can add this parameter to request that Amazon S3
+            encode the keys in the response.
+
+            Valid options: ``url``
+        :type encoding_type: string
+
+        :rtype: ResultSet
+        :return: The result from S3 listing the keys requested
+        """
+        self.validate_get_all_versions_params(params)
+        return self._get_all([('Version', self.key_class),
+                              ('CommonPrefixes', Prefix),
+                              ('DeleteMarker', DeleteMarker)],
+                             'versions', headers, **params)
+
+    def validate_get_all_versions_params(self, params):
+        """
+        Validate that the parameters passed to get_all_versions are valid.
+        Overridden by subclasses that allow a different set of parameters.
+
+        :type params: dict
+        :param params: Parameters to validate.
+        """
+        self.validate_kwarg_names(
+                params, ['maxkeys', 'max_keys', 'prefix', 'key_marker',
+                         'version_id_marker', 'delimiter', 'encoding_type'])
+
+    def get_all_multipart_uploads(self, headers=None, **params):
+        """
+        A lower-level, version-aware method for listing active
+        MultiPart uploads for a bucket.  This closely models the
+        actual S3 API and requires you to manually handle the paging
+        of results.  For a higher-level method that handles the
+        details of paging for you, you can use the list method.
+
+        :type max_uploads: int
+        :param max_uploads: The maximum number of uploads to retrieve.
+            Default value is 1000.
+
+        :type key_marker: string
+        :param key_marker: Together with upload_id_marker, this
+            parameter specifies the multipart upload after which
+            listing should begin.  If upload_id_marker is not
+            specified, only the keys lexicographically greater than
+            the specified key_marker will be included in the list.
+
+            If upload_id_marker is specified, any multipart uploads
+            for a key equal to the key_marker might also be included,
+            provided those multipart uploads have upload IDs
+            lexicographically greater than the specified
+            upload_id_marker.
+
+        :type upload_id_marker: string
+        :param upload_id_marker: Together with key-marker, specifies
+            the multipart upload after which listing should begin. If
+            key_marker is not specified, the upload_id_marker
+            parameter is ignored.  Otherwise, any multipart uploads
+            for a key equal to the key_marker might be included in the
+            list only if they have an upload ID lexicographically
+            greater than the specified upload_id_marker.
+
+        :type encoding_type: string
+        :param encoding_type: Requests Amazon S3 to encode the response and
+            specifies the encoding method to use.
+
+            An object key can contain any Unicode character; however, XML 1.0
+            parser cannot parse some characters, such as characters with an
+            ASCII value from 0 to 10. For characters that are not supported in
+            XML 1.0, you can add this parameter to request that Amazon S3
+            encode the keys in the response.
+
+            Valid options: ``url``
+
+        :type delimiter: string
+        :param delimiter: Character you use to group keys.
+            All keys that contain the same string between the prefix, if
+            specified, and the first occurrence of the delimiter after the
+            prefix are grouped under a single result element, CommonPrefixes.
+            If you don't specify the prefix parameter, then the substring
+            starts at the beginning of the key. The keys that are grouped
+            under CommonPrefixes result element are not returned elsewhere
+            in the response.
+
+        :type prefix: string
+        :param prefix: Lists in-progress uploads only for those keys that
+            begin with the specified prefix. You can use prefixes to separate
+            a bucket into different grouping of keys. (You can think of using
+            prefix to make groups in the same way you'd use a folder in a
+            file system.)
+
+        :rtype: ResultSet
+        :return: The result from S3 listing the uploads requested
+
+        """
+        self.validate_kwarg_names(params, ['max_uploads', 'key_marker',
+                                           'upload_id_marker', 'encoding_type',
+                                           'delimiter', 'prefix'])
+        return self._get_all([('Upload', MultiPartUpload),
+                              ('CommonPrefixes', Prefix)],
+                             'uploads', headers, **params)
+
+    def new_key(self, key_name=None):
+        """
+        Creates a new key
+
+        :type key_name: string
+        :param key_name: The name of the key to create
+
+        :rtype: :class:`boto.s3.key.Key` or subclass
+        :returns: An instance of the newly created key object
+        """
+        if not key_name:
+            raise ValueError('Empty key names are not allowed')
+        return self.key_class(self, key_name)
+
+    def generate_url(self, expires_in, method='GET', headers=None,
+                     force_http=False, response_headers=None,
+                     expires_in_absolute=False):
+        return self.connection.generate_url(expires_in, method, self.name,
+                                            headers=headers,
+                                            force_http=force_http,
+                                            response_headers=response_headers,
+                                            expires_in_absolute=expires_in_absolute)
+
+    def delete_keys(self, keys, quiet=False, mfa_token=None, headers=None):
+        """
+        Deletes a set of keys using S3's Multi-object delete API. If a
+        VersionID is specified for that key then that version is removed.
+        Returns a MultiDeleteResult Object, which contains Deleted
+        and Error elements for each key you ask to delete.
+
+        :type keys: list
+        :param keys: A list of either key_names or (key_name, versionid) pairs
+            or a list of Key instances.
+
+        :type quiet: boolean
+        :param quiet: In quiet mode the response includes only keys
+            where the delete operation encountered an error. For a
+            successful deletion, the operation does not return any
+            information about the delete in the response body.
+
+        :type mfa_token: tuple or list of strings
+        :param mfa_token: A tuple or list consisting of the serial
+            number from the MFA device and the current value of the
+            six-digit token associated with the device.  This value is
+            required anytime you are deleting versioned objects from a
+            bucket that has the MFADelete option on the bucket.
+
+        :returns: An instance of MultiDeleteResult
+        """
+        ikeys = iter(keys)
+        result = MultiDeleteResult(self)
+        provider = self.connection.provider
+        query_args = 'delete'
+
+        def delete_keys2(hdrs):
+            hdrs = hdrs or {}
+            data = u"""<?xml version="1.0" encoding="UTF-8"?>"""
+            data += u"<Delete>"
+            if quiet:
+                data += u"<Quiet>true</Quiet>"
+            count = 0
+            while count < 1000:
+                try:
+                    key = next(ikeys)
+                except StopIteration:
+                    break
+                if isinstance(key, six.string_types):
+                    key_name = key
+                    version_id = None
+                elif isinstance(key, tuple) and len(key) == 2:
+                    key_name, version_id = key
+                elif (isinstance(key, Key) or isinstance(key, DeleteMarker)) and key.name:
+                    key_name = key.name
+                    version_id = key.version_id
+                else:
+                    if isinstance(key, Prefix):
+                        key_name = key.name
+                        code = 'PrefixSkipped'   # Don't delete Prefix
+                    else:
+                        key_name = repr(key)   # try get a string
+                        code = 'InvalidArgument'  # other unknown type
+                    message = 'Invalid. No delete action taken for this object.'
+                    error = Error(key_name, code=code, message=message)
+                    result.errors.append(error)
+                    continue
+                count += 1
+                data += u"<Object><Key>%s</Key>" % xml.sax.saxutils.escape(key_name)
+                if version_id:
+                    data += u"<VersionId>%s</VersionId>" % version_id
+                data += u"</Object>"
+            data += u"</Delete>"
+            if count <= 0:
+                return False  # no more
+            data = data.encode('utf-8')
+            fp = BytesIO(data)
+            md5 = boto.utils.compute_md5(fp)
+            hdrs['Content-MD5'] = md5[1]
+            hdrs['Content-Type'] = 'text/xml'
+            if mfa_token:
+                hdrs[provider.mfa_header] = ' '.join(mfa_token)
+            response = self.connection.make_request('POST', self.name,
+                                                    headers=hdrs,
+                                                    query_args=query_args,
+                                                    data=data)
+            body = response.read()
+            if response.status == 200:
+                h = handler.XmlHandler(result, self)
+                if not isinstance(body, bytes):
+                    body = body.encode('utf-8')
+                xml.sax.parseString(body, h)
+                return count >= 1000  # more?
+            else:
+                raise provider.storage_response_error(response.status,
+                                                      response.reason,
+                                                      body)
+        while delete_keys2(headers):
+            pass
+        return result
+
+    def delete_key(self, key_name, headers=None, version_id=None,
+                   mfa_token=None):
+        """
+        Deletes a key from the bucket.  If a version_id is provided,
+        only that version of the key will be deleted.
+
+        :type key_name: string
+        :param key_name: The key name to delete
+
+        :type version_id: string
+        :param version_id: The version ID (optional)
+
+        :type mfa_token: tuple or list of strings
+        :param mfa_token: A tuple or list consisting of the serial
+            number from the MFA device and the current value of the
+            six-digit token associated with the device.  This value is
+            required anytime you are deleting versioned objects from a
+            bucket that has the MFADelete option on the bucket.
+
+        :rtype: :class:`boto.s3.key.Key` or subclass
+        :returns: A key object holding information on what was
+            deleted.  The Caller can see if a delete_marker was
+            created or removed and what version_id the delete created
+            or removed.
+        """
+        if not key_name:
+            raise ValueError('Empty key names are not allowed')
+        return self._delete_key_internal(key_name, headers=headers,
+                                         version_id=version_id,
+                                         mfa_token=mfa_token,
+                                         query_args_l=None)
+
+    def _delete_key_internal(self, key_name, headers=None, version_id=None,
+                             mfa_token=None, query_args_l=None):
+        query_args_l = query_args_l or []
+        provider = self.connection.provider
+        if version_id:
+            query_args_l.append('versionId=%s' % version_id)
+        query_args = '&'.join(query_args_l) or None
+        if mfa_token:
+            if not headers:
+                headers = {}
+            headers[provider.mfa_header] = ' '.join(mfa_token)
+        response = self.connection.make_request('DELETE', self.name, key_name,
+                                                headers=headers,
+                                                query_args=query_args)
+        body = response.read()
+        if response.status != 204:
+            raise provider.storage_response_error(response.status,
+                                                  response.reason, body)
+        else:
+            # return a key object with information on what was deleted.
+            k = self.key_class(self)
+            k.name = key_name
+            k.handle_version_headers(response)
+            k.handle_addl_headers(response.getheaders())
+            return k
+
+    def copy_key(self, new_key_name, src_bucket_name,
+                 src_key_name, metadata=None, src_version_id=None,
+                 storage_class='STANDARD', preserve_acl=False,
+                 encrypt_key=False, headers=None, query_args=None):
+        """
+        Create a new key in the bucket by copying another existing key.
+
+        :type new_key_name: string
+        :param new_key_name: The name of the new key
+
+        :type src_bucket_name: string
+        :param src_bucket_name: The name of the source bucket
+
+        :type src_key_name: string
+        :param src_key_name: The name of the source key
+
+        :type src_version_id: string
+        :param src_version_id: The version id for the key.  This param
+            is optional.  If not specified, the newest version of the
+            key will be copied.
+
+        :type metadata: dict
+        :param metadata: Metadata to be associated with new key.  If
+            metadata is supplied, it will replace the metadata of the
+            source key being copied.  If no metadata is supplied, the
+            source key's metadata will be copied to the new key.
+
+        :type storage_class: string
+        :param storage_class: The storage class of the new key.  By
+            default, the new key will use the standard storage class.
+            Possible values are: STANDARD | REDUCED_REDUNDANCY
+
+        :type preserve_acl: bool
+        :param preserve_acl: If True, the ACL from the source key will
+            be copied to the destination key.  If False, the
+            destination key will have the default ACL.  Note that
+            preserving the ACL in the new key object will require two
+            additional API calls to S3, one to retrieve the current
+            ACL and one to set that ACL on the new object.  If you
+            don't care about the ACL, a value of False will be
+            significantly more efficient.
+
+        :type encrypt_key: bool
+        :param encrypt_key: If True, the new copy of the object will
+            be encrypted on the server-side by S3 and will be stored
+            in an encrypted form while at rest in S3.
+
+        :type headers: dict
+        :param headers: A dictionary of header name/value pairs.
+
+        :type query_args: string
+        :param query_args: A string of additional querystring arguments
+            to append to the request
+
+        :rtype: :class:`boto.s3.key.Key` or subclass
+        :returns: An instance of the newly created key object
+        """
+        headers = headers or {}
+        provider = self.connection.provider
+        src_key_name = boto.utils.get_utf8_value(src_key_name)
+        if preserve_acl:
+            if self.name == src_bucket_name:
+                src_bucket = self
+            else:
+                src_bucket = self.connection.get_bucket(
+                    src_bucket_name, validate=False, headers=headers)
+            acl = src_bucket.get_xml_acl(src_key_name, headers=headers)
+        if encrypt_key:
+            headers[provider.server_side_encryption_header] = 'AES256'
+        src = '%s/%s' % (src_bucket_name, urllib.parse.quote(src_key_name))
+        if src_version_id:
+            src += '?versionId=%s' % src_version_id
+        headers[provider.copy_source_header] = str(src)
+        # make sure storage_class_header key exists before accessing it
+        if provider.storage_class_header and storage_class:
+            headers[provider.storage_class_header] = storage_class
+        if metadata is not None:
+            headers[provider.metadata_directive_header] = 'REPLACE'
+            headers = boto.utils.merge_meta(headers, metadata, provider)
+        elif not query_args:  # Can't use this header with multi-part copy.
+            headers[provider.metadata_directive_header] = 'COPY'
+        response = self.connection.make_request('PUT', self.name, new_key_name,
+                                                headers=headers,
+                                                query_args=query_args)
+        body = response.read()
+        if response.status == 200:
+            key = self.new_key(new_key_name)
+            h = handler.XmlHandler(key, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            if hasattr(key, 'Error'):
+                raise provider.storage_copy_error(key.Code, key.Message, body)
+            key.handle_version_headers(response)
+            key.handle_addl_headers(response.getheaders())
+            if preserve_acl:
+                self.set_xml_acl(acl, new_key_name)
+            return key
+        else:
+            raise provider.storage_response_error(response.status,
+                                                  response.reason, body)
+
+    def set_canned_acl(self, acl_str, key_name='', headers=None,
+                       version_id=None):
+        assert acl_str in CannedACLStrings
+
+        if headers:
+            headers[self.connection.provider.acl_header] = acl_str
+        else:
+            headers = {self.connection.provider.acl_header: acl_str}
+
+        query_args = 'acl'
+        if version_id:
+            query_args += '&versionId=%s' % version_id
+        response = self.connection.make_request('PUT', self.name, key_name,
+                headers=headers, query_args=query_args)
+        body = response.read()
+        if response.status != 200:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_xml_acl(self, key_name='', headers=None, version_id=None):
+        query_args = 'acl'
+        if version_id:
+            query_args += '&versionId=%s' % version_id
+        response = self.connection.make_request('GET', self.name, key_name,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        if response.status != 200:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+        return body
+
+    def set_xml_acl(self, acl_str, key_name='', headers=None, version_id=None,
+                    query_args='acl'):
+        if version_id:
+            query_args += '&versionId=%s' % version_id
+        if not isinstance(acl_str, bytes):
+            acl_str = acl_str.encode('utf-8')
+        response = self.connection.make_request('PUT', self.name, key_name,
+                                                data=acl_str,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        if response.status != 200:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_acl(self, acl_or_str, key_name='', headers=None, version_id=None):
+        if isinstance(acl_or_str, Policy):
+            self.set_xml_acl(acl_or_str.to_xml(), key_name,
+                             headers, version_id)
+        else:
+            self.set_canned_acl(acl_or_str, key_name,
+                                headers, version_id)
+
+    def get_acl(self, key_name='', headers=None, version_id=None):
+        query_args = 'acl'
+        if version_id:
+            query_args += '&versionId=%s' % version_id
+        response = self.connection.make_request('GET', self.name, key_name,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        if response.status == 200:
+            policy = Policy(self)
+            h = handler.XmlHandler(policy, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            return policy
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_subresource(self, subresource, value, key_name='', headers=None,
+                        version_id=None):
+        """
+        Set a subresource for a bucket or key.
+
+        :type subresource: string
+        :param subresource: The subresource to set.
+
+        :type value: string
+        :param value: The value of the subresource.
+
+        :type key_name: string
+        :param key_name: The key to operate on, or None to operate on the
+            bucket.
+
+        :type headers: dict
+        :param headers: Additional HTTP headers to include in the request.
+
+        :type src_version_id: string
+        :param src_version_id: Optional. The version id of the key to
+            operate on. If not specified, operate on the newest
+            version.
+        """
+        if not subresource:
+            raise TypeError('set_subresource called with subresource=None')
+        query_args = subresource
+        if version_id:
+            query_args += '&versionId=%s' % version_id
+        if not isinstance(value, bytes):
+            value = value.encode('utf-8')
+        response = self.connection.make_request('PUT', self.name, key_name,
+                                                data=value,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        if response.status != 200:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_subresource(self, subresource, key_name='', headers=None,
+                        version_id=None):
+        """
+        Get a subresource for a bucket or key.
+
+        :type subresource: string
+        :param subresource: The subresource to get.
+
+        :type key_name: string
+        :param key_name: The key to operate on, or None to operate on the
+            bucket.
+
+        :type headers: dict
+        :param headers: Additional HTTP headers to include in the request.
+
+        :type src_version_id: string
+        :param src_version_id: Optional. The version id of the key to
+            operate on. If not specified, operate on the newest
+            version.
+
+        :rtype: string
+        :returns: The value of the subresource.
+        """
+        if not subresource:
+            raise TypeError('get_subresource called with subresource=None')
+        query_args = subresource
+        if version_id:
+            query_args += '&versionId=%s' % version_id
+        response = self.connection.make_request('GET', self.name, key_name,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        if response.status != 200:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+        return body
+
+    def make_public(self, recursive=False, headers=None):
+        self.set_canned_acl('public-read', headers=headers)
+        if recursive:
+            for key in self:
+                self.set_canned_acl('public-read', key.name, headers=headers)
+
+    def add_email_grant(self, permission, email_address,
+                        recursive=False, headers=None):
+        """
+        Convenience method that provides a quick way to add an email grant
+        to a bucket. This method retrieves the current ACL, creates a new
+        grant based on the parameters passed in, adds that grant to the ACL
+        and then PUT's the new ACL back to S3.
+
+        :type permission: string
+        :param permission: The permission being granted. Should be one of:
+            (READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL).
+
+        :type email_address: string
+        :param email_address: The email address associated with the AWS
+            account your are granting the permission to.
+
+        :type recursive: boolean
+        :param recursive: A boolean value to controls whether the
+            command will apply the grant to all keys within the bucket
+            or not.  The default value is False.  By passing a True
+            value, the call will iterate through all keys in the
+            bucket and apply the same grant to each key.  CAUTION: If
+            you have a lot of keys, this could take a long time!
+        """
+        if permission not in S3Permissions:
+            raise self.connection.provider.storage_permissions_error(
+                'Unknown Permission: %s' % permission)
+        policy = self.get_acl(headers=headers)
+        policy.acl.add_email_grant(permission, email_address)
+        self.set_acl(policy, headers=headers)
+        if recursive:
+            for key in self:
+                key.add_email_grant(permission, email_address, headers=headers)
+
+    def add_user_grant(self, permission, user_id, recursive=False,
+                       headers=None, display_name=None):
+        """
+        Convenience method that provides a quick way to add a canonical
+        user grant to a bucket.  This method retrieves the current ACL,
+        creates a new grant based on the parameters passed in, adds that
+        grant to the ACL and then PUT's the new ACL back to S3.
+
+        :type permission: string
+        :param permission: The permission being granted. Should be one of:
+            (READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL).
+
+        :type user_id: string
+        :param user_id:     The canonical user id associated with the AWS
+            account your are granting the permission to.
+
+        :type recursive: boolean
+        :param recursive: A boolean value to controls whether the
+            command will apply the grant to all keys within the bucket
+            or not.  The default value is False.  By passing a True
+            value, the call will iterate through all keys in the
+            bucket and apply the same grant to each key.  CAUTION: If
+            you have a lot of keys, this could take a long time!
+
+        :type display_name: string
+        :param display_name: An option string containing the user's
+            Display Name.  Only required on Walrus.
+        """
+        if permission not in S3Permissions:
+            raise self.connection.provider.storage_permissions_error(
+                'Unknown Permission: %s' % permission)
+        policy = self.get_acl(headers=headers)
+        policy.acl.add_user_grant(permission, user_id,
+                                  display_name=display_name)
+        self.set_acl(policy, headers=headers)
+        if recursive:
+            for key in self:
+                key.add_user_grant(permission, user_id, headers=headers,
+                                   display_name=display_name)
+
+    def list_grants(self, headers=None):
+        policy = self.get_acl(headers=headers)
+        return policy.acl.grants
+
+    def get_location(self, headers=None):
+        """
+        Returns the LocationConstraint for the bucket.
+
+        :rtype: str
+        :return: The LocationConstraint for the bucket or the empty
+            string if no constraint was specified when bucket was created.
+        """
+        response = self.connection.make_request('GET', self.name,
+                                                headers=headers,
+                                                query_args='location')
+        body = response.read()
+        if response.status == 200:
+            rs = ResultSet(self)
+            h = handler.XmlHandler(rs, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            return rs.LocationConstraint
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_xml_logging(self, logging_str, headers=None):
+        """
+        Set logging on a bucket directly to the given xml string.
+
+        :type logging_str: unicode string
+        :param logging_str: The XML for the bucketloggingstatus which
+            will be set.  The string will be converted to utf-8 before
+            it is sent.  Usually, you will obtain this XML from the
+            BucketLogging object.
+
+        :rtype: bool
+        :return: True if ok or raises an exception.
+        """
+        body = logging_str
+        if not isinstance(body, bytes):
+            body = body.encode('utf-8')
+        response = self.connection.make_request('PUT', self.name, data=body,
+                query_args='logging', headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def enable_logging(self, target_bucket, target_prefix='',
+                       grants=None, headers=None):
+        """
+        Enable logging on a bucket.
+
+        :type target_bucket: bucket or string
+        :param target_bucket: The bucket to log to.
+
+        :type target_prefix: string
+        :param target_prefix: The prefix which should be prepended to the
+            generated log files written to the target_bucket.
+
+        :type grants: list of Grant objects
+        :param grants: A list of extra permissions which will be granted on
+            the log files which are created.
+
+        :rtype: bool
+        :return: True if ok or raises an exception.
+        """
+        if isinstance(target_bucket, Bucket):
+            target_bucket = target_bucket.name
+        blogging = BucketLogging(target=target_bucket, prefix=target_prefix,
+                                 grants=grants)
+        return self.set_xml_logging(blogging.to_xml(), headers=headers)
+
+    def disable_logging(self, headers=None):
+        """
+        Disable logging on a bucket.
+
+        :rtype: bool
+        :return: True if ok or raises an exception.
+        """
+        blogging = BucketLogging()
+        return self.set_xml_logging(blogging.to_xml(), headers=headers)
+
+    def get_logging_status(self, headers=None):
+        """
+        Get the logging status for this bucket.
+
+        :rtype: :class:`boto.s3.bucketlogging.BucketLogging`
+        :return: A BucketLogging object for this bucket.
+        """
+        response = self.connection.make_request('GET', self.name,
+                query_args='logging', headers=headers)
+        body = response.read()
+        if response.status == 200:
+            blogging = BucketLogging()
+            h = handler.XmlHandler(blogging, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            return blogging
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_as_logging_target(self, headers=None):
+        """
+        Setup the current bucket as a logging target by granting the necessary
+        permissions to the LogDelivery group to write log files to this bucket.
+        """
+        policy = self.get_acl(headers=headers)
+        g1 = Grant(permission='WRITE', type='Group', uri=self.LoggingGroup)
+        g2 = Grant(permission='READ_ACP', type='Group', uri=self.LoggingGroup)
+        policy.acl.add_grant(g1)
+        policy.acl.add_grant(g2)
+        self.set_acl(policy, headers=headers)
+
+    def get_request_payment(self, headers=None):
+        response = self.connection.make_request('GET', self.name,
+                query_args='requestPayment', headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return body
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_request_payment(self, payer='BucketOwner', headers=None):
+        body = self.BucketPaymentBody % payer
+        response = self.connection.make_request('PUT', self.name, data=body,
+                query_args='requestPayment', headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def configure_versioning(self, versioning, mfa_delete=False,
+                             mfa_token=None, headers=None):
+        """
+        Configure versioning for this bucket.
+
+        ..note:: This feature is currently in beta.
+
+        :type versioning: bool
+        :param versioning: A boolean indicating whether version is
+            enabled (True) or disabled (False).
+
+        :type mfa_delete: bool
+        :param mfa_delete: A boolean indicating whether the
+            Multi-Factor Authentication Delete feature is enabled
+            (True) or disabled (False).  If mfa_delete is enabled then
+            all Delete operations will require the token from your MFA
+            device to be passed in the request.
+
+        :type mfa_token: tuple or list of strings
+        :param mfa_token: A tuple or list consisting of the serial
+            number from the MFA device and the current value of the
+            six-digit token associated with the device.  This value is
+            required when you are changing the status of the MfaDelete
+            property of the bucket.
+        """
+        if versioning:
+            ver = 'Enabled'
+        else:
+            ver = 'Suspended'
+        if mfa_delete:
+            mfa = 'Enabled'
+        else:
+            mfa = 'Disabled'
+        body = self.VersioningBody % (ver, mfa)
+        if mfa_token:
+            if not headers:
+                headers = {}
+            provider = self.connection.provider
+            headers[provider.mfa_header] = ' '.join(mfa_token)
+        response = self.connection.make_request('PUT', self.name, data=body,
+                query_args='versioning', headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_versioning_status(self, headers=None):
+        """
+        Returns the current status of versioning on the bucket.
+
+        :rtype: dict
+        :returns: A dictionary containing a key named 'Versioning'
+            that can have a value of either Enabled, Disabled, or
+            Suspended. Also, if MFADelete has ever been enabled on the
+            bucket, the dictionary will contain a key named
+            'MFADelete' which will have a value of either Enabled or
+            Suspended.
+        """
+        response = self.connection.make_request('GET', self.name,
+                query_args='versioning', headers=headers)
+        body = response.read()
+        if not isinstance(body, six.string_types):
+            body = body.decode('utf-8')
+        boto.log.debug(body)
+        if response.status == 200:
+            d = {}
+            ver = re.search(self.VersionRE, body)
+            if ver:
+                d['Versioning'] = ver.group(1)
+            mfa = re.search(self.MFADeleteRE, body)
+            if mfa:
+                d['MfaDelete'] = mfa.group(1)
+            return d
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def configure_lifecycle(self, lifecycle_config, headers=None):
+        """
+        Configure lifecycle for this bucket.
+
+        :type lifecycle_config: :class:`boto.s3.lifecycle.Lifecycle`
+        :param lifecycle_config: The lifecycle configuration you want
+            to configure for this bucket.
+        """
+        xml = lifecycle_config.to_xml()
+        #xml = xml.encode('utf-8')
+        fp = StringIO(xml)
+        md5 = boto.utils.compute_md5(fp)
+        if headers is None:
+            headers = {}
+        headers['Content-MD5'] = md5[1]
+        headers['Content-Type'] = 'text/xml'
+        response = self.connection.make_request('PUT', self.name,
+                                                data=fp.getvalue(),
+                                                query_args='lifecycle',
+                                                headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_lifecycle_config(self, headers=None):
+        """
+        Returns the current lifecycle configuration on the bucket.
+
+        :rtype: :class:`boto.s3.lifecycle.Lifecycle`
+        :returns: A LifecycleConfig object that describes all current
+            lifecycle rules in effect for the bucket.
+        """
+        response = self.connection.make_request('GET', self.name,
+                query_args='lifecycle', headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 200:
+            lifecycle = Lifecycle()
+            h = handler.XmlHandler(lifecycle, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            return lifecycle
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def delete_lifecycle_configuration(self, headers=None):
+        """
+        Removes all lifecycle configuration from the bucket.
+        """
+        response = self.connection.make_request('DELETE', self.name,
+                                                query_args='lifecycle',
+                                                headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 204:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def configure_website(self, suffix=None, error_key=None,
+                          redirect_all_requests_to=None,
+                          routing_rules=None,
+                          headers=None):
+        """
+        Configure this bucket to act as a website
+
+        :type suffix: str
+        :param suffix: Suffix that is appended to a request that is for a
+            "directory" on the website endpoint (e.g. if the suffix is
+            index.html and you make a request to samplebucket/images/
+            the data that is returned will be for the object with the
+            key name images/index.html).  The suffix must not be empty
+            and must not include a slash character.
+
+        :type error_key: str
+        :param error_key: The object key name to use when a 4XX class
+            error occurs.  This is optional.
+
+        :type redirect_all_requests_to: :class:`boto.s3.website.RedirectLocation`
+        :param redirect_all_requests_to: Describes the redirect behavior for
+            every request to this bucket's website endpoint. If this value is
+            non None, no other values are considered when configuring the
+            website configuration for the bucket. This is an instance of
+            ``RedirectLocation``.
+
+        :type routing_rules: :class:`boto.s3.website.RoutingRules`
+        :param routing_rules: Object which specifies conditions
+            and redirects that apply when the conditions are met.
+
+        """
+        config = website.WebsiteConfiguration(
+                suffix, error_key, redirect_all_requests_to,
+                routing_rules)
+        return self.set_website_configuration(config, headers=headers)
+
+    def set_website_configuration(self, config, headers=None):
+        """
+        :type config: boto.s3.website.WebsiteConfiguration
+        :param config: Configuration data
+        """
+        return self.set_website_configuration_xml(config.to_xml(),
+          headers=headers)
+
+
+    def set_website_configuration_xml(self, xml, headers=None):
+        """Upload xml website configuration"""
+        response = self.connection.make_request('PUT', self.name, data=xml,
+                                                query_args='website',
+                                                headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_website_configuration(self, headers=None):
+        """
+        Returns the current status of website configuration on the bucket.
+
+        :rtype: dict
+        :returns: A dictionary containing a Python representation
+            of the XML response from S3. The overall structure is:
+
+        * WebsiteConfiguration
+
+          * IndexDocument
+
+            * Suffix : suffix that is appended to request that
+              is for a "directory" on the website endpoint
+            * ErrorDocument
+
+              * Key : name of object to serve when an error occurs
+
+        """
+        return self.get_website_configuration_with_xml(headers)[0]
+
+    def get_website_configuration_obj(self, headers=None):
+        """Get the website configuration as a
+        :class:`boto.s3.website.WebsiteConfiguration` object.
+        """
+        config_xml = self.get_website_configuration_xml(headers=headers)
+        config = website.WebsiteConfiguration()
+        h = handler.XmlHandler(config, self)
+        xml.sax.parseString(config_xml, h)
+        return config
+
+    def get_website_configuration_with_xml(self, headers=None):
+        """
+        Returns the current status of website configuration on the bucket as
+        unparsed XML.
+
+        :rtype: 2-Tuple
+        :returns: 2-tuple containing:
+
+            1) A dictionary containing a Python representation \
+                of the XML response. The overall structure is:
+
+              * WebsiteConfiguration
+
+                * IndexDocument
+
+                  * Suffix : suffix that is appended to request that \
+                    is for a "directory" on the website endpoint
+
+                  * ErrorDocument
+
+                    * Key : name of object to serve when an error occurs
+
+
+            2) unparsed XML describing the bucket's website configuration
+
+        """
+
+        body = self.get_website_configuration_xml(headers=headers)
+        e = boto.jsonresponse.Element()
+        h = boto.jsonresponse.XmlHandler(e, None)
+        h.parse(body)
+        return e, body
+
+    def get_website_configuration_xml(self, headers=None):
+        """Get raw website configuration xml"""
+        response = self.connection.make_request('GET', self.name,
+                query_args='website', headers=headers)
+        body = response.read().decode('utf-8')
+        boto.log.debug(body)
+
+        if response.status != 200:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+        return body
+
+    def delete_website_configuration(self, headers=None):
+        """
+        Removes all website configuration from the bucket.
+        """
+        response = self.connection.make_request('DELETE', self.name,
+                query_args='website', headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 204:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_website_endpoint(self):
+        """
+        Returns the fully qualified hostname to use is you want to access this
+        bucket as a website.  This doesn't validate whether the bucket has
+        been correctly configured as a website or not.
+        """
+        l = [self.name]
+        l.append(S3WebsiteEndpointTranslate.translate_region(self.get_location()))
+        l.append('.'.join(self.connection.host.split('.')[-2:]))
+        return '.'.join(l)
+
+    def get_policy(self, headers=None):
+        """
+        Returns the JSON policy associated with the bucket.  The policy
+        is returned as an uninterpreted JSON string.
+        """
+        response = self.connection.make_request('GET', self.name,
+                query_args='policy', headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return body
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_policy(self, policy, headers=None):
+        """
+        Add or replace the JSON policy associated with the bucket.
+
+        :type policy: str
+        :param policy: The JSON policy as a string.
+        """
+        response = self.connection.make_request('PUT', self.name,
+                                                data=policy,
+                                                query_args='policy',
+                                                headers=headers)
+        body = response.read()
+        if response.status >= 200 and response.status <= 204:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def delete_policy(self, headers=None):
+        response = self.connection.make_request('DELETE', self.name,
+                                                data='/?policy',
+                                                query_args='policy',
+                                                headers=headers)
+        body = response.read()
+        if response.status >= 200 and response.status <= 204:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_cors_xml(self, cors_xml, headers=None):
+        """
+        Set the CORS (Cross-Origin Resource Sharing) for a bucket.
+
+        :type cors_xml: str
+        :param cors_xml: The XML document describing your desired
+            CORS configuration.  See the S3 documentation for details
+            of the exact syntax required.
+        """
+        fp = StringIO(cors_xml)
+        md5 = boto.utils.compute_md5(fp)
+        if headers is None:
+            headers = {}
+        headers['Content-MD5'] = md5[1]
+        headers['Content-Type'] = 'text/xml'
+        response = self.connection.make_request('PUT', self.name,
+                                                data=fp.getvalue(),
+                                                query_args='cors',
+                                                headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_cors(self, cors_config, headers=None):
+        """
+        Set the CORS for this bucket given a boto CORSConfiguration
+        object.
+
+        :type cors_config: :class:`boto.s3.cors.CORSConfiguration`
+        :param cors_config: The CORS configuration you want
+            to configure for this bucket.
+        """
+        return self.set_cors_xml(cors_config.to_xml())
+
+    def get_cors_xml(self, headers=None):
+        """
+        Returns the current CORS configuration on the bucket as an
+        XML document.
+        """
+        response = self.connection.make_request('GET', self.name,
+                query_args='cors', headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 200:
+            return body
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def get_cors(self, headers=None):
+        """
+        Returns the current CORS configuration on the bucket.
+
+        :rtype: :class:`boto.s3.cors.CORSConfiguration`
+        :returns: A CORSConfiguration object that describes all current
+            CORS rules in effect for the bucket.
+        """
+        body = self.get_cors_xml(headers)
+        cors = CORSConfiguration()
+        h = handler.XmlHandler(cors, self)
+        xml.sax.parseString(body, h)
+        return cors
+
+    def delete_cors(self, headers=None):
+        """
+        Removes all CORS configuration from the bucket.
+        """
+        response = self.connection.make_request('DELETE', self.name,
+                                                query_args='cors',
+                                                headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 204:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def initiate_multipart_upload(self, key_name, headers=None,
+                                  reduced_redundancy=False,
+                                  metadata=None, encrypt_key=False,
+                                  policy=None):
+        """
+        Start a multipart upload operation.
+
+        .. note::
+
+            Note: After you initiate multipart upload and upload one or more
+            parts, you must either complete or abort multipart upload in order
+            to stop getting charged for storage of the uploaded parts. Only
+            after you either complete or abort multipart upload, Amazon S3
+            frees up the parts storage and stops charging you for the parts
+            storage.
+
+        :type key_name: string
+        :param key_name: The name of the key that will ultimately
+            result from this multipart upload operation.  This will be
+            exactly as the key appears in the bucket after the upload
+            process has been completed.
+
+        :type headers: dict
+        :param headers: Additional HTTP headers to send and store with the
+            resulting key in S3.
+
+        :type reduced_redundancy: boolean
+        :param reduced_redundancy: In multipart uploads, the storage
+            class is specified when initiating the upload, not when
+            uploading individual parts.  So if you want the resulting
+            key to use the reduced redundancy storage class set this
+            flag when you initiate the upload.
+
+        :type metadata: dict
+        :param metadata: Any metadata that you would like to set on the key
+            that results from the multipart upload.
+
+        :type encrypt_key: bool
+        :param encrypt_key: If True, the new copy of the object will
+            be encrypted on the server-side by S3 and will be stored
+            in an encrypted form while at rest in S3.
+
+        :type policy: :class:`boto.s3.acl.CannedACLStrings`
+        :param policy: A canned ACL policy that will be applied to the
+            new key (once completed) in S3.
+        """
+        query_args = 'uploads'
+        provider = self.connection.provider
+        headers = headers or {}
+        if policy:
+            headers[provider.acl_header] = policy
+        if reduced_redundancy:
+            storage_class_header = provider.storage_class_header
+            if storage_class_header:
+                headers[storage_class_header] = 'REDUCED_REDUNDANCY'
+            # TODO: what if the provider doesn't support reduced redundancy?
+            # (see boto.s3.key.Key.set_contents_from_file)
+        if encrypt_key:
+            headers[provider.server_side_encryption_header] = 'AES256'
+        if metadata is None:
+            metadata = {}
+
+        headers = boto.utils.merge_meta(headers, metadata,
+                self.connection.provider)
+        response = self.connection.make_request('POST', self.name, key_name,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 200:
+            resp = MultiPartUpload(self)
+            h = handler.XmlHandler(resp, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            return resp
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def complete_multipart_upload(self, key_name, upload_id,
+                                  xml_body, headers=None):
+        """
+        Complete a multipart upload operation.
+        """
+        query_args = 'uploadId=%s' % upload_id
+        if headers is None:
+            headers = {}
+        headers['Content-Type'] = 'text/xml'
+        response = self.connection.make_request('POST', self.name, key_name,
+                                                query_args=query_args,
+                                                headers=headers, data=xml_body)
+        contains_error = False
+        body = response.read().decode('utf-8')
+        # Some errors will be reported in the body of the response
+        # even though the HTTP response code is 200.  This check
+        # does a quick and dirty peek in the body for an error element.
+        if body.find('<Error>') > 0:
+            contains_error = True
+        boto.log.debug(body)
+        if response.status == 200 and not contains_error:
+            resp = CompleteMultiPartUpload(self)
+            h = handler.XmlHandler(resp, self)
+            if not isinstance(body, bytes):
+                body = body.encode('utf-8')
+            xml.sax.parseString(body, h)
+            # Use a dummy key to parse various response headers
+            # for versioning, encryption info and then explicitly
+            # set the completed MPU object values from key.
+            k = self.key_class(self)
+            k.handle_version_headers(response)
+            k.handle_encryption_headers(response)
+            resp.version_id = k.version_id
+            resp.encrypted = k.encrypted
+            return resp
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def cancel_multipart_upload(self, key_name, upload_id, headers=None):
+        """
+        To verify that all parts have been removed, so you don't get charged
+        for the part storage, you should call the List Parts operation and
+        ensure the parts list is empty.
+        """
+        query_args = 'uploadId=%s' % upload_id
+        response = self.connection.make_request('DELETE', self.name, key_name,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status != 204:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def delete(self, headers=None):
+        return self.connection.delete_bucket(self.name, headers=headers)
+
+    def get_tags(self, headers=None):
+        response = self.get_xml_tags(headers)
+        tags = Tags()
+        h = handler.XmlHandler(tags, self)
+        if not isinstance(response, bytes):
+            response = response.encode('utf-8')
+        xml.sax.parseString(response, h)
+        return tags
+
+    def get_xml_tags(self, headers=None):
+        response = self.connection.make_request('GET', self.name,
+                                                query_args='tagging',
+                                                headers=headers)
+        body = response.read()
+        if response.status == 200:
+            return body
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+
+    def set_xml_tags(self, tag_str, headers=None, query_args='tagging'):
+        if headers is None:
+            headers = {}
+        md5 = boto.utils.compute_md5(StringIO(tag_str))
+        headers['Content-MD5'] = md5[1]
+        headers['Content-Type'] = 'text/xml'
+        if not isinstance(tag_str, bytes):
+            tag_str = tag_str.encode('utf-8')
+        response = self.connection.make_request('PUT', self.name,
+                                                data=tag_str,
+                                                query_args=query_args,
+                                                headers=headers)
+        body = response.read()
+        if response.status != 204:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)
+        return True
+
+    def set_tags(self, tags, headers=None):
+        return self.set_xml_tags(tags.to_xml(), headers=headers)
+
+    def delete_tags(self, headers=None):
+        response = self.connection.make_request('DELETE', self.name,
+                                                query_args='tagging',
+                                                headers=headers)
+        body = response.read()
+        boto.log.debug(body)
+        if response.status == 204:
+            return True
+        else:
+            raise self.connection.provider.storage_response_error(
+                response.status, response.reason, body)