2
Client wrapper for Amazon's Simple Storage Service.
4
API stability: unstable.
6
Various API-incompatible changes are planned in order to expose missing
7
functionality in this wrapper.
12
from base64 import b64encode
15
from xml.etree.ElementTree import XML
17
from elementtree.ElementTree import XML
19
from epsilon.extime import Time
21
from twisted.web.client import getPage
22
from twisted.web.http import datetimeToString
25
def calculateMD5(data):
26
digest = md5.new(data).digest()
27
return b64encode(digest)
30
def hmac_sha1(secret, data):
31
digest = hmac.new(secret, data, sha).digest()
32
return b64encode(digest)
35
class S3Request(object):
36
def __init__(self, verb, bucket=None, objectName=None, data='',
37
contentType=None, metadata={}, rootURI='https://s3.amazonaws.com',
38
accessKey=None, secretKey=None):
41
self.objectName = objectName
43
self.contentType = contentType
44
self.metadata = metadata
45
self.rootURI = rootURI
46
self.accessKey = accessKey
47
self.secretKey = secretKey
48
self.date = datetimeToString()
50
if (accessKey is not None and secretKey is None) or (accessKey is None and secretKey is not None):
51
raise ValueError('Must provide both accessKey and secretKey, or neither')
55
if self.bucket is not None:
57
if self.objectName is not None:
58
path += '/' + self.objectName
62
return self.rootURI + self.getURIPath()
65
headers = {'Content-Length': len(self.data),
66
'Content-MD5': calculateMD5(self.data),
69
for key, value in self.metadata.iteritems():
70
headers['x-amz-meta-' + key] = value
72
if self.contentType is not None:
73
headers['Content-Type'] = self.contentType
75
if self.accessKey is not None:
76
signature = self.getSignature(headers)
77
headers['Authorization'] = 'AWS %s:%s' % (self.accessKey, signature)
81
def getCanonicalizedResource(self):
82
return self.getURIPath()
84
def getCanonicalizedAmzHeaders(self, headers):
86
headers = [(name.lower(), value) for name, value in headers.iteritems() if name.lower().startswith('x-amz-')]
88
return ''.join('%s:%s\n' % (name, value) for name, value in headers)
90
def getSignature(self, headers):
91
text = self.verb + '\n'
92
text += headers.get('Content-MD5', '') + '\n'
93
text += headers.get('Content-Type', '') + '\n'
94
text += headers.get('Date', '') + '\n'
95
text += self.getCanonicalizedAmzHeaders(headers)
96
text += self.getCanonicalizedResource()
97
return hmac_sha1(self.secretKey, text)
100
return self.getPage(url=self.getURI(), method=self.verb, postdata=self.data, headers=self.getHeaders())
102
def getPage(self, *a, **kw):
103
return getPage(*a, **kw)
106
NS = '{http://s3.amazonaws.com/doc/2006-03-01/}'
109
rootURI = 'https://s3.amazonaws.com/'
110
requestFactory = S3Request
112
def __init__(self, accessKey, secretKey):
113
self.accessKey = accessKey
114
self.secretKey = secretKey
116
def makeRequest(self, *a, **kw):
118
Create a request with the arguments passed in.
120
This uses the requestFactory attribute, adding the credentials to the
123
return self.requestFactory(accessKey=self.accessKey, secretKey=self.secretKey, *a, **kw)
125
def _parseBucketList(self, response):
127
Parse XML bucket list response.
130
for bucket in root.find(NS + 'Buckets'):
131
yield {'name': bucket.findtext(NS + 'Name'),
132
'created': Time.fromISO8601TimeAndDate(bucket.findtext(NS + 'CreationDate'))}
134
def listBuckets(self):
138
Returns a list of all the buckets owned by the authenticated sender of
141
return self.makeRequest('GET').submit().addCallback(self._parseBucketList)
143
def createBucket(self, bucket):
147
return self.makeRequest('PUT', bucket).submit()
149
def deleteBucket(self, bucket):
153
The bucket must be empty before it can be deleted.
155
return self.makeRequest('DELETE', bucket).submit()
157
def putObject(self, bucket, objectName, data, contentType=None, metadata={}):
159
Put an object in a bucket.
161
Any existing object of the same name will be replaced.
163
return self.makeRequest('PUT', bucket, objectName, data, contentType, metadata)
165
def getObject(self, bucket, objectName):
167
Get an object from a bucket.
169
return self.makeRequest('GET', bucket, objectName)
171
def headObject(self, bucket, objectName):
173
Retrieve object metadata only.
175
This is like getObject, but the object's content is not retrieved.
176
Currently the metadata is not returned to the caller either, so this
177
method is mostly useless, and only provided for completeness.
179
return self.makeRequest('HEAD', bucket, objectName)
181
def deleteObject(self, bucket, objectName):
183
Delete an object from a bucket.
185
Once deleted, there is no method to restore or undelete an object.
187
return self.makeRequest('DELETE', bucket, objectName)