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
26
from boto.connection import AWSAuthConnection
27
from boto import handler
28
from boto.s3.bucket import Bucket
29
from boto.s3.key import Key
30
from boto.resultset import ResultSet
31
from boto.exception import S3ResponseError, S3CreateError, BotoClientError
33
def assert_case_insensitive(f):
34
def wrapper(*args, **kwargs):
35
if len(args) == 3 and not (args[2].islower() or args[2].isalnum()):
36
raise BotoClientError("Bucket names cannot contain upper-case " \
37
"characters when using either the sub-domain or virtual " \
38
"hosting calling format.")
39
return f(*args, **kwargs)
43
def build_url_base(self, protocol, server, bucket, key=''):
44
url_base = '%s://' % protocol
45
url_base += self.build_host(server, bucket)
46
url_base += self.build_path_base(bucket, key)
49
def build_host(self, server, bucket):
53
return self.get_bucket_server(server, bucket)
55
def build_auth_path(self, bucket, key=''):
59
return path + '/%s' % urllib.quote(key)
61
def build_path_base(self, bucket, key=''):
62
return '/%s' % urllib.quote(key)
64
class SubdomainCallingFormat(_CallingFormat):
65
@assert_case_insensitive
66
def get_bucket_server(self, server, bucket):
67
return '%s.%s' % (bucket, server)
69
class VHostCallingFormat(_CallingFormat):
70
@assert_case_insensitive
71
def get_bucket_server(self, server, bucket):
74
class OrdinaryCallingFormat(_CallingFormat):
75
def get_bucket_server(self, server, bucket):
78
def build_path_base(self, bucket, key=''):
81
path_base += "%s/" % bucket
82
return path_base + urllib.quote(key)
89
#boto.set_stream_logger('s3')
91
class S3Connection(AWSAuthConnection):
93
DefaultHost = 's3.amazonaws.com'
94
QueryString = 'Signature=%s&Expires=%d&AWSAccessKeyId=%s'
96
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
97
is_secure=True, port=None, proxy=None, proxy_port=None,
98
proxy_user=None, proxy_pass=None,
99
host=DefaultHost, debug=0, https_connection_factory=None,
100
calling_format=SubdomainCallingFormat(), path='/'):
101
self.calling_format = calling_format
102
AWSAuthConnection.__init__(self, host,
103
aws_access_key_id, aws_secret_access_key,
104
is_secure, port, proxy, proxy_port, proxy_user, proxy_pass,
105
debug=debug, https_connection_factory=https_connection_factory,
109
return self.get_all_buckets()
111
def __contains__(self, bucket_name):
112
return not (self.lookup(bucket_name) is None)
114
def build_post_policy(self, expiration_time, conditions):
116
Taken from the AWS book Python examples and modified for use with boto
118
if type(expiration_time) != time.struct_time:
119
raise 'Policy document must include a valid expiration Time object'
121
# Convert conditions object mappings to condition statements
123
return '{"expiration": "%s",\n"conditions": [%s]}' % \
124
(time.strftime(boto.utils.ISO8601, expiration_time), ",".join(conditions))
127
def build_post_form_args(self, bucket_name, key, expires_in = 6000,
128
acl = None, success_action_redirect = None, max_content_length = None,
129
http_method = "http", fields=None, conditions=None):
131
Taken from the AWS book Python examples and modified for use with boto
132
This only returns the arguments required for the post form, not the actual form
133
This does not return the file input field which also needs to be added
135
:param bucket_name: Bucket to submit to
136
:type bucket_name: string
138
:param key: Key name, optionally add ${filename} to the end to attach the submitted filename
141
:param expires_in: Time (in seconds) before this expires, defaults to 6000
142
:type expires_in: integer
144
:param acl: ACL rule to use, if any
145
:type acl: :class:`boto.s3.acl.ACL`
147
:param success_action_redirect: URL to redirect to on success
148
:type success_action_redirect: string
150
:param max_content_length: Maximum size for this file
151
:type max_content_length: integer
153
:type http_method: string
154
:param http_method: HTTP Method to use, "http" or "https"
158
:return: A dictionary containing field names/values as well as a url to POST to
160
.. code-block:: python
163
"action": action_url_to_post_to,
171
"value": field_value2
179
if conditions == None:
181
expiration = time.gmtime(int(time.time() + expires_in))
183
# Generate policy document
184
conditions.append('{"bucket": "%s"}' % bucket_name)
185
if key.endswith("${filename}"):
186
conditions.append('["starts-with", "$key", "%s"]' % key[:-len("${filename}")])
188
conditions.append('{"key": "%s"}' % key)
190
conditions.append('{"acl": "%s"}' % acl)
191
fields.append({ "name": "acl", "value": acl})
192
if success_action_redirect:
193
conditions.append('{"success_action_redirect": "%s"}' % success_action_redirect)
194
fields.append({ "name": "success_action_redirect", "value": success_action_redirect})
195
if max_content_length:
196
conditions.append('["content-length-range", 0, %i]' % max_content_length)
197
fields.append({"name":'content-length-range', "value": "0,%i" % max_content_length})
199
policy = self.build_post_policy(expiration, conditions)
201
# Add the base64-encoded policy document as the 'policy' field
202
policy_b64 = base64.b64encode(policy)
203
fields.append({"name": "policy", "value": policy_b64})
205
# Add the AWS access key as the 'AWSAccessKeyId' field
206
fields.append({"name": "AWSAccessKeyId", "value": self.aws_access_key_id})
208
# Add signature for encoded policy document as the 'AWSAccessKeyId' field
209
hmac_copy = self.hmac.copy()
210
hmac_copy.update(policy_b64)
211
signature = base64.encodestring(hmac_copy.digest()).strip()
212
fields.append({"name": "signature", "value": signature})
213
fields.append({"name": "key", "value": key})
215
# HTTPS protocol will be used if the secure HTTP option is enabled.
216
url = '%s://%s.s3.amazonaws.com/' % (http_method, bucket_name)
218
return {"action": url, "fields": fields}
221
def generate_url(self, expires_in, method, bucket='', key='',
222
headers=None, query_auth=True, force_http=False):
225
expires = int(time.time() + expires_in)
226
auth_path = self.calling_format.build_auth_path(bucket, key)
227
canonical_str = boto.utils.canonical_string(method, auth_path,
229
hmac_copy = self.hmac.copy()
230
hmac_copy.update(canonical_str)
231
b64_hmac = base64.encodestring(hmac_copy.digest()).strip()
232
encoded_canonical = urllib.quote_plus(b64_hmac)
233
self.calling_format.build_path_base(bucket, key)
235
query_part = '?' + self.QueryString % (encoded_canonical, expires,
236
self.aws_access_key_id)
237
if 'x-amz-security-token' in headers:
238
query_part += '&x-amz-security-token=%s' % urllib.quote(headers['x-amz-security-token']);
245
protocol = self.protocol
247
return self.calling_format.build_url_base(protocol, self.server_name(port),
248
bucket, key) + query_part
250
def get_all_buckets(self, headers=None):
251
response = self.make_request('GET')
252
body = response.read()
253
if response.status > 300:
254
raise S3ResponseError(response.status, response.reason, body, headers=headers)
255
rs = ResultSet([('Bucket', Bucket)])
256
h = handler.XmlHandler(rs, self)
257
xml.sax.parseString(body, h)
260
def get_canonical_user_id(self, headers=None):
262
Convenience method that returns the "CanonicalUserID" of the user who's credentials
263
are associated with the connection. The only way to get this value is to do a GET
264
request on the service which returns all buckets associated with the account. As part
265
of that response, the canonical userid is returned. This method simply does all of
266
that and then returns just the user id.
269
:return: A string containing the canonical user id.
271
rs = self.get_all_buckets(headers=headers)
274
def get_bucket(self, bucket_name, validate=True, headers=None):
275
bucket = Bucket(self, bucket_name)
277
bucket.get_all_keys(headers, maxkeys=0)
280
def lookup(self, bucket_name, validate=True, headers=None):
282
bucket = self.get_bucket(bucket_name, validate, headers=headers)
287
def create_bucket(self, bucket_name, headers=None,
288
location=Location.DEFAULT, policy=None):
290
Creates a new located bucket. By default it's in the USA. You can pass
291
Location.EU to create an European bucket.
293
:type bucket_name: string
294
:param bucket_name: The name of the new bucket
297
:param headers: Additional headers to pass along with the request to AWS.
299
:type location: :class:`boto.s3.connection.Location`
300
:param location: The location of the new bucket
302
:type policy: :class:`boto.s3.acl.CannedACLStrings`
303
:param policy: A canned ACL policy that will be applied to the new key in S3.
306
# TODO: Not sure what Exception Type from boto.exception to use.
307
if not bucket_name.islower():
308
raise Exception("Bucket names must be lower case.")
312
headers['x-amz-acl'] = policy
314
headers = {'x-amz-acl' : policy}
315
if location == Location.DEFAULT:
318
data = '<CreateBucketConstraint><LocationConstraint>' + \
319
location + '</LocationConstraint></CreateBucketConstraint>'
320
response = self.make_request('PUT', bucket_name, headers=headers,
322
body = response.read()
323
if response.status == 409:
324
raise S3CreateError(response.status, response.reason, body)
325
if response.status == 200:
326
return Bucket(self, bucket_name)
328
raise S3ResponseError(response.status, response.reason, body)
330
def delete_bucket(self, bucket, headers=None):
331
response = self.make_request('DELETE', bucket, headers=headers)
332
body = response.read()
333
if response.status != 204:
334
raise S3ResponseError(response.status, response.reason, body)
336
def make_request(self, method, bucket='', key='', headers=None, data='',
337
query_args=None, sender=None):
338
if isinstance(bucket, Bucket):
340
if isinstance(key, Key):
342
path = self.calling_format.build_path_base(bucket, key)
343
auth_path = self.calling_format.build_auth_path(bucket, key)
344
host = self.calling_format.build_host(self.server_name(), bucket)
346
path += '?' + query_args
347
auth_path += '?' + query_args
348
return AWSAuthConnection.make_request(self, method, path, headers,
349
data, host, auth_path, sender)