| # 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) |