1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
|
import os
import mimetypes
import warnings
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from django.conf import settings
from django.core.files.base import File
from django.core.files.storage import Storage
from django.core.exceptions import ImproperlyConfigured
try:
from S3 import AWSAuthConnection, QueryStringAuthGenerator, CallingFormat
except ImportError:
raise ImproperlyConfigured("Could not load amazon's S3 bindings.\nSee "
"http://developer.amazonwebservices.com/connect/entry.jspa?externalID=134")
ACCESS_KEY_NAME = getattr(settings, 'AWS_S3_ACCESS_KEY_ID', getattr(settings, 'AWS_ACCESS_KEY_ID', None))
SECRET_KEY_NAME = getattr(settings, 'AWS_S3_SECRET_ACCESS_KEY', getattr(settings, 'AWS_SECRET_ACCESS_KEY', None))
HEADERS = getattr(settings, 'AWS_HEADERS', {})
DEFAULT_ACL = getattr(settings, 'AWS_DEFAULT_ACL', 'public-read')
QUERYSTRING_ACTIVE = getattr(settings, 'AWS_QUERYSTRING_ACTIVE', False)
QUERYSTRING_EXPIRE = getattr(settings, 'AWS_QUERYSTRING_EXPIRE', 60)
SECURE_URLS = getattr(settings, 'AWS_S3_SECURE_URLS', False)
BUCKET_PREFIX = getattr(settings, 'AWS_BUCKET_PREFIX', '')
CALLING_FORMAT = getattr(settings, 'AWS_CALLING_FORMAT', CallingFormat.PATH)
PRELOAD_METADATA = getattr(settings, 'AWS_PRELOAD_METADATA', False)
IS_GZIPPED = getattr(settings, 'AWS_IS_GZIPPED', False)
GZIP_CONTENT_TYPES = getattr(settings, 'GZIP_CONTENT_TYPES', (
'text/css',
'application/javascript',
'application/x-javascript'
))
if IS_GZIPPED:
from gzip import GzipFile
class S3Storage(Storage):
"""Amazon Simple Storage Service"""
def __init__(self, bucket=settings.AWS_STORAGE_BUCKET_NAME,
access_key=None, secret_key=None, acl=DEFAULT_ACL,
calling_format=CALLING_FORMAT, encrypt=False,
gzip=IS_GZIPPED, gzip_content_types=GZIP_CONTENT_TYPES,
preload_metadata=PRELOAD_METADATA):
warnings.warn(
"The s3 backend is deprecated and will be removed in version 1.2. "
"Use the s3boto backend instead.",
PendingDeprecationWarning
)
self.bucket = bucket
self.acl = acl
self.encrypt = encrypt
self.gzip = gzip
self.gzip_content_types = gzip_content_types
self.preload_metadata = preload_metadata
if encrypt:
try:
import ezPyCrypto
except ImportError:
raise ImproperlyConfigured("Could not load ezPyCrypto.\nSee "
"http://www.freenet.org.nz/ezPyCrypto/ to install it.")
self.crypto_key = ezPyCrypto.key
if not access_key and not secret_key:
access_key, secret_key = self._get_access_keys()
self.connection = AWSAuthConnection(access_key, secret_key,
calling_format=calling_format)
self.generator = QueryStringAuthGenerator(access_key, secret_key,
calling_format=calling_format,
is_secure=SECURE_URLS)
self.generator.set_expires_in(QUERYSTRING_EXPIRE)
self.headers = HEADERS
self._entries = {}
def _get_access_keys(self):
access_key = ACCESS_KEY_NAME
secret_key = SECRET_KEY_NAME
if (access_key or secret_key) and (not access_key or not secret_key):
access_key = os.environ.get(ACCESS_KEY_NAME)
secret_key = os.environ.get(SECRET_KEY_NAME)
if access_key and secret_key:
# Both were provided, so use them
return access_key, secret_key
return None, None
@property
def entries(self):
if self.preload_metadata and not self._entries:
self._entries = dict((entry.key, entry)
for entry in self.connection.list_bucket(self.bucket).entries)
return self._entries
def _get_connection(self):
return AWSAuthConnection(*self._get_access_keys())
def _clean_name(self, name):
# Useful for windows' paths
return os.path.join(BUCKET_PREFIX, os.path.normpath(name).replace('\\', '/'))
def _compress_string(self, s):
"""Gzip a given string."""
zbuf = StringIO()
zfile = GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
zfile.write(s)
zfile.close()
return zbuf.getvalue()
def _put_file(self, name, content):
if self.encrypt:
# Create a key object
key = self.crypto_key()
# Read in a public key
fd = open(settings.CRYPTO_KEYS_PUBLIC, "rb")
public_key = fd.read()
fd.close()
# import this public key
key.importKey(public_key)
# Now encrypt some text against this public key
content = key.encString(content)
content_type = mimetypes.guess_type(name)[0] or "application/x-octet-stream"
if self.gzip and content_type in self.gzip_content_types:
content = self._compress_string(content)
self.headers.update({'Content-Encoding': 'gzip'})
self.headers.update({
'x-amz-acl': self.acl,
'Content-Type': content_type,
'Content-Length' : str(len(content)),
})
response = self.connection.put(self.bucket, name, content, self.headers)
if response.http_response.status not in (200, 206):
raise IOError("S3StorageError: %s" % response.message)
def _open(self, name, mode='rb'):
name = self._clean_name(name)
remote_file = S3StorageFile(name, self, mode=mode)
return remote_file
def _read(self, name, start_range=None, end_range=None):
name = self._clean_name(name)
if start_range is None:
headers = {}
else:
headers = {'Range': 'bytes=%s-%s' % (start_range, end_range)}
response = self.connection.get(self.bucket, name, headers)
if response.http_response.status not in (200, 206):
raise IOError("S3StorageError: %s" % response.message)
headers = response.http_response.msg
if self.encrypt:
# Read in a private key
fd = open(settings.CRYPTO_KEYS_PRIVATE, "rb")
private_key = fd.read()
fd.close()
# Create a key object, and auto-import private key
key = self.crypto_key(private_key)
# Decrypt this file
response.object.data = key.decString(response.object.data)
return response.object.data, headers.get('etag', None), headers.get('content-range', None)
def _save(self, name, content):
name = self._clean_name(name)
content.open()
if hasattr(content, 'chunks'):
content_str = ''.join(chunk for chunk in content.chunks())
else:
content_str = content.read()
self._put_file(name, content_str)
return name
def delete(self, name):
name = self._clean_name(name)
response = self.connection.delete(self.bucket, name)
if response.http_response.status != 204:
raise IOError("S3StorageError: %s" % response.message)
def exists(self, name):
name = self._clean_name(name)
if self.entries:
return name in self.entries
response = self.connection._make_request('HEAD', self.bucket, name)
return response.status == 200
def size(self, name):
name = self._clean_name(name)
if self.entries:
entry = self.entries.get(name)
if entry:
return entry.size
return 0
response = self.connection._make_request('HEAD', self.bucket, name)
content_length = response.getheader('Content-Length')
return content_length and int(content_length) or 0
def url(self, name):
name = self._clean_name(name)
if QUERYSTRING_ACTIVE:
return self.generator.generate_url('GET', self.bucket, name)
else:
return self.generator.make_bare_url(self.bucket, name)
def modified_time(self, name):
try:
from dateutil import parser, tz
except ImportError:
raise NotImplementedError()
name = self._clean_name(name)
if self.entries:
last_modified = self.entries.get(name).last_modified
else:
response = self.connection._make_request('HEAD', self.bucket, name)
last_modified = response.getheader('Last-Modified')
# convert to string to date
last_modified_date = parser.parse(last_modified)
# if the date has no timzone, assume UTC
if last_modified_date.tzinfo == None:
last_modified_date = last_modified_date.replace(tzinfo=tz.tzutc())
# convert date to local time w/o timezone
return last_modified_date.astimezone(tz.tzlocal()).replace(tzinfo=None)
## UNCOMMENT BELOW IF NECESSARY
#def get_available_name(self, name):
# """ Overwrite existing file with the same name. """
# name = self._clean_name(name)
# return name
class PreloadingS3Storage(S3Storage):
pass
class S3StorageFile(File):
def __init__(self, name, storage, mode):
self._name = name
self._storage = storage
self._mode = mode
self._is_dirty = False
self.file = StringIO()
self.start_range = 0
@property
def size(self):
if not hasattr(self, '_size'):
self._size = self._storage.size(self._name)
return self._size
def read(self, num_bytes=None):
if num_bytes is None:
args = []
self.start_range = 0
else:
args = [self.start_range, self.start_range+num_bytes-1]
data, etags, content_range = self._storage._read(self._name, *args)
if content_range is not None:
current_range, size = content_range.split(' ', 1)[1].split('/', 1)
start_range, end_range = current_range.split('-', 1)
self._size, self.start_range = int(size), int(end_range)+1
self.file = StringIO(data)
return self.file.getvalue()
def write(self, content):
if 'w' not in self._mode:
raise AttributeError("File was opened for read-only access.")
self.file = StringIO(content)
self._is_dirty = True
def close(self):
if self._is_dirty:
self._storage._put_file(self._name, self.file.getvalue())
self.file.close()
|