Index: third_party/gsutil/boto/boto/s3/bucket.py |
diff --git a/third_party/gsutil/boto/boto/s3/bucket.py b/third_party/gsutil/boto/boto/s3/bucket.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c36a4079a150c7559a85d8a32c79df1eecf47e42 |
--- /dev/null |
+++ b/third_party/gsutil/boto/boto/s3/bucket.py |
@@ -0,0 +1,1634 @@ |
+# 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 StringIO |
+import urllib |
+import re |
+import base64 |
+from collections import defaultdict |
+ |
+# as per http://goo.gl/BDuud (02/19/2011) |
+ |
+ |
+class S3WebsiteEndpointTranslate: |
+ |
+ trans_region = defaultdict(lambda: 's3-website-us-east-1') |
+ trans_region['eu-west-1'] = 's3-website-eu-west-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' |
+ |
+ @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>""" |
+ |
+ WebsiteBody = """<?xml version="1.0" encoding="UTF-8"?> |
+ <WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> |
+ <IndexDocument><Suffix>%s</Suffix></IndexDocument> |
+ %s |
+ </WebsiteConfiguration>""" |
+ |
+ WebsiteErrorFragment = """<ErrorDocument><Key>%s</Key></ErrorDocument>""" |
+ |
+ 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): |
+ """ |
+ Check to see if a particular key exists within the bucket. This |
+ method uses a HEAD request to check for the existance of the key. |
+ Returns: An instance of a Key object or None |
+ |
+ :type key_name: string |
+ :param key_name: The name of the key to retrieve |
+ |
+ :type response_headers: dict |
+ :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. |
+ |
+ :rtype: :class:`boto.s3.key.Key` |
+ :returns: A Key object from this bucket. |
+ """ |
+ query_args_l = [] |
+ if version_id: |
+ query_args_l.append('versionId=%s' % version_id) |
+ if response_headers: |
+ for rk, rv in response_headers.iteritems(): |
+ query_args_l.append('%s=%s' % (rk, urllib.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) |
+ k.etag = response.getheader('etag') |
+ k.content_type = response.getheader('content-type') |
+ k.content_encoding = response.getheader('content-encoding') |
+ k.content_disposition = response.getheader('content-disposition') |
+ k.content_language = response.getheader('content-language') |
+ k.last_modified = response.getheader('last-modified') |
+ # 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.cache_control = response.getheader('cache-control') |
+ k.name = key_name |
+ k.handle_version_headers(response) |
+ k.handle_encryption_headers(response) |
+ k.handle_restore_headers(response) |
+ 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): |
+ """ |
+ 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 |
+ |
+ :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet` |
+ :return: an instance of a BucketListResultSet that handles paging, etc |
+ """ |
+ return BucketListResultSet(self, prefix, delimiter, marker, headers) |
+ |
+ def list_versions(self, prefix='', delimiter='', key_marker='', |
+ version_id_marker='', headers=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 marker: string |
+ :param marker: The "marker" of where you are in the result set |
+ |
+ :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) |
+ |
+ def list_multipart_uploads(self, key_marker='', |
+ upload_id_marker='', |
+ headers=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 marker: string |
+ :param marker: The "marker" of where you are in the result set |
+ |
+ :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet` |
+ :return: an instance of a BucketListResultSet that handles paging, etc |
+ """ |
+ return MultiPartUploadListResultSet(self, key_marker, |
+ upload_id_marker, |
+ headers) |
+ |
+ def _get_all(self, element_map, initial_query_string='', |
+ headers=None, **params): |
+ l = [] |
+ for k, v in params.items(): |
+ k = k.replace('_', '-') |
+ if k == 'maxkeys': |
+ k = 'max-keys' |
+ if isinstance(v, unicode): |
+ v = v.encode('utf-8') |
+ if v is not None and v != '': |
+ l.append('%s=%s' % (urllib.quote(k), urllib.quote(str(v)))) |
+ if len(l): |
+ s = initial_query_string + '&' + '&'.join(l) |
+ else: |
+ s = initial_query_string |
+ response = self.connection.make_request('GET', self.name, |
+ headers=headers, |
+ query_args=s) |
+ body = response.read() |
+ boto.log.debug(body) |
+ if response.status == 200: |
+ rs = ResultSet(element_map) |
+ h = handler.XmlHandler(rs, self) |
+ xml.sax.parseString(body, h) |
+ return rs |
+ else: |
+ raise self.connection.provider.storage_response_error( |
+ response.status, response.reason, body) |
+ |
+ 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. |
+ |
+ :rtype: ResultSet |
+ :return: The result from S3 listing the keys requested |
+ |
+ """ |
+ 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. |
+ |
+ :rtype: ResultSet |
+ :return: The result from S3 listing the keys requested |
+ """ |
+ return self._get_all([('Version', self.key_class), |
+ ('CommonPrefixes', Prefix), |
+ ('DeleteMarker', DeleteMarker)], |
+ 'versions', headers, **params) |
+ |
+ 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. |
+ |
+ :rtype: ResultSet |
+ :return: The result from S3 listing the uploads requested |
+ |
+ """ |
+ 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 = ikeys.next() |
+ except StopIteration: |
+ break |
+ if isinstance(key, basestring): |
+ 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 = StringIO.StringIO(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) |
+ 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. |
+ """ |
+ 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) |
+ 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) |
+ acl = src_bucket.get_xml_acl(src_key_name) |
+ if encrypt_key: |
+ headers[provider.server_side_encryption_header] = 'AES256' |
+ src = '%s/%s' % (src_bucket_name, urllib.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) |
+ xml.sax.parseString(body, h) |
+ if hasattr(key, 'Error'): |
+ raise provider.storage_copy_error(key.Code, key.Message, body) |
+ key.handle_version_headers(response) |
+ 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 |
+ response = self.connection.make_request('PUT', self.name, key_name, |
+ data=acl_str.encode('UTF-8'), |
+ 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) |
+ 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 |
+ response = self.connection.make_request('PUT', self.name, key_name, |
+ data=value.encode('UTF-8'), |
+ 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): |
+ """ |
+ 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, |
+ query_args='location') |
+ body = response.read() |
+ if response.status == 200: |
+ rs = ResultSet(self) |
+ h = handler.XmlHandler(rs, self) |
+ 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.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) |
+ 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() |
+ 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.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) |
+ 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) |
+ body = config.to_xml() |
+ response = self.connection.make_request('PUT', self.name, data=body, |
+ 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_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. |
+ """ |
+ response = self.connection.make_request('GET', self.name, |
+ query_args='website', headers=headers) |
+ body = response.read() |
+ boto.log.debug(body) |
+ |
+ if response.status != 200: |
+ raise self.connection.provider.storage_response_error( |
+ response.status, response.reason, body) |
+ |
+ e = boto.jsonresponse.Element() |
+ h = boto.jsonresponse.XmlHandler(e, None) |
+ h.parse(body) |
+ return e, 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.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. |
+ |
+ :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) |
+ 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() |
+ # 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) |
+ 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): |
+ 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): |
+ response = self.get_xml_tags() |
+ tags = Tags() |
+ h = handler.XmlHandler(tags, self) |
+ xml.sax.parseString(response, h) |
+ return tags |
+ |
+ def get_xml_tags(self): |
+ response = self.connection.make_request('GET', self.name, |
+ query_args='tagging', |
+ headers=None) |
+ 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.StringIO(tag_str)) |
+ headers['Content-MD5'] = md5[1] |
+ headers['Content-Type'] = 'text/xml' |
+ response = self.connection.make_request('PUT', self.name, |
+ data=tag_str.encode('utf-8'), |
+ 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) |