1# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/ 2# Copyright (c) 2010, Eucalyptus Systems, Inc. 3# All rights reserved. 4# 5# Permission is hereby granted, free of charge, to any person obtaining a 6# copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, dis- 9# tribute, sublicense, and/or sell copies of the Software, and to permit 10# persons to whom the Software is furnished to do so, subject to the fol- 11# lowing conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22# IN THE SOFTWARE. 23 24import boto 25from boto import handler 26from boto.resultset import ResultSet 27from boto.exception import BotoClientError 28from boto.s3.acl import Policy, CannedACLStrings, Grant 29from boto.s3.key import Key 30from boto.s3.prefix import Prefix 31from boto.s3.deletemarker import DeleteMarker 32from boto.s3.multipart import MultiPartUpload 33from boto.s3.multipart import CompleteMultiPartUpload 34from boto.s3.multidelete import MultiDeleteResult 35from boto.s3.multidelete import Error 36from boto.s3.bucketlistresultset import BucketListResultSet 37from boto.s3.bucketlistresultset import VersionedBucketListResultSet 38from boto.s3.bucketlistresultset import MultiPartUploadListResultSet 39from boto.s3.lifecycle import Lifecycle 40from boto.s3.tagging import Tags 41from boto.s3.cors import CORSConfiguration 42from boto.s3.bucketlogging import BucketLogging 43from boto.s3 import website 44import boto.jsonresponse 45import boto.utils 46import xml.sax 47import xml.sax.saxutils 48import re 49import base64 50from collections import defaultdict 51from boto.compat import BytesIO, six, StringIO, urllib 52 53# as per http://goo.gl/BDuud (02/19/2011) 54 55 56class S3WebsiteEndpointTranslate(object): 57 58 trans_region = defaultdict(lambda: 's3-website-us-east-1') 59 trans_region['eu-west-1'] = 's3-website-eu-west-1' 60 trans_region['us-west-1'] = 's3-website-us-west-1' 61 trans_region['us-west-2'] = 's3-website-us-west-2' 62 trans_region['sa-east-1'] = 's3-website-sa-east-1' 63 trans_region['ap-northeast-1'] = 's3-website-ap-northeast-1' 64 trans_region['ap-southeast-1'] = 's3-website-ap-southeast-1' 65 trans_region['ap-southeast-2'] = 's3-website-ap-southeast-2' 66 trans_region['cn-north-1'] = 's3-website.cn-north-1' 67 68 @classmethod 69 def translate_region(self, reg): 70 return self.trans_region[reg] 71 72S3Permissions = ['READ', 'WRITE', 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL'] 73 74 75class Bucket(object): 76 77 LoggingGroup = 'http://acs.amazonaws.com/groups/s3/LogDelivery' 78 79 BucketPaymentBody = """<?xml version="1.0" encoding="UTF-8"?> 80 <RequestPaymentConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> 81 <Payer>%s</Payer> 82 </RequestPaymentConfiguration>""" 83 84 VersioningBody = """<?xml version="1.0" encoding="UTF-8"?> 85 <VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> 86 <Status>%s</Status> 87 <MfaDelete>%s</MfaDelete> 88 </VersioningConfiguration>""" 89 90 VersionRE = '<Status>([A-Za-z]+)</Status>' 91 MFADeleteRE = '<MfaDelete>([A-Za-z]+)</MfaDelete>' 92 93 def __init__(self, connection=None, name=None, key_class=Key): 94 self.name = name 95 self.connection = connection 96 self.key_class = key_class 97 98 def __repr__(self): 99 return '<Bucket: %s>' % self.name 100 101 def __iter__(self): 102 return iter(BucketListResultSet(self)) 103 104 def __contains__(self, key_name): 105 return not (self.get_key(key_name) is None) 106 107 def startElement(self, name, attrs, connection): 108 return None 109 110 def endElement(self, name, value, connection): 111 if name == 'Name': 112 self.name = value 113 elif name == 'CreationDate': 114 self.creation_date = value 115 else: 116 setattr(self, name, value) 117 118 def set_key_class(self, key_class): 119 """ 120 Set the Key class associated with this bucket. By default, this 121 would be the boto.s3.key.Key class but if you want to subclass that 122 for some reason this allows you to associate your new class with a 123 bucket so that when you call bucket.new_key() or when you get a listing 124 of keys in the bucket you will get an instances of your key class 125 rather than the default. 126 127 :type key_class: class 128 :param key_class: A subclass of Key that can be more specific 129 """ 130 self.key_class = key_class 131 132 def lookup(self, key_name, headers=None): 133 """ 134 Deprecated: Please use get_key method. 135 136 :type key_name: string 137 :param key_name: The name of the key to retrieve 138 139 :rtype: :class:`boto.s3.key.Key` 140 :returns: A Key object from this bucket. 141 """ 142 return self.get_key(key_name, headers=headers) 143 144 def get_key(self, key_name, headers=None, version_id=None, 145 response_headers=None, validate=True): 146 """ 147 Check to see if a particular key exists within the bucket. This 148 method uses a HEAD request to check for the existence of the key. 149 Returns: An instance of a Key object or None 150 151 :param key_name: The name of the key to retrieve 152 :type key_name: string 153 154 :param headers: The headers to send when retrieving the key 155 :type headers: dict 156 157 :param version_id: 158 :type version_id: string 159 160 :param response_headers: A dictionary containing HTTP 161 headers/values that will override any headers associated 162 with the stored object in the response. See 163 http://goo.gl/EWOPb for details. 164 :type response_headers: dict 165 166 :param validate: Verifies whether the key exists. If ``False``, this 167 will not hit the service, constructing an in-memory object. 168 Default is ``True``. 169 :type validate: bool 170 171 :rtype: :class:`boto.s3.key.Key` 172 :returns: A Key object from this bucket. 173 """ 174 if validate is False: 175 if headers or version_id or response_headers: 176 raise BotoClientError( 177 "When providing 'validate=False', no other params " + \ 178 "are allowed." 179 ) 180 181 # This leans on the default behavior of ``new_key`` (not hitting 182 # the service). If that changes, that behavior should migrate here. 183 return self.new_key(key_name) 184 185 query_args_l = [] 186 if version_id: 187 query_args_l.append('versionId=%s' % version_id) 188 if response_headers: 189 for rk, rv in six.iteritems(response_headers): 190 query_args_l.append('%s=%s' % (rk, urllib.parse.quote(rv))) 191 192 key, resp = self._get_key_internal(key_name, headers, query_args_l) 193 return key 194 195 def _get_key_internal(self, key_name, headers, query_args_l): 196 query_args = '&'.join(query_args_l) or None 197 response = self.connection.make_request('HEAD', self.name, key_name, 198 headers=headers, 199 query_args=query_args) 200 response.read() 201 # Allow any success status (2xx) - for example this lets us 202 # support Range gets, which return status 206: 203 if response.status / 100 == 2: 204 k = self.key_class(self) 205 provider = self.connection.provider 206 k.metadata = boto.utils.get_aws_metadata(response.msg, provider) 207 for field in Key.base_fields: 208 k.__dict__[field.lower().replace('-', '_')] = \ 209 response.getheader(field) 210 # the following machinations are a workaround to the fact that 211 # apache/fastcgi omits the content-length header on HEAD 212 # requests when the content-length is zero. 213 # See http://goo.gl/0Tdax for more details. 214 clen = response.getheader('content-length') 215 if clen: 216 k.size = int(response.getheader('content-length')) 217 else: 218 k.size = 0 219 k.name = key_name 220 k.handle_version_headers(response) 221 k.handle_encryption_headers(response) 222 k.handle_restore_headers(response) 223 k.handle_addl_headers(response.getheaders()) 224 return k, response 225 else: 226 if response.status == 404: 227 return None, response 228 else: 229 raise self.connection.provider.storage_response_error( 230 response.status, response.reason, '') 231 232 def list(self, prefix='', delimiter='', marker='', headers=None, 233 encoding_type=None): 234 """ 235 List key objects within a bucket. This returns an instance of an 236 BucketListResultSet that automatically handles all of the result 237 paging, etc. from S3. You just need to keep iterating until 238 there are no more results. 239 240 Called with no arguments, this will return an iterator object across 241 all keys within the bucket. 242 243 The Key objects returned by the iterator are obtained by parsing 244 the results of a GET on the bucket, also known as the List Objects 245 request. The XML returned by this request contains only a subset 246 of the information about each key. Certain metadata fields such 247 as Content-Type and user metadata are not available in the XML. 248 Therefore, if you want these additional metadata fields you will 249 have to do a HEAD request on the Key in the bucket. 250 251 :type prefix: string 252 :param prefix: allows you to limit the listing to a particular 253 prefix. For example, if you call the method with 254 prefix='/foo/' then the iterator will only cycle through 255 the keys that begin with the string '/foo/'. 256 257 :type delimiter: string 258 :param delimiter: can be used in conjunction with the prefix 259 to allow you to organize and browse your keys 260 hierarchically. See http://goo.gl/Xx63h for more details. 261 262 :type marker: string 263 :param marker: The "marker" of where you are in the result set 264 265 :param encoding_type: Requests Amazon S3 to encode the response and 266 specifies the encoding method to use. 267 268 An object key can contain any Unicode character; however, XML 1.0 269 parser cannot parse some characters, such as characters with an 270 ASCII value from 0 to 10. For characters that are not supported in 271 XML 1.0, you can add this parameter to request that Amazon S3 272 encode the keys in the response. 273 274 Valid options: ``url`` 275 :type encoding_type: string 276 277 :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet` 278 :return: an instance of a BucketListResultSet that handles paging, etc 279 """ 280 return BucketListResultSet(self, prefix, delimiter, marker, headers, 281 encoding_type=encoding_type) 282 283 def list_versions(self, prefix='', delimiter='', key_marker='', 284 version_id_marker='', headers=None, encoding_type=None): 285 """ 286 List version objects within a bucket. This returns an 287 instance of an VersionedBucketListResultSet that automatically 288 handles all of the result paging, etc. from S3. You just need 289 to keep iterating until there are no more results. Called 290 with no arguments, this will return an iterator object across 291 all keys within the bucket. 292 293 :type prefix: string 294 :param prefix: allows you to limit the listing to a particular 295 prefix. For example, if you call the method with 296 prefix='/foo/' then the iterator will only cycle through 297 the keys that begin with the string '/foo/'. 298 299 :type delimiter: string 300 :param delimiter: can be used in conjunction with the prefix 301 to allow you to organize and browse your keys 302 hierarchically. See: 303 304 http://aws.amazon.com/releasenotes/Amazon-S3/213 305 306 for more details. 307 308 :type key_marker: string 309 :param key_marker: The "marker" of where you are in the result set 310 311 :param encoding_type: Requests Amazon S3 to encode the response and 312 specifies the encoding method to use. 313 314 An object key can contain any Unicode character; however, XML 1.0 315 parser cannot parse some characters, such as characters with an 316 ASCII value from 0 to 10. For characters that are not supported in 317 XML 1.0, you can add this parameter to request that Amazon S3 318 encode the keys in the response. 319 320 Valid options: ``url`` 321 :type encoding_type: string 322 323 :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet` 324 :return: an instance of a BucketListResultSet that handles paging, etc 325 """ 326 return VersionedBucketListResultSet(self, prefix, delimiter, 327 key_marker, version_id_marker, 328 headers, 329 encoding_type=encoding_type) 330 331 def list_multipart_uploads(self, key_marker='', 332 upload_id_marker='', 333 headers=None, encoding_type=None): 334 """ 335 List multipart upload objects within a bucket. This returns an 336 instance of an MultiPartUploadListResultSet that automatically 337 handles all of the result paging, etc. from S3. You just need 338 to keep iterating until there are no more results. 339 340 :type key_marker: string 341 :param key_marker: The "marker" of where you are in the result set 342 343 :type upload_id_marker: string 344 :param upload_id_marker: The upload identifier 345 346 :param encoding_type: Requests Amazon S3 to encode the response and 347 specifies the encoding method to use. 348 349 An object key can contain any Unicode character; however, XML 1.0 350 parser cannot parse some characters, such as characters with an 351 ASCII value from 0 to 10. For characters that are not supported in 352 XML 1.0, you can add this parameter to request that Amazon S3 353 encode the keys in the response. 354 355 Valid options: ``url`` 356 :type encoding_type: string 357 358 :rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet` 359 :return: an instance of a BucketListResultSet that handles paging, etc 360 """ 361 return MultiPartUploadListResultSet(self, key_marker, 362 upload_id_marker, 363 headers, 364 encoding_type=encoding_type) 365 366 def _get_all_query_args(self, params, initial_query_string=''): 367 pairs = [] 368 369 if initial_query_string: 370 pairs.append(initial_query_string) 371 372 for key, value in sorted(params.items(), key=lambda x: x[0]): 373 if value is None: 374 continue 375 key = key.replace('_', '-') 376 if key == 'maxkeys': 377 key = 'max-keys' 378 if not isinstance(value, six.string_types + (six.binary_type,)): 379 value = six.text_type(value) 380 if not isinstance(value, six.binary_type): 381 value = value.encode('utf-8') 382 if value: 383 pairs.append(u'%s=%s' % ( 384 urllib.parse.quote(key), 385 urllib.parse.quote(value) 386 )) 387 388 return '&'.join(pairs) 389 390 def _get_all(self, element_map, initial_query_string='', 391 headers=None, **params): 392 query_args = self._get_all_query_args( 393 params, 394 initial_query_string=initial_query_string 395 ) 396 response = self.connection.make_request('GET', self.name, 397 headers=headers, 398 query_args=query_args) 399 body = response.read() 400 boto.log.debug(body) 401 if response.status == 200: 402 rs = ResultSet(element_map) 403 h = handler.XmlHandler(rs, self) 404 if not isinstance(body, bytes): 405 body = body.encode('utf-8') 406 xml.sax.parseString(body, h) 407 return rs 408 else: 409 raise self.connection.provider.storage_response_error( 410 response.status, response.reason, body) 411 412 def validate_kwarg_names(self, kwargs, names): 413 """ 414 Checks that all named arguments are in the specified list of names. 415 416 :type kwargs: dict 417 :param kwargs: Dictionary of kwargs to validate. 418 419 :type names: list 420 :param names: List of possible named arguments. 421 """ 422 for kwarg in kwargs: 423 if kwarg not in names: 424 raise TypeError('Invalid argument "%s"!' % kwarg) 425 426 def get_all_keys(self, headers=None, **params): 427 """ 428 A lower-level method for listing contents of a bucket. This 429 closely models the actual S3 API and requires you to manually 430 handle the paging of results. For a higher-level method that 431 handles the details of paging for you, you can use the list 432 method. 433 434 :type max_keys: int 435 :param max_keys: The maximum number of keys to retrieve 436 437 :type prefix: string 438 :param prefix: The prefix of the keys you want to retrieve 439 440 :type marker: string 441 :param marker: The "marker" of where you are in the result set 442 443 :type delimiter: string 444 :param delimiter: If this optional, Unicode string parameter 445 is included with your request, then keys that contain the 446 same string between the prefix and the first occurrence of 447 the delimiter will be rolled up into a single result 448 element in the CommonPrefixes collection. These rolled-up 449 keys are not returned elsewhere in the response. 450 451 :param encoding_type: Requests Amazon S3 to encode the response and 452 specifies the encoding method to use. 453 454 An object key can contain any Unicode character; however, XML 1.0 455 parser cannot parse some characters, such as characters with an 456 ASCII value from 0 to 10. For characters that are not supported in 457 XML 1.0, you can add this parameter to request that Amazon S3 458 encode the keys in the response. 459 460 Valid options: ``url`` 461 :type encoding_type: string 462 463 :rtype: ResultSet 464 :return: The result from S3 listing the keys requested 465 466 """ 467 self.validate_kwarg_names(params, ['maxkeys', 'max_keys', 'prefix', 468 'marker', 'delimiter', 469 'encoding_type']) 470 return self._get_all([('Contents', self.key_class), 471 ('CommonPrefixes', Prefix)], 472 '', headers, **params) 473 474 def get_all_versions(self, headers=None, **params): 475 """ 476 A lower-level, version-aware method for listing contents of a 477 bucket. This closely models the actual S3 API and requires 478 you to manually handle the paging of results. For a 479 higher-level method that handles the details of paging for 480 you, you can use the list method. 481 482 :type max_keys: int 483 :param max_keys: The maximum number of keys to retrieve 484 485 :type prefix: string 486 :param prefix: The prefix of the keys you want to retrieve 487 488 :type key_marker: string 489 :param key_marker: The "marker" of where you are in the result set 490 with respect to keys. 491 492 :type version_id_marker: string 493 :param version_id_marker: The "marker" of where you are in the result 494 set with respect to version-id's. 495 496 :type delimiter: string 497 :param delimiter: If this optional, Unicode string parameter 498 is included with your request, then keys that contain the 499 same string between the prefix and the first occurrence of 500 the delimiter will be rolled up into a single result 501 element in the CommonPrefixes collection. These rolled-up 502 keys are not returned elsewhere in the response. 503 504 :param encoding_type: Requests Amazon S3 to encode the response and 505 specifies the encoding method to use. 506 507 An object key can contain any Unicode character; however, XML 1.0 508 parser cannot parse some characters, such as characters with an 509 ASCII value from 0 to 10. For characters that are not supported in 510 XML 1.0, you can add this parameter to request that Amazon S3 511 encode the keys in the response. 512 513 Valid options: ``url`` 514 :type encoding_type: string 515 516 :rtype: ResultSet 517 :return: The result from S3 listing the keys requested 518 """ 519 self.validate_get_all_versions_params(params) 520 return self._get_all([('Version', self.key_class), 521 ('CommonPrefixes', Prefix), 522 ('DeleteMarker', DeleteMarker)], 523 'versions', headers, **params) 524 525 def validate_get_all_versions_params(self, params): 526 """ 527 Validate that the parameters passed to get_all_versions are valid. 528 Overridden by subclasses that allow a different set of parameters. 529 530 :type params: dict 531 :param params: Parameters to validate. 532 """ 533 self.validate_kwarg_names( 534 params, ['maxkeys', 'max_keys', 'prefix', 'key_marker', 535 'version_id_marker', 'delimiter', 'encoding_type']) 536 537 def get_all_multipart_uploads(self, headers=None, **params): 538 """ 539 A lower-level, version-aware method for listing active 540 MultiPart uploads for a bucket. This closely models the 541 actual S3 API and requires you to manually handle the paging 542 of results. For a higher-level method that handles the 543 details of paging for you, you can use the list method. 544 545 :type max_uploads: int 546 :param max_uploads: The maximum number of uploads to retrieve. 547 Default value is 1000. 548 549 :type key_marker: string 550 :param key_marker: Together with upload_id_marker, this 551 parameter specifies the multipart upload after which 552 listing should begin. If upload_id_marker is not 553 specified, only the keys lexicographically greater than 554 the specified key_marker will be included in the list. 555 556 If upload_id_marker is specified, any multipart uploads 557 for a key equal to the key_marker might also be included, 558 provided those multipart uploads have upload IDs 559 lexicographically greater than the specified 560 upload_id_marker. 561 562 :type upload_id_marker: string 563 :param upload_id_marker: Together with key-marker, specifies 564 the multipart upload after which listing should begin. If 565 key_marker is not specified, the upload_id_marker 566 parameter is ignored. Otherwise, any multipart uploads 567 for a key equal to the key_marker might be included in the 568 list only if they have an upload ID lexicographically 569 greater than the specified upload_id_marker. 570 571 :type encoding_type: string 572 :param encoding_type: Requests Amazon S3 to encode the response and 573 specifies the encoding method to use. 574 575 An object key can contain any Unicode character; however, XML 1.0 576 parser cannot parse some characters, such as characters with an 577 ASCII value from 0 to 10. For characters that are not supported in 578 XML 1.0, you can add this parameter to request that Amazon S3 579 encode the keys in the response. 580 581 Valid options: ``url`` 582 583 :type delimiter: string 584 :param delimiter: Character you use to group keys. 585 All keys that contain the same string between the prefix, if 586 specified, and the first occurrence of the delimiter after the 587 prefix are grouped under a single result element, CommonPrefixes. 588 If you don't specify the prefix parameter, then the substring 589 starts at the beginning of the key. The keys that are grouped 590 under CommonPrefixes result element are not returned elsewhere 591 in the response. 592 593 :type prefix: string 594 :param prefix: Lists in-progress uploads only for those keys that 595 begin with the specified prefix. You can use prefixes to separate 596 a bucket into different grouping of keys. (You can think of using 597 prefix to make groups in the same way you'd use a folder in a 598 file system.) 599 600 :rtype: ResultSet 601 :return: The result from S3 listing the uploads requested 602 603 """ 604 self.validate_kwarg_names(params, ['max_uploads', 'key_marker', 605 'upload_id_marker', 'encoding_type', 606 'delimiter', 'prefix']) 607 return self._get_all([('Upload', MultiPartUpload), 608 ('CommonPrefixes', Prefix)], 609 'uploads', headers, **params) 610 611 def new_key(self, key_name=None): 612 """ 613 Creates a new key 614 615 :type key_name: string 616 :param key_name: The name of the key to create 617 618 :rtype: :class:`boto.s3.key.Key` or subclass 619 :returns: An instance of the newly created key object 620 """ 621 if not key_name: 622 raise ValueError('Empty key names are not allowed') 623 return self.key_class(self, key_name) 624 625 def generate_url(self, expires_in, method='GET', headers=None, 626 force_http=False, response_headers=None, 627 expires_in_absolute=False): 628 return self.connection.generate_url(expires_in, method, self.name, 629 headers=headers, 630 force_http=force_http, 631 response_headers=response_headers, 632 expires_in_absolute=expires_in_absolute) 633 634 def delete_keys(self, keys, quiet=False, mfa_token=None, headers=None): 635 """ 636 Deletes a set of keys using S3's Multi-object delete API. If a 637 VersionID is specified for that key then that version is removed. 638 Returns a MultiDeleteResult Object, which contains Deleted 639 and Error elements for each key you ask to delete. 640 641 :type keys: list 642 :param keys: A list of either key_names or (key_name, versionid) pairs 643 or a list of Key instances. 644 645 :type quiet: boolean 646 :param quiet: In quiet mode the response includes only keys 647 where the delete operation encountered an error. For a 648 successful deletion, the operation does not return any 649 information about the delete in the response body. 650 651 :type mfa_token: tuple or list of strings 652 :param mfa_token: A tuple or list consisting of the serial 653 number from the MFA device and the current value of the 654 six-digit token associated with the device. This value is 655 required anytime you are deleting versioned objects from a 656 bucket that has the MFADelete option on the bucket. 657 658 :returns: An instance of MultiDeleteResult 659 """ 660 ikeys = iter(keys) 661 result = MultiDeleteResult(self) 662 provider = self.connection.provider 663 query_args = 'delete' 664 665 def delete_keys2(hdrs): 666 hdrs = hdrs or {} 667 data = u"""<?xml version="1.0" encoding="UTF-8"?>""" 668 data += u"<Delete>" 669 if quiet: 670 data += u"<Quiet>true</Quiet>" 671 count = 0 672 while count < 1000: 673 try: 674 key = next(ikeys) 675 except StopIteration: 676 break 677 if isinstance(key, six.string_types): 678 key_name = key 679 version_id = None 680 elif isinstance(key, tuple) and len(key) == 2: 681 key_name, version_id = key 682 elif (isinstance(key, Key) or isinstance(key, DeleteMarker)) and key.name: 683 key_name = key.name 684 version_id = key.version_id 685 else: 686 if isinstance(key, Prefix): 687 key_name = key.name 688 code = 'PrefixSkipped' # Don't delete Prefix 689 else: 690 key_name = repr(key) # try get a string 691 code = 'InvalidArgument' # other unknown type 692 message = 'Invalid. No delete action taken for this object.' 693 error = Error(key_name, code=code, message=message) 694 result.errors.append(error) 695 continue 696 count += 1 697 data += u"<Object><Key>%s</Key>" % xml.sax.saxutils.escape(key_name) 698 if version_id: 699 data += u"<VersionId>%s</VersionId>" % version_id 700 data += u"</Object>" 701 data += u"</Delete>" 702 if count <= 0: 703 return False # no more 704 data = data.encode('utf-8') 705 fp = BytesIO(data) 706 md5 = boto.utils.compute_md5(fp) 707 hdrs['Content-MD5'] = md5[1] 708 hdrs['Content-Type'] = 'text/xml' 709 if mfa_token: 710 hdrs[provider.mfa_header] = ' '.join(mfa_token) 711 response = self.connection.make_request('POST', self.name, 712 headers=hdrs, 713 query_args=query_args, 714 data=data) 715 body = response.read() 716 if response.status == 200: 717 h = handler.XmlHandler(result, self) 718 if not isinstance(body, bytes): 719 body = body.encode('utf-8') 720 xml.sax.parseString(body, h) 721 return count >= 1000 # more? 722 else: 723 raise provider.storage_response_error(response.status, 724 response.reason, 725 body) 726 while delete_keys2(headers): 727 pass 728 return result 729 730 def delete_key(self, key_name, headers=None, version_id=None, 731 mfa_token=None): 732 """ 733 Deletes a key from the bucket. If a version_id is provided, 734 only that version of the key will be deleted. 735 736 :type key_name: string 737 :param key_name: The key name to delete 738 739 :type version_id: string 740 :param version_id: The version ID (optional) 741 742 :type mfa_token: tuple or list of strings 743 :param mfa_token: A tuple or list consisting of the serial 744 number from the MFA device and the current value of the 745 six-digit token associated with the device. This value is 746 required anytime you are deleting versioned objects from a 747 bucket that has the MFADelete option on the bucket. 748 749 :rtype: :class:`boto.s3.key.Key` or subclass 750 :returns: A key object holding information on what was 751 deleted. The Caller can see if a delete_marker was 752 created or removed and what version_id the delete created 753 or removed. 754 """ 755 if not key_name: 756 raise ValueError('Empty key names are not allowed') 757 return self._delete_key_internal(key_name, headers=headers, 758 version_id=version_id, 759 mfa_token=mfa_token, 760 query_args_l=None) 761 762 def _delete_key_internal(self, key_name, headers=None, version_id=None, 763 mfa_token=None, query_args_l=None): 764 query_args_l = query_args_l or [] 765 provider = self.connection.provider 766 if version_id: 767 query_args_l.append('versionId=%s' % version_id) 768 query_args = '&'.join(query_args_l) or None 769 if mfa_token: 770 if not headers: 771 headers = {} 772 headers[provider.mfa_header] = ' '.join(mfa_token) 773 response = self.connection.make_request('DELETE', self.name, key_name, 774 headers=headers, 775 query_args=query_args) 776 body = response.read() 777 if response.status != 204: 778 raise provider.storage_response_error(response.status, 779 response.reason, body) 780 else: 781 # return a key object with information on what was deleted. 782 k = self.key_class(self) 783 k.name = key_name 784 k.handle_version_headers(response) 785 k.handle_addl_headers(response.getheaders()) 786 return k 787 788 def copy_key(self, new_key_name, src_bucket_name, 789 src_key_name, metadata=None, src_version_id=None, 790 storage_class='STANDARD', preserve_acl=False, 791 encrypt_key=False, headers=None, query_args=None): 792 """ 793 Create a new key in the bucket by copying another existing key. 794 795 :type new_key_name: string 796 :param new_key_name: The name of the new key 797 798 :type src_bucket_name: string 799 :param src_bucket_name: The name of the source bucket 800 801 :type src_key_name: string 802 :param src_key_name: The name of the source key 803 804 :type src_version_id: string 805 :param src_version_id: The version id for the key. This param 806 is optional. If not specified, the newest version of the 807 key will be copied. 808 809 :type metadata: dict 810 :param metadata: Metadata to be associated with new key. If 811 metadata is supplied, it will replace the metadata of the 812 source key being copied. If no metadata is supplied, the 813 source key's metadata will be copied to the new key. 814 815 :type storage_class: string 816 :param storage_class: The storage class of the new key. By 817 default, the new key will use the standard storage class. 818 Possible values are: STANDARD | REDUCED_REDUNDANCY 819 820 :type preserve_acl: bool 821 :param preserve_acl: If True, the ACL from the source key will 822 be copied to the destination key. If False, the 823 destination key will have the default ACL. Note that 824 preserving the ACL in the new key object will require two 825 additional API calls to S3, one to retrieve the current 826 ACL and one to set that ACL on the new object. If you 827 don't care about the ACL, a value of False will be 828 significantly more efficient. 829 830 :type encrypt_key: bool 831 :param encrypt_key: If True, the new copy of the object will 832 be encrypted on the server-side by S3 and will be stored 833 in an encrypted form while at rest in S3. 834 835 :type headers: dict 836 :param headers: A dictionary of header name/value pairs. 837 838 :type query_args: string 839 :param query_args: A string of additional querystring arguments 840 to append to the request 841 842 :rtype: :class:`boto.s3.key.Key` or subclass 843 :returns: An instance of the newly created key object 844 """ 845 headers = headers or {} 846 provider = self.connection.provider 847 src_key_name = boto.utils.get_utf8_value(src_key_name) 848 if preserve_acl: 849 if self.name == src_bucket_name: 850 src_bucket = self 851 else: 852 src_bucket = self.connection.get_bucket( 853 src_bucket_name, validate=False) 854 acl = src_bucket.get_xml_acl(src_key_name) 855 if encrypt_key: 856 headers[provider.server_side_encryption_header] = 'AES256' 857 src = '%s/%s' % (src_bucket_name, urllib.parse.quote(src_key_name)) 858 if src_version_id: 859 src += '?versionId=%s' % src_version_id 860 headers[provider.copy_source_header] = str(src) 861 # make sure storage_class_header key exists before accessing it 862 if provider.storage_class_header and storage_class: 863 headers[provider.storage_class_header] = storage_class 864 if metadata is not None: 865 headers[provider.metadata_directive_header] = 'REPLACE' 866 headers = boto.utils.merge_meta(headers, metadata, provider) 867 elif not query_args: # Can't use this header with multi-part copy. 868 headers[provider.metadata_directive_header] = 'COPY' 869 response = self.connection.make_request('PUT', self.name, new_key_name, 870 headers=headers, 871 query_args=query_args) 872 body = response.read() 873 if response.status == 200: 874 key = self.new_key(new_key_name) 875 h = handler.XmlHandler(key, self) 876 if not isinstance(body, bytes): 877 body = body.encode('utf-8') 878 xml.sax.parseString(body, h) 879 if hasattr(key, 'Error'): 880 raise provider.storage_copy_error(key.Code, key.Message, body) 881 key.handle_version_headers(response) 882 key.handle_addl_headers(response.getheaders()) 883 if preserve_acl: 884 self.set_xml_acl(acl, new_key_name) 885 return key 886 else: 887 raise provider.storage_response_error(response.status, 888 response.reason, body) 889 890 def set_canned_acl(self, acl_str, key_name='', headers=None, 891 version_id=None): 892 assert acl_str in CannedACLStrings 893 894 if headers: 895 headers[self.connection.provider.acl_header] = acl_str 896 else: 897 headers = {self.connection.provider.acl_header: acl_str} 898 899 query_args = 'acl' 900 if version_id: 901 query_args += '&versionId=%s' % version_id 902 response = self.connection.make_request('PUT', self.name, key_name, 903 headers=headers, query_args=query_args) 904 body = response.read() 905 if response.status != 200: 906 raise self.connection.provider.storage_response_error( 907 response.status, response.reason, body) 908 909 def get_xml_acl(self, key_name='', headers=None, version_id=None): 910 query_args = 'acl' 911 if version_id: 912 query_args += '&versionId=%s' % version_id 913 response = self.connection.make_request('GET', self.name, key_name, 914 query_args=query_args, 915 headers=headers) 916 body = response.read() 917 if response.status != 200: 918 raise self.connection.provider.storage_response_error( 919 response.status, response.reason, body) 920 return body 921 922 def set_xml_acl(self, acl_str, key_name='', headers=None, version_id=None, 923 query_args='acl'): 924 if version_id: 925 query_args += '&versionId=%s' % version_id 926 if not isinstance(acl_str, bytes): 927 acl_str = acl_str.encode('utf-8') 928 response = self.connection.make_request('PUT', self.name, key_name, 929 data=acl_str, 930 query_args=query_args, 931 headers=headers) 932 body = response.read() 933 if response.status != 200: 934 raise self.connection.provider.storage_response_error( 935 response.status, response.reason, body) 936 937 def set_acl(self, acl_or_str, key_name='', headers=None, version_id=None): 938 if isinstance(acl_or_str, Policy): 939 self.set_xml_acl(acl_or_str.to_xml(), key_name, 940 headers, version_id) 941 else: 942 self.set_canned_acl(acl_or_str, key_name, 943 headers, version_id) 944 945 def get_acl(self, key_name='', headers=None, version_id=None): 946 query_args = 'acl' 947 if version_id: 948 query_args += '&versionId=%s' % version_id 949 response = self.connection.make_request('GET', self.name, key_name, 950 query_args=query_args, 951 headers=headers) 952 body = response.read() 953 if response.status == 200: 954 policy = Policy(self) 955 h = handler.XmlHandler(policy, self) 956 if not isinstance(body, bytes): 957 body = body.encode('utf-8') 958 xml.sax.parseString(body, h) 959 return policy 960 else: 961 raise self.connection.provider.storage_response_error( 962 response.status, response.reason, body) 963 964 def set_subresource(self, subresource, value, key_name='', headers=None, 965 version_id=None): 966 """ 967 Set a subresource for a bucket or key. 968 969 :type subresource: string 970 :param subresource: The subresource to set. 971 972 :type value: string 973 :param value: The value of the subresource. 974 975 :type key_name: string 976 :param key_name: The key to operate on, or None to operate on the 977 bucket. 978 979 :type headers: dict 980 :param headers: Additional HTTP headers to include in the request. 981 982 :type src_version_id: string 983 :param src_version_id: Optional. The version id of the key to 984 operate on. If not specified, operate on the newest 985 version. 986 """ 987 if not subresource: 988 raise TypeError('set_subresource called with subresource=None') 989 query_args = subresource 990 if version_id: 991 query_args += '&versionId=%s' % version_id 992 if not isinstance(value, bytes): 993 value = value.encode('utf-8') 994 response = self.connection.make_request('PUT', self.name, key_name, 995 data=value, 996 query_args=query_args, 997 headers=headers) 998 body = response.read() 999 if response.status != 200: 1000 raise self.connection.provider.storage_response_error( 1001 response.status, response.reason, body) 1002 1003 def get_subresource(self, subresource, key_name='', headers=None, 1004 version_id=None): 1005 """ 1006 Get a subresource for a bucket or key. 1007 1008 :type subresource: string 1009 :param subresource: The subresource to get. 1010 1011 :type key_name: string 1012 :param key_name: The key to operate on, or None to operate on the 1013 bucket. 1014 1015 :type headers: dict 1016 :param headers: Additional HTTP headers to include in the request. 1017 1018 :type src_version_id: string 1019 :param src_version_id: Optional. The version id of the key to 1020 operate on. If not specified, operate on the newest 1021 version. 1022 1023 :rtype: string 1024 :returns: The value of the subresource. 1025 """ 1026 if not subresource: 1027 raise TypeError('get_subresource called with subresource=None') 1028 query_args = subresource 1029 if version_id: 1030 query_args += '&versionId=%s' % version_id 1031 response = self.connection.make_request('GET', self.name, key_name, 1032 query_args=query_args, 1033 headers=headers) 1034 body = response.read() 1035 if response.status != 200: 1036 raise self.connection.provider.storage_response_error( 1037 response.status, response.reason, body) 1038 return body 1039 1040 def make_public(self, recursive=False, headers=None): 1041 self.set_canned_acl('public-read', headers=headers) 1042 if recursive: 1043 for key in self: 1044 self.set_canned_acl('public-read', key.name, headers=headers) 1045 1046 def add_email_grant(self, permission, email_address, 1047 recursive=False, headers=None): 1048 """ 1049 Convenience method that provides a quick way to add an email grant 1050 to a bucket. This method retrieves the current ACL, creates a new 1051 grant based on the parameters passed in, adds that grant to the ACL 1052 and then PUT's the new ACL back to S3. 1053 1054 :type permission: string 1055 :param permission: The permission being granted. Should be one of: 1056 (READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL). 1057 1058 :type email_address: string 1059 :param email_address: The email address associated with the AWS 1060 account your are granting the permission to. 1061 1062 :type recursive: boolean 1063 :param recursive: A boolean value to controls whether the 1064 command will apply the grant to all keys within the bucket 1065 or not. The default value is False. By passing a True 1066 value, the call will iterate through all keys in the 1067 bucket and apply the same grant to each key. CAUTION: If 1068 you have a lot of keys, this could take a long time! 1069 """ 1070 if permission not in S3Permissions: 1071 raise self.connection.provider.storage_permissions_error( 1072 'Unknown Permission: %s' % permission) 1073 policy = self.get_acl(headers=headers) 1074 policy.acl.add_email_grant(permission, email_address) 1075 self.set_acl(policy, headers=headers) 1076 if recursive: 1077 for key in self: 1078 key.add_email_grant(permission, email_address, headers=headers) 1079 1080 def add_user_grant(self, permission, user_id, recursive=False, 1081 headers=None, display_name=None): 1082 """ 1083 Convenience method that provides a quick way to add a canonical 1084 user grant to a bucket. This method retrieves the current ACL, 1085 creates a new grant based on the parameters passed in, adds that 1086 grant to the ACL and then PUT's the new ACL back to S3. 1087 1088 :type permission: string 1089 :param permission: The permission being granted. Should be one of: 1090 (READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL). 1091 1092 :type user_id: string 1093 :param user_id: The canonical user id associated with the AWS 1094 account your are granting the permission to. 1095 1096 :type recursive: boolean 1097 :param recursive: A boolean value to controls whether the 1098 command will apply the grant to all keys within the bucket 1099 or not. The default value is False. By passing a True 1100 value, the call will iterate through all keys in the 1101 bucket and apply the same grant to each key. CAUTION: If 1102 you have a lot of keys, this could take a long time! 1103 1104 :type display_name: string 1105 :param display_name: An option string containing the user's 1106 Display Name. Only required on Walrus. 1107 """ 1108 if permission not in S3Permissions: 1109 raise self.connection.provider.storage_permissions_error( 1110 'Unknown Permission: %s' % permission) 1111 policy = self.get_acl(headers=headers) 1112 policy.acl.add_user_grant(permission, user_id, 1113 display_name=display_name) 1114 self.set_acl(policy, headers=headers) 1115 if recursive: 1116 for key in self: 1117 key.add_user_grant(permission, user_id, headers=headers, 1118 display_name=display_name) 1119 1120 def list_grants(self, headers=None): 1121 policy = self.get_acl(headers=headers) 1122 return policy.acl.grants 1123 1124 def get_location(self): 1125 """ 1126 Returns the LocationConstraint for the bucket. 1127 1128 :rtype: str 1129 :return: The LocationConstraint for the bucket or the empty 1130 string if no constraint was specified when bucket was created. 1131 """ 1132 response = self.connection.make_request('GET', self.name, 1133 query_args='location') 1134 body = response.read() 1135 if response.status == 200: 1136 rs = ResultSet(self) 1137 h = handler.XmlHandler(rs, self) 1138 if not isinstance(body, bytes): 1139 body = body.encode('utf-8') 1140 xml.sax.parseString(body, h) 1141 return rs.LocationConstraint 1142 else: 1143 raise self.connection.provider.storage_response_error( 1144 response.status, response.reason, body) 1145 1146 def set_xml_logging(self, logging_str, headers=None): 1147 """ 1148 Set logging on a bucket directly to the given xml string. 1149 1150 :type logging_str: unicode string 1151 :param logging_str: The XML for the bucketloggingstatus which 1152 will be set. The string will be converted to utf-8 before 1153 it is sent. Usually, you will obtain this XML from the 1154 BucketLogging object. 1155 1156 :rtype: bool 1157 :return: True if ok or raises an exception. 1158 """ 1159 body = logging_str 1160 if not isinstance(body, bytes): 1161 body = body.encode('utf-8') 1162 response = self.connection.make_request('PUT', self.name, data=body, 1163 query_args='logging', headers=headers) 1164 body = response.read() 1165 if response.status == 200: 1166 return True 1167 else: 1168 raise self.connection.provider.storage_response_error( 1169 response.status, response.reason, body) 1170 1171 def enable_logging(self, target_bucket, target_prefix='', 1172 grants=None, headers=None): 1173 """ 1174 Enable logging on a bucket. 1175 1176 :type target_bucket: bucket or string 1177 :param target_bucket: The bucket to log to. 1178 1179 :type target_prefix: string 1180 :param target_prefix: The prefix which should be prepended to the 1181 generated log files written to the target_bucket. 1182 1183 :type grants: list of Grant objects 1184 :param grants: A list of extra permissions which will be granted on 1185 the log files which are created. 1186 1187 :rtype: bool 1188 :return: True if ok or raises an exception. 1189 """ 1190 if isinstance(target_bucket, Bucket): 1191 target_bucket = target_bucket.name 1192 blogging = BucketLogging(target=target_bucket, prefix=target_prefix, 1193 grants=grants) 1194 return self.set_xml_logging(blogging.to_xml(), headers=headers) 1195 1196 def disable_logging(self, headers=None): 1197 """ 1198 Disable logging on a bucket. 1199 1200 :rtype: bool 1201 :return: True if ok or raises an exception. 1202 """ 1203 blogging = BucketLogging() 1204 return self.set_xml_logging(blogging.to_xml(), headers=headers) 1205 1206 def get_logging_status(self, headers=None): 1207 """ 1208 Get the logging status for this bucket. 1209 1210 :rtype: :class:`boto.s3.bucketlogging.BucketLogging` 1211 :return: A BucketLogging object for this bucket. 1212 """ 1213 response = self.connection.make_request('GET', self.name, 1214 query_args='logging', headers=headers) 1215 body = response.read() 1216 if response.status == 200: 1217 blogging = BucketLogging() 1218 h = handler.XmlHandler(blogging, self) 1219 if not isinstance(body, bytes): 1220 body = body.encode('utf-8') 1221 xml.sax.parseString(body, h) 1222 return blogging 1223 else: 1224 raise self.connection.provider.storage_response_error( 1225 response.status, response.reason, body) 1226 1227 def set_as_logging_target(self, headers=None): 1228 """ 1229 Setup the current bucket as a logging target by granting the necessary 1230 permissions to the LogDelivery group to write log files to this bucket. 1231 """ 1232 policy = self.get_acl(headers=headers) 1233 g1 = Grant(permission='WRITE', type='Group', uri=self.LoggingGroup) 1234 g2 = Grant(permission='READ_ACP', type='Group', uri=self.LoggingGroup) 1235 policy.acl.add_grant(g1) 1236 policy.acl.add_grant(g2) 1237 self.set_acl(policy, headers=headers) 1238 1239 def get_request_payment(self, headers=None): 1240 response = self.connection.make_request('GET', self.name, 1241 query_args='requestPayment', headers=headers) 1242 body = response.read() 1243 if response.status == 200: 1244 return body 1245 else: 1246 raise self.connection.provider.storage_response_error( 1247 response.status, response.reason, body) 1248 1249 def set_request_payment(self, payer='BucketOwner', headers=None): 1250 body = self.BucketPaymentBody % payer 1251 response = self.connection.make_request('PUT', self.name, data=body, 1252 query_args='requestPayment', headers=headers) 1253 body = response.read() 1254 if response.status == 200: 1255 return True 1256 else: 1257 raise self.connection.provider.storage_response_error( 1258 response.status, response.reason, body) 1259 1260 def configure_versioning(self, versioning, mfa_delete=False, 1261 mfa_token=None, headers=None): 1262 """ 1263 Configure versioning for this bucket. 1264 1265 ..note:: This feature is currently in beta. 1266 1267 :type versioning: bool 1268 :param versioning: A boolean indicating whether version is 1269 enabled (True) or disabled (False). 1270 1271 :type mfa_delete: bool 1272 :param mfa_delete: A boolean indicating whether the 1273 Multi-Factor Authentication Delete feature is enabled 1274 (True) or disabled (False). If mfa_delete is enabled then 1275 all Delete operations will require the token from your MFA 1276 device to be passed in the request. 1277 1278 :type mfa_token: tuple or list of strings 1279 :param mfa_token: A tuple or list consisting of the serial 1280 number from the MFA device and the current value of the 1281 six-digit token associated with the device. This value is 1282 required when you are changing the status of the MfaDelete 1283 property of the bucket. 1284 """ 1285 if versioning: 1286 ver = 'Enabled' 1287 else: 1288 ver = 'Suspended' 1289 if mfa_delete: 1290 mfa = 'Enabled' 1291 else: 1292 mfa = 'Disabled' 1293 body = self.VersioningBody % (ver, mfa) 1294 if mfa_token: 1295 if not headers: 1296 headers = {} 1297 provider = self.connection.provider 1298 headers[provider.mfa_header] = ' '.join(mfa_token) 1299 response = self.connection.make_request('PUT', self.name, data=body, 1300 query_args='versioning', headers=headers) 1301 body = response.read() 1302 if response.status == 200: 1303 return True 1304 else: 1305 raise self.connection.provider.storage_response_error( 1306 response.status, response.reason, body) 1307 1308 def get_versioning_status(self, headers=None): 1309 """ 1310 Returns the current status of versioning on the bucket. 1311 1312 :rtype: dict 1313 :returns: A dictionary containing a key named 'Versioning' 1314 that can have a value of either Enabled, Disabled, or 1315 Suspended. Also, if MFADelete has ever been enabled on the 1316 bucket, the dictionary will contain a key named 1317 'MFADelete' which will have a value of either Enabled or 1318 Suspended. 1319 """ 1320 response = self.connection.make_request('GET', self.name, 1321 query_args='versioning', headers=headers) 1322 body = response.read() 1323 if not isinstance(body, six.string_types): 1324 body = body.decode('utf-8') 1325 boto.log.debug(body) 1326 if response.status == 200: 1327 d = {} 1328 ver = re.search(self.VersionRE, body) 1329 if ver: 1330 d['Versioning'] = ver.group(1) 1331 mfa = re.search(self.MFADeleteRE, body) 1332 if mfa: 1333 d['MfaDelete'] = mfa.group(1) 1334 return d 1335 else: 1336 raise self.connection.provider.storage_response_error( 1337 response.status, response.reason, body) 1338 1339 def configure_lifecycle(self, lifecycle_config, headers=None): 1340 """ 1341 Configure lifecycle for this bucket. 1342 1343 :type lifecycle_config: :class:`boto.s3.lifecycle.Lifecycle` 1344 :param lifecycle_config: The lifecycle configuration you want 1345 to configure for this bucket. 1346 """ 1347 xml = lifecycle_config.to_xml() 1348 #xml = xml.encode('utf-8') 1349 fp = StringIO(xml) 1350 md5 = boto.utils.compute_md5(fp) 1351 if headers is None: 1352 headers = {} 1353 headers['Content-MD5'] = md5[1] 1354 headers['Content-Type'] = 'text/xml' 1355 response = self.connection.make_request('PUT', self.name, 1356 data=fp.getvalue(), 1357 query_args='lifecycle', 1358 headers=headers) 1359 body = response.read() 1360 if response.status == 200: 1361 return True 1362 else: 1363 raise self.connection.provider.storage_response_error( 1364 response.status, response.reason, body) 1365 1366 def get_lifecycle_config(self, headers=None): 1367 """ 1368 Returns the current lifecycle configuration on the bucket. 1369 1370 :rtype: :class:`boto.s3.lifecycle.Lifecycle` 1371 :returns: A LifecycleConfig object that describes all current 1372 lifecycle rules in effect for the bucket. 1373 """ 1374 response = self.connection.make_request('GET', self.name, 1375 query_args='lifecycle', headers=headers) 1376 body = response.read() 1377 boto.log.debug(body) 1378 if response.status == 200: 1379 lifecycle = Lifecycle() 1380 h = handler.XmlHandler(lifecycle, self) 1381 if not isinstance(body, bytes): 1382 body = body.encode('utf-8') 1383 xml.sax.parseString(body, h) 1384 return lifecycle 1385 else: 1386 raise self.connection.provider.storage_response_error( 1387 response.status, response.reason, body) 1388 1389 def delete_lifecycle_configuration(self, headers=None): 1390 """ 1391 Removes all lifecycle configuration from the bucket. 1392 """ 1393 response = self.connection.make_request('DELETE', self.name, 1394 query_args='lifecycle', 1395 headers=headers) 1396 body = response.read() 1397 boto.log.debug(body) 1398 if response.status == 204: 1399 return True 1400 else: 1401 raise self.connection.provider.storage_response_error( 1402 response.status, response.reason, body) 1403 1404 def configure_website(self, suffix=None, error_key=None, 1405 redirect_all_requests_to=None, 1406 routing_rules=None, 1407 headers=None): 1408 """ 1409 Configure this bucket to act as a website 1410 1411 :type suffix: str 1412 :param suffix: Suffix that is appended to a request that is for a 1413 "directory" on the website endpoint (e.g. if the suffix is 1414 index.html and you make a request to samplebucket/images/ 1415 the data that is returned will be for the object with the 1416 key name images/index.html). The suffix must not be empty 1417 and must not include a slash character. 1418 1419 :type error_key: str 1420 :param error_key: The object key name to use when a 4XX class 1421 error occurs. This is optional. 1422 1423 :type redirect_all_requests_to: :class:`boto.s3.website.RedirectLocation` 1424 :param redirect_all_requests_to: Describes the redirect behavior for 1425 every request to this bucket's website endpoint. If this value is 1426 non None, no other values are considered when configuring the 1427 website configuration for the bucket. This is an instance of 1428 ``RedirectLocation``. 1429 1430 :type routing_rules: :class:`boto.s3.website.RoutingRules` 1431 :param routing_rules: Object which specifies conditions 1432 and redirects that apply when the conditions are met. 1433 1434 """ 1435 config = website.WebsiteConfiguration( 1436 suffix, error_key, redirect_all_requests_to, 1437 routing_rules) 1438 return self.set_website_configuration(config, headers=headers) 1439 1440 def set_website_configuration(self, config, headers=None): 1441 """ 1442 :type config: boto.s3.website.WebsiteConfiguration 1443 :param config: Configuration data 1444 """ 1445 return self.set_website_configuration_xml(config.to_xml(), 1446 headers=headers) 1447 1448 1449 def set_website_configuration_xml(self, xml, headers=None): 1450 """Upload xml website configuration""" 1451 response = self.connection.make_request('PUT', self.name, data=xml, 1452 query_args='website', 1453 headers=headers) 1454 body = response.read() 1455 if response.status == 200: 1456 return True 1457 else: 1458 raise self.connection.provider.storage_response_error( 1459 response.status, response.reason, body) 1460 1461 def get_website_configuration(self, headers=None): 1462 """ 1463 Returns the current status of website configuration on the bucket. 1464 1465 :rtype: dict 1466 :returns: A dictionary containing a Python representation 1467 of the XML response from S3. The overall structure is: 1468 1469 * WebsiteConfiguration 1470 1471 * IndexDocument 1472 1473 * Suffix : suffix that is appended to request that 1474 is for a "directory" on the website endpoint 1475 * ErrorDocument 1476 1477 * Key : name of object to serve when an error occurs 1478 1479 """ 1480 return self.get_website_configuration_with_xml(headers)[0] 1481 1482 def get_website_configuration_obj(self, headers=None): 1483 """Get the website configuration as a 1484 :class:`boto.s3.website.WebsiteConfiguration` object. 1485 """ 1486 config_xml = self.get_website_configuration_xml(headers=headers) 1487 config = website.WebsiteConfiguration() 1488 h = handler.XmlHandler(config, self) 1489 xml.sax.parseString(config_xml, h) 1490 return config 1491 1492 def get_website_configuration_with_xml(self, headers=None): 1493 """ 1494 Returns the current status of website configuration on the bucket as 1495 unparsed XML. 1496 1497 :rtype: 2-Tuple 1498 :returns: 2-tuple containing: 1499 1500 1) A dictionary containing a Python representation \ 1501 of the XML response. The overall structure is: 1502 1503 * WebsiteConfiguration 1504 1505 * IndexDocument 1506 1507 * Suffix : suffix that is appended to request that \ 1508 is for a "directory" on the website endpoint 1509 1510 * ErrorDocument 1511 1512 * Key : name of object to serve when an error occurs 1513 1514 1515 2) unparsed XML describing the bucket's website configuration 1516 1517 """ 1518 1519 body = self.get_website_configuration_xml(headers=headers) 1520 e = boto.jsonresponse.Element() 1521 h = boto.jsonresponse.XmlHandler(e, None) 1522 h.parse(body) 1523 return e, body 1524 1525 def get_website_configuration_xml(self, headers=None): 1526 """Get raw website configuration xml""" 1527 response = self.connection.make_request('GET', self.name, 1528 query_args='website', headers=headers) 1529 body = response.read().decode('utf-8') 1530 boto.log.debug(body) 1531 1532 if response.status != 200: 1533 raise self.connection.provider.storage_response_error( 1534 response.status, response.reason, body) 1535 return body 1536 1537 def delete_website_configuration(self, headers=None): 1538 """ 1539 Removes all website configuration from the bucket. 1540 """ 1541 response = self.connection.make_request('DELETE', self.name, 1542 query_args='website', headers=headers) 1543 body = response.read() 1544 boto.log.debug(body) 1545 if response.status == 204: 1546 return True 1547 else: 1548 raise self.connection.provider.storage_response_error( 1549 response.status, response.reason, body) 1550 1551 def get_website_endpoint(self): 1552 """ 1553 Returns the fully qualified hostname to use is you want to access this 1554 bucket as a website. This doesn't validate whether the bucket has 1555 been correctly configured as a website or not. 1556 """ 1557 l = [self.name] 1558 l.append(S3WebsiteEndpointTranslate.translate_region(self.get_location())) 1559 l.append('.'.join(self.connection.host.split('.')[-2:])) 1560 return '.'.join(l) 1561 1562 def get_policy(self, headers=None): 1563 """ 1564 Returns the JSON policy associated with the bucket. The policy 1565 is returned as an uninterpreted JSON string. 1566 """ 1567 response = self.connection.make_request('GET', self.name, 1568 query_args='policy', headers=headers) 1569 body = response.read() 1570 if response.status == 200: 1571 return body 1572 else: 1573 raise self.connection.provider.storage_response_error( 1574 response.status, response.reason, body) 1575 1576 def set_policy(self, policy, headers=None): 1577 """ 1578 Add or replace the JSON policy associated with the bucket. 1579 1580 :type policy: str 1581 :param policy: The JSON policy as a string. 1582 """ 1583 response = self.connection.make_request('PUT', self.name, 1584 data=policy, 1585 query_args='policy', 1586 headers=headers) 1587 body = response.read() 1588 if response.status >= 200 and response.status <= 204: 1589 return True 1590 else: 1591 raise self.connection.provider.storage_response_error( 1592 response.status, response.reason, body) 1593 1594 def delete_policy(self, headers=None): 1595 response = self.connection.make_request('DELETE', self.name, 1596 data='/?policy', 1597 query_args='policy', 1598 headers=headers) 1599 body = response.read() 1600 if response.status >= 200 and response.status <= 204: 1601 return True 1602 else: 1603 raise self.connection.provider.storage_response_error( 1604 response.status, response.reason, body) 1605 1606 def set_cors_xml(self, cors_xml, headers=None): 1607 """ 1608 Set the CORS (Cross-Origin Resource Sharing) for a bucket. 1609 1610 :type cors_xml: str 1611 :param cors_xml: The XML document describing your desired 1612 CORS configuration. See the S3 documentation for details 1613 of the exact syntax required. 1614 """ 1615 fp = StringIO(cors_xml) 1616 md5 = boto.utils.compute_md5(fp) 1617 if headers is None: 1618 headers = {} 1619 headers['Content-MD5'] = md5[1] 1620 headers['Content-Type'] = 'text/xml' 1621 response = self.connection.make_request('PUT', self.name, 1622 data=fp.getvalue(), 1623 query_args='cors', 1624 headers=headers) 1625 body = response.read() 1626 if response.status == 200: 1627 return True 1628 else: 1629 raise self.connection.provider.storage_response_error( 1630 response.status, response.reason, body) 1631 1632 def set_cors(self, cors_config, headers=None): 1633 """ 1634 Set the CORS for this bucket given a boto CORSConfiguration 1635 object. 1636 1637 :type cors_config: :class:`boto.s3.cors.CORSConfiguration` 1638 :param cors_config: The CORS configuration you want 1639 to configure for this bucket. 1640 """ 1641 return self.set_cors_xml(cors_config.to_xml()) 1642 1643 def get_cors_xml(self, headers=None): 1644 """ 1645 Returns the current CORS configuration on the bucket as an 1646 XML document. 1647 """ 1648 response = self.connection.make_request('GET', self.name, 1649 query_args='cors', headers=headers) 1650 body = response.read() 1651 boto.log.debug(body) 1652 if response.status == 200: 1653 return body 1654 else: 1655 raise self.connection.provider.storage_response_error( 1656 response.status, response.reason, body) 1657 1658 def get_cors(self, headers=None): 1659 """ 1660 Returns the current CORS configuration on the bucket. 1661 1662 :rtype: :class:`boto.s3.cors.CORSConfiguration` 1663 :returns: A CORSConfiguration object that describes all current 1664 CORS rules in effect for the bucket. 1665 """ 1666 body = self.get_cors_xml(headers) 1667 cors = CORSConfiguration() 1668 h = handler.XmlHandler(cors, self) 1669 xml.sax.parseString(body, h) 1670 return cors 1671 1672 def delete_cors(self, headers=None): 1673 """ 1674 Removes all CORS configuration from the bucket. 1675 """ 1676 response = self.connection.make_request('DELETE', self.name, 1677 query_args='cors', 1678 headers=headers) 1679 body = response.read() 1680 boto.log.debug(body) 1681 if response.status == 204: 1682 return True 1683 else: 1684 raise self.connection.provider.storage_response_error( 1685 response.status, response.reason, body) 1686 1687 def initiate_multipart_upload(self, key_name, headers=None, 1688 reduced_redundancy=False, 1689 metadata=None, encrypt_key=False, 1690 policy=None): 1691 """ 1692 Start a multipart upload operation. 1693 1694 .. note:: 1695 1696 Note: After you initiate multipart upload and upload one or more 1697 parts, you must either complete or abort multipart upload in order 1698 to stop getting charged for storage of the uploaded parts. Only 1699 after you either complete or abort multipart upload, Amazon S3 1700 frees up the parts storage and stops charging you for the parts 1701 storage. 1702 1703 :type key_name: string 1704 :param key_name: The name of the key that will ultimately 1705 result from this multipart upload operation. This will be 1706 exactly as the key appears in the bucket after the upload 1707 process has been completed. 1708 1709 :type headers: dict 1710 :param headers: Additional HTTP headers to send and store with the 1711 resulting key in S3. 1712 1713 :type reduced_redundancy: boolean 1714 :param reduced_redundancy: In multipart uploads, the storage 1715 class is specified when initiating the upload, not when 1716 uploading individual parts. So if you want the resulting 1717 key to use the reduced redundancy storage class set this 1718 flag when you initiate the upload. 1719 1720 :type metadata: dict 1721 :param metadata: Any metadata that you would like to set on the key 1722 that results from the multipart upload. 1723 1724 :type encrypt_key: bool 1725 :param encrypt_key: If True, the new copy of the object will 1726 be encrypted on the server-side by S3 and will be stored 1727 in an encrypted form while at rest in S3. 1728 1729 :type policy: :class:`boto.s3.acl.CannedACLStrings` 1730 :param policy: A canned ACL policy that will be applied to the 1731 new key (once completed) in S3. 1732 """ 1733 query_args = 'uploads' 1734 provider = self.connection.provider 1735 headers = headers or {} 1736 if policy: 1737 headers[provider.acl_header] = policy 1738 if reduced_redundancy: 1739 storage_class_header = provider.storage_class_header 1740 if storage_class_header: 1741 headers[storage_class_header] = 'REDUCED_REDUNDANCY' 1742 # TODO: what if the provider doesn't support reduced redundancy? 1743 # (see boto.s3.key.Key.set_contents_from_file) 1744 if encrypt_key: 1745 headers[provider.server_side_encryption_header] = 'AES256' 1746 if metadata is None: 1747 metadata = {} 1748 1749 headers = boto.utils.merge_meta(headers, metadata, 1750 self.connection.provider) 1751 response = self.connection.make_request('POST', self.name, key_name, 1752 query_args=query_args, 1753 headers=headers) 1754 body = response.read() 1755 boto.log.debug(body) 1756 if response.status == 200: 1757 resp = MultiPartUpload(self) 1758 h = handler.XmlHandler(resp, self) 1759 if not isinstance(body, bytes): 1760 body = body.encode('utf-8') 1761 xml.sax.parseString(body, h) 1762 return resp 1763 else: 1764 raise self.connection.provider.storage_response_error( 1765 response.status, response.reason, body) 1766 1767 def complete_multipart_upload(self, key_name, upload_id, 1768 xml_body, headers=None): 1769 """ 1770 Complete a multipart upload operation. 1771 """ 1772 query_args = 'uploadId=%s' % upload_id 1773 if headers is None: 1774 headers = {} 1775 headers['Content-Type'] = 'text/xml' 1776 response = self.connection.make_request('POST', self.name, key_name, 1777 query_args=query_args, 1778 headers=headers, data=xml_body) 1779 contains_error = False 1780 body = response.read().decode('utf-8') 1781 # Some errors will be reported in the body of the response 1782 # even though the HTTP response code is 200. This check 1783 # does a quick and dirty peek in the body for an error element. 1784 if body.find('<Error>') > 0: 1785 contains_error = True 1786 boto.log.debug(body) 1787 if response.status == 200 and not contains_error: 1788 resp = CompleteMultiPartUpload(self) 1789 h = handler.XmlHandler(resp, self) 1790 if not isinstance(body, bytes): 1791 body = body.encode('utf-8') 1792 xml.sax.parseString(body, h) 1793 # Use a dummy key to parse various response headers 1794 # for versioning, encryption info and then explicitly 1795 # set the completed MPU object values from key. 1796 k = self.key_class(self) 1797 k.handle_version_headers(response) 1798 k.handle_encryption_headers(response) 1799 resp.version_id = k.version_id 1800 resp.encrypted = k.encrypted 1801 return resp 1802 else: 1803 raise self.connection.provider.storage_response_error( 1804 response.status, response.reason, body) 1805 1806 def cancel_multipart_upload(self, key_name, upload_id, headers=None): 1807 """ 1808 To verify that all parts have been removed, so you don't get charged 1809 for the part storage, you should call the List Parts operation and 1810 ensure the parts list is empty. 1811 """ 1812 query_args = 'uploadId=%s' % upload_id 1813 response = self.connection.make_request('DELETE', self.name, key_name, 1814 query_args=query_args, 1815 headers=headers) 1816 body = response.read() 1817 boto.log.debug(body) 1818 if response.status != 204: 1819 raise self.connection.provider.storage_response_error( 1820 response.status, response.reason, body) 1821 1822 def delete(self, headers=None): 1823 return self.connection.delete_bucket(self.name, headers=headers) 1824 1825 def get_tags(self): 1826 response = self.get_xml_tags() 1827 tags = Tags() 1828 h = handler.XmlHandler(tags, self) 1829 if not isinstance(response, bytes): 1830 response = response.encode('utf-8') 1831 xml.sax.parseString(response, h) 1832 return tags 1833 1834 def get_xml_tags(self): 1835 response = self.connection.make_request('GET', self.name, 1836 query_args='tagging', 1837 headers=None) 1838 body = response.read() 1839 if response.status == 200: 1840 return body 1841 else: 1842 raise self.connection.provider.storage_response_error( 1843 response.status, response.reason, body) 1844 1845 def set_xml_tags(self, tag_str, headers=None, query_args='tagging'): 1846 if headers is None: 1847 headers = {} 1848 md5 = boto.utils.compute_md5(StringIO(tag_str)) 1849 headers['Content-MD5'] = md5[1] 1850 headers['Content-Type'] = 'text/xml' 1851 if not isinstance(tag_str, bytes): 1852 tag_str = tag_str.encode('utf-8') 1853 response = self.connection.make_request('PUT', self.name, 1854 data=tag_str, 1855 query_args=query_args, 1856 headers=headers) 1857 body = response.read() 1858 if response.status != 204: 1859 raise self.connection.provider.storage_response_error( 1860 response.status, response.reason, body) 1861 return True 1862 1863 def set_tags(self, tags, headers=None): 1864 return self.set_xml_tags(tags.to_xml(), headers=headers) 1865 1866 def delete_tags(self, headers=None): 1867 response = self.connection.make_request('DELETE', self.name, 1868 query_args='tagging', 1869 headers=headers) 1870 body = response.read() 1871 boto.log.debug(body) 1872 if response.status == 204: 1873 return True 1874 else: 1875 raise self.connection.provider.storage_response_error( 1876 response.status, response.reason, body) 1877