1
# Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
3
# Permission is hereby granted, free of charge, to any person obtaining a
4
# copy of this software and associated documentation files (the
5
# "Software"), to deal in the Software without restriction, including
6
# without limitation the rights to use, copy, modify, merge, publish, dis-
7
# tribute, sublicense, and/or sell copies of the Software, and to permit
8
# persons to whom the Software is furnished to do so, subject to the fol-
11
# The above copyright notice and this permission notice shall be included
12
# in all copies or substantial portions of the Software.
14
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23
from boto import handler
24
from boto.resultset import ResultSet
25
from boto.s3.acl import Policy, CannedACLStrings, Grant
26
from boto.s3.key import Key
27
from boto.s3.prefix import Prefix
28
from boto.s3.deletemarker import DeleteMarker
29
from boto.exception import S3ResponseError, S3PermissionsError, S3CopyError
30
from boto.s3.bucketlistresultset import BucketListResultSet
31
from boto.s3.bucketlistresultset import VersionedBucketListResultSet
37
S3Permissions = ['READ', 'WRITE', 'READ_ACP', 'WRITE_ACP', 'FULL_CONTROL']
41
BucketLoggingBody = """<?xml version="1.0" encoding="UTF-8"?>
42
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
44
<TargetBucket>%s</TargetBucket>
45
<TargetPrefix>%s</TargetPrefix>
47
</BucketLoggingStatus>"""
49
EmptyBucketLoggingBody = """<?xml version="1.0" encoding="UTF-8"?>
50
<BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
51
</BucketLoggingStatus>"""
53
LoggingGroup = 'http://acs.amazonaws.com/groups/s3/LogDelivery'
55
BucketPaymentBody = """<?xml version="1.0" encoding="UTF-8"?>
56
<RequestPaymentConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
58
</RequestPaymentConfiguration>"""
60
VersioningBody = """<?xml version="1.0" encoding="UTF-8"?>
61
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
63
<MfaDelete>%s</MfaDelete>
64
</VersioningConfiguration>"""
66
VersionRE = '<Status>([A-Za-z]+)</Status>'
67
MFADeleteRE = '<MfaDelete>([A-Za-z]+)</MfaDelete>'
69
def __init__(self, connection=None, name=None, key_class=Key):
71
self.connection = connection
72
self.key_class = key_class
75
return '<Bucket: %s>' % self.name
78
return iter(BucketListResultSet(self))
80
def __contains__(self, key_name):
81
return not (self.get_key(key_name) is None)
83
def startElement(self, name, attrs, connection):
86
def endElement(self, name, value, connection):
89
elif name == 'CreationDate':
90
self.creation_date = value
92
setattr(self, name, value)
94
def set_key_class(self, key_class):
96
Set the Key class associated with this bucket. By default, this
97
would be the boto.s3.key.Key class but if you want to subclass that
98
for some reason this allows you to associate your new class with a
99
bucket so that when you call bucket.new_key() or when you get a listing
100
of keys in the bucket you will get an instances of your key class
101
rather than the default.
103
:type key_class: class
104
:param key_class: A subclass of Key that can be more specific
106
self.key_class = key_class
108
def lookup(self, key_name, headers=None):
110
Deprecated: Please use get_key method.
112
:type key_name: string
113
:param key_name: The name of the key to retrieve
115
:rtype: :class:`boto.s3.key.Key`
116
:returns: A Key object from this bucket.
118
return self.get_key(key_name, headers=headers)
120
def get_key(self, key_name, headers=None, version_id=None):
122
Check to see if a particular key exists within the bucket. This
123
method uses a HEAD request to check for the existance of the key.
124
Returns: An instance of a Key object or None
126
:type key_name: string
127
:param key_name: The name of the key to retrieve
129
:rtype: :class:`boto.s3.key.Key`
130
:returns: A Key object from this bucket.
133
query_args = 'versionId=%s' % version_id
136
response = self.connection.make_request('HEAD', self.name, key_name,
138
query_args=query_args)
139
if response.status == 200:
141
k = self.key_class(self)
142
k.metadata = boto.utils.get_aws_metadata(response.msg)
143
k.etag = response.getheader('etag')
144
k.content_type = response.getheader('content-type')
145
k.content_encoding = response.getheader('content-encoding')
146
k.last_modified = response.getheader('last-modified')
147
k.size = int(response.getheader('content-length'))
149
k.handle_version_headers(response)
152
if response.status == 404:
156
raise S3ResponseError(response.status, response.reason, '')
158
def list(self, prefix='', delimiter='', marker='', headers=None):
160
List key objects within a bucket. This returns an instance of an
161
BucketListResultSet that automatically handles all of the result
162
paging, etc. from S3. You just need to keep iterating until
163
there are no more results.
164
Called with no arguments, this will return an iterator object across
165
all keys within the bucket.
168
:param prefix: allows you to limit the listing to a particular
169
prefix. For example, if you call the method with
170
prefix='/foo/' then the iterator will only cycle
171
through the keys that begin with the string '/foo/'.
173
:type delimiter: string
174
:param delimiter: can be used in conjunction with the prefix
175
to allow you to organize and browse your keys
177
http://docs.amazonwebservices.com/AmazonS3/2006-03-01/
181
:param marker: The "marker" of where you are in the result set
183
:rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet`
184
:return: an instance of a BucketListResultSet that handles paging, etc
186
return BucketListResultSet(self, prefix, delimiter, marker, headers)
188
def list_versions(self, prefix='', delimiter='', key_marker='',
189
version_id_marker='', headers=None):
191
List key objects within a bucket. This returns an instance of an
192
BucketListResultSet that automatically handles all of the result
193
paging, etc. from S3. You just need to keep iterating until
194
there are no more results.
195
Called with no arguments, this will return an iterator object across
196
all keys within the bucket.
199
:param prefix: allows you to limit the listing to a particular
200
prefix. For example, if you call the method with
201
prefix='/foo/' then the iterator will only cycle
202
through the keys that begin with the string '/foo/'.
204
:type delimiter: string
205
:param delimiter: can be used in conjunction with the prefix
206
to allow you to organize and browse your keys
208
http://docs.amazonwebservices.com/AmazonS3/2006-03-01/
212
:param marker: The "marker" of where you are in the result set
214
:rtype: :class:`boto.s3.bucketlistresultset.BucketListResultSet`
215
:return: an instance of a BucketListResultSet that handles paging, etc
217
return VersionedBucketListResultSet(self, prefix, delimiter, key_marker,
218
version_id_marker, headers)
220
def _get_all(self, element_map, initial_query_string='',
221
headers=None, **params):
223
for k,v in params.items():
224
k = k.replace('_', '-')
227
if isinstance(v, unicode):
228
v = v.encode('utf-8')
229
if v is not None and v != '':
230
l.append('%s=%s' % (urllib.quote(k), urllib.quote(str(v))))
232
s = initial_query_string + '&' + '&'.join(l)
234
s = initial_query_string
235
response = self.connection.make_request('GET', self.name,
236
headers=headers, query_args=s)
237
body = response.read()
239
if response.status == 200:
240
rs = ResultSet(element_map)
241
h = handler.XmlHandler(rs, self)
242
xml.sax.parseString(body, h)
245
raise S3ResponseError(response.status, response.reason, body)
247
def get_all_keys(self, headers=None, **params):
249
A lower-level method for listing contents of a bucket.
250
This closely models the actual S3 API and requires you to manually
251
handle the paging of results. For a higher-level method
252
that handles the details of paging for you, you can use the list method.
255
:param max_keys: The maximum number of keys to retrieve
258
:param prefix: The prefix of the keys you want to retrieve
261
:param marker: The "marker" of where you are in the result set
263
:type delimiter: string
264
:param delimiter: If this optional, Unicode string parameter
265
is included with your request, then keys that
266
contain the same string between the prefix and
267
the first occurrence of the delimiter will be
268
rolled up into a single result element in the
269
CommonPrefixes collection. These rolled-up keys
270
are not returned elsewhere in the response.
273
:return: The result from S3 listing the keys requested
276
return self._get_all([('Contents', self.key_class),
277
('CommonPrefixes', Prefix)],
278
'', headers, **params)
280
def get_all_versions(self, headers=None, **params):
282
A lower-level, version-aware method for listing contents of a bucket.
283
This closely models the actual S3 API and requires you to manually
284
handle the paging of results. For a higher-level method
285
that handles the details of paging for you, you can use the list method.
288
:param max_keys: The maximum number of keys to retrieve
291
:param prefix: The prefix of the keys you want to retrieve
293
:type key_marker: string
294
:param key_marker: The "marker" of where you are in the result set
295
with respect to keys.
297
:type version_id_marker: string
298
:param version_id_marker: The "marker" of where you are in the result
299
set with respect to version-id's.
301
:type delimiter: string
302
:param delimiter: If this optional, Unicode string parameter
303
is included with your request, then keys that
304
contain the same string between the prefix and
305
the first occurrence of the delimiter will be
306
rolled up into a single result element in the
307
CommonPrefixes collection. These rolled-up keys
308
are not returned elsewhere in the response.
311
:return: The result from S3 listing the keys requested
314
return self._get_all([('Version', self.key_class),
315
('CommonPrefixes', Prefix),
316
('DeleteMarker', DeleteMarker)],
317
'versions', headers, **params)
319
def new_key(self, key_name=None):
323
:type key_name: string
324
:param key_name: The name of the key to create
326
:rtype: :class:`boto.s3.key.Key` or subclass
327
:returns: An instance of the newly created key object
329
return self.key_class(self, key_name)
331
def generate_url(self, expires_in, method='GET',
332
headers=None, force_http=False):
333
return self.connection.generate_url(expires_in, method, self.name,
335
force_http=force_http)
337
def delete_key(self, key_name, headers=None,
338
version_id=None, mfa_token=None):
340
Deletes a key from the bucket. If a version_id is provided,
341
only that version of the key will be deleted.
343
:type key_name: string
344
:param key_name: The key name to delete
346
:type version_id: string
347
:param version_id: The version ID (optional)
349
:type mfa_token: tuple or list of strings
350
:param mfa_token: A tuple or list consisting of the serial number
351
from the MFA device and the current value of
352
the six-digit token associated with the device.
353
This value is required anytime you are
354
deleting versioned objects from a bucket
355
that has the MFADelete option on the bucket.
358
query_args = 'versionId=%s' % version_id
364
headers['x-amz-mfa'] = ' '.join(mfa_token)
365
response = self.connection.make_request('DELETE', self.name, key_name,
367
query_args=query_args)
368
body = response.read()
369
if response.status != 204:
370
raise S3ResponseError(response.status, response.reason, body)
372
def copy_key(self, new_key_name, src_bucket_name,
373
src_key_name, metadata=None, src_version_id=None):
375
Create a new key in the bucket by copying another existing key.
377
:type new_key_name: string
378
:param new_key_name: The name of the new key
380
:type src_bucket_name: string
381
:param src_bucket_name: The name of the source bucket
383
:type src_key_name: string
384
:param src_key_name: The name of the source key
386
:type src_version_id: string
387
:param src_version_id: The version id for the key. This param
388
is optional. If not specified, the newest
389
version of the key will be copied.
392
:param metadata: Metadata to be associated with new key.
393
If metadata is supplied, it will replace the
394
metadata of the source key being copied.
395
If no metadata is supplied, the source key's
396
metadata will be copied to the new key.
398
:rtype: :class:`boto.s3.key.Key` or subclass
399
:returns: An instance of the newly created key object
401
src = '%s/%s' % (src_bucket_name, urllib.quote(src_key_name))
403
src += '?version_id=%s' % src_version_id
405
headers = {'x-amz-copy-source' : src,
406
'x-amz-metadata-directive' : 'REPLACE'}
407
headers = boto.utils.merge_meta(headers, metadata)
409
headers = {'x-amz-copy-source' : src,
410
'x-amz-metadata-directive' : 'COPY'}
411
response = self.connection.make_request('PUT', self.name, new_key_name,
413
body = response.read()
414
if response.status == 200:
415
key = self.new_key(new_key_name)
416
h = handler.XmlHandler(key, self)
417
xml.sax.parseString(body, h)
418
if hasattr(key, 'Error'):
419
raise S3CopyError(key.Code, key.Message, body)
420
key.handle_version_headers(response)
423
raise S3ResponseError(response.status, response.reason, body)
425
def set_canned_acl(self, acl_str, key_name='', headers=None,
427
assert acl_str in CannedACLStrings
430
headers['x-amz-acl'] = acl_str
432
headers={'x-amz-acl': acl_str}
436
query_args += '&versionId=%s' % version_id
437
response = self.connection.make_request('PUT', self.name, key_name,
438
headers=headers, query_args=query_args)
439
body = response.read()
440
if response.status != 200:
441
raise S3ResponseError(response.status, response.reason, body)
443
def get_xml_acl(self, key_name='', headers=None, version_id=None):
446
query_args += '&versionId=%s' % version_id
447
response = self.connection.make_request('GET', self.name, key_name,
448
query_args=query_args,
450
body = response.read()
451
if response.status != 200:
452
raise S3ResponseError(response.status, response.reason, body)
455
def set_xml_acl(self, acl_str, key_name='', headers=None, version_id=None):
458
query_args += '&versionId=%s' % version_id
459
response = self.connection.make_request('PUT', self.name, key_name,
461
query_args=query_args,
463
body = response.read()
464
if response.status != 200:
465
raise S3ResponseError(response.status, response.reason, body)
467
def set_acl(self, acl_or_str, key_name='', headers=None, version_id=None):
468
if isinstance(acl_or_str, Policy):
469
self.set_xml_acl(acl_or_str.to_xml(), key_name,
472
self.set_canned_acl(acl_or_str, key_name,
475
def get_acl(self, key_name='', headers=None, version_id=None):
478
query_args += '&versionId=%s' % version_id
479
response = self.connection.make_request('GET', self.name, key_name,
480
query_args=query_args,
482
body = response.read()
483
if response.status == 200:
484
policy = Policy(self)
485
h = handler.XmlHandler(policy, self)
486
xml.sax.parseString(body, h)
489
raise S3ResponseError(response.status, response.reason, body)
491
def make_public(self, recursive=False, headers=None):
492
self.set_canned_acl('public-read', headers=headers)
495
self.set_canned_acl('public-read', key.name, headers=headers)
497
def add_email_grant(self, permission, email_address,
498
recursive=False, headers=None):
500
Convenience method that provides a quick way to add an email grant
501
to a bucket. This method retrieves the current ACL, creates a new
502
grant based on the parameters passed in, adds that grant to the ACL
503
and then PUT's the new ACL back to S3.
505
:type permission: string
506
:param permission: The permission being granted. Should be one of:
507
(READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL).
509
:type email_address: string
510
:param email_address: The email address associated with the AWS
511
account your are granting the permission to.
513
:type recursive: boolean
514
:param recursive: A boolean value to controls whether the command
515
will apply the grant to all keys within the bucket
516
or not. The default value is False. By passing a
517
True value, the call will iterate through all keys
518
in the bucket and apply the same grant to each key.
519
CAUTION: If you have a lot of keys, this could take
522
if permission not in S3Permissions:
523
raise S3PermissionsError('Unknown Permission: %s' % permission)
524
policy = self.get_acl(headers=headers)
525
policy.acl.add_email_grant(permission, email_address)
526
self.set_acl(policy, headers=headers)
529
key.add_email_grant(permission, email_address, headers=headers)
531
def add_user_grant(self, permission, user_id, recursive=False, headers=None):
533
Convenience method that provides a quick way to add a canonical user grant to a bucket.
534
This method retrieves the current ACL, creates a new grant based on the parameters
535
passed in, adds that grant to the ACL and then PUT's the new ACL back to S3.
537
:type permission: string
538
:param permission: The permission being granted. Should be one of:
539
(READ, WRITE, READ_ACP, WRITE_ACP, FULL_CONTROL).
541
:type user_id: string
542
:param user_id: The canonical user id associated with the AWS account your are granting
545
:type recursive: boolean
546
:param recursive: A boolean value to controls whether the command
547
will apply the grant to all keys within the bucket
548
or not. The default value is False. By passing a
549
True value, the call will iterate through all keys
550
in the bucket and apply the same grant to each key.
551
CAUTION: If you have a lot of keys, this could take
554
if permission not in S3Permissions:
555
raise S3PermissionsError('Unknown Permission: %s' % permission)
556
policy = self.get_acl(headers=headers)
557
policy.acl.add_user_grant(permission, user_id)
558
self.set_acl(policy, headers=headers)
561
key.add_user_grant(permission, user_id, headers=headers)
563
def list_grants(self, headers=None):
564
policy = self.get_acl(headers=headers)
565
return policy.acl.grants
567
def get_location(self):
569
Returns the LocationConstraint for the bucket.
572
:return: The LocationConstraint for the bucket or the empty string if
573
no constraint was specified when bucket was created.
575
response = self.connection.make_request('GET', self.name,
576
query_args='location')
577
body = response.read()
578
if response.status == 200:
580
h = handler.XmlHandler(rs, self)
581
xml.sax.parseString(body, h)
582
return rs.LocationConstraint
584
raise S3ResponseError(response.status, response.reason, body)
586
def enable_logging(self, target_bucket, target_prefix='', headers=None):
587
if isinstance(target_bucket, Bucket):
588
target_bucket = target_bucket.name
589
body = self.BucketLoggingBody % (target_bucket, target_prefix)
590
response = self.connection.make_request('PUT', self.name, data=body,
591
query_args='logging', headers=headers)
592
body = response.read()
593
if response.status == 200:
596
raise S3ResponseError(response.status, response.reason, body)
598
def disable_logging(self, headers=None):
599
body = self.EmptyBucketLoggingBody
600
response = self.connection.make_request('PUT', self.name, data=body,
601
query_args='logging', headers=headers)
602
body = response.read()
603
if response.status == 200:
606
raise S3ResponseError(response.status, response.reason, body)
608
def get_logging_status(self, headers=None):
609
response = self.connection.make_request('GET', self.name,
610
query_args='logging', headers=headers)
611
body = response.read()
612
if response.status == 200:
615
raise S3ResponseError(response.status, response.reason, body)
617
def set_as_logging_target(self, headers=None):
618
policy = self.get_acl(headers=headers)
619
g1 = Grant(permission='WRITE', type='Group', uri=self.LoggingGroup)
620
g2 = Grant(permission='READ_ACP', type='Group', uri=self.LoggingGroup)
621
policy.acl.add_grant(g1)
622
policy.acl.add_grant(g2)
623
self.set_acl(policy, headers=headers)
625
def get_request_payment(self, headers=None):
626
response = self.connection.make_request('GET', self.name,
627
query_args='requestPayment', headers=headers)
628
body = response.read()
629
if response.status == 200:
632
raise S3ResponseError(response.status, response.reason, body)
634
def set_request_payment(self, payer='BucketOwner', headers=None):
635
body = self.BucketPaymentBody % payer
636
response = self.connection.make_request('PUT', self.name, data=body,
637
query_args='requestPayment', headers=headers)
638
body = response.read()
639
if response.status == 200:
642
raise S3ResponseError(response.status, response.reason, body)
644
def configure_versioning(self, versioning, mfa_delete=False,
645
mfa_token=None, headers=None):
647
Configure versioning for this bucket.
648
Note: This feature is currently in beta release and is available
649
only in the Northern California region.
651
:type versioning: bool
652
:param versioning: A boolean indicating whether version is
653
enabled (True) or disabled (False).
655
:type mfa_delete: bool
656
:param mfa_delete: A boolean indicating whether the Multi-Factor
657
Authentication Delete feature is enabled (True)
658
or disabled (False). If mfa_delete is enabled
659
then all Delete operations will require the
660
token from your MFA device to be passed in
663
:type mfa_token: tuple or list of strings
664
:param mfa_token: A tuple or list consisting of the serial number
665
from the MFA device and the current value of
666
the six-digit token associated with the device.
667
This value is required when you are changing
668
the status of the MfaDelete property of
679
body = self.VersioningBody % (ver, mfa)
683
headers['x-amz-mfa'] = ' '.join(mfa_token)
684
response = self.connection.make_request('PUT', self.name, data=body,
685
query_args='versioning', headers=headers)
686
body = response.read()
687
if response.status == 200:
690
raise S3ResponseError(response.status, response.reason, body)
692
def get_versioning_status(self, headers=None):
694
Returns the current status of versioning on the bucket.
697
:returns: A dictionary containing a key named 'Versioning'
698
that can have a value of either Enabled, Disabled,
699
or Suspended. Also, if MFADelete has ever been enabled
700
on the bucket, the dictionary will contain a key
701
named 'MFADelete' which will have a value of either
702
Enabled or Suspended.
704
response = self.connection.make_request('GET', self.name,
705
query_args='versioning', headers=headers)
706
body = response.read()
708
if response.status == 200:
710
ver = re.search(self.VersionRE, body)
712
d['Versioning'] = ver.group(1)
713
mfa = re.search(self.MFADeleteRE, body)
715
d['MfaDelete'] = mfa.group(1)
718
raise S3ResponseError(response.status, response.reason, body)
720
def delete(self, headers=None):
721
return self.connection.delete_bucket(self.name, headers=headers)