1 | import os
|
---|
2 | from mimetypes import guess_type
|
---|
3 |
|
---|
4 | try:
|
---|
5 | from cStringIO import StringIO
|
---|
6 | except ImportError:
|
---|
7 | from StringIO import StringIO
|
---|
8 |
|
---|
9 | from django.conf import settings
|
---|
10 | from django.core.exceptions import ImproperlyConfigured
|
---|
11 | from django.core.files.base import File
|
---|
12 | from django.core.files.storage import Storage
|
---|
13 | from django.utils.functional import curry
|
---|
14 | import ezPyCrypto
|
---|
15 |
|
---|
16 | ACCESS_KEY_NAME = 'AWS_ACCESS_KEY_ID'
|
---|
17 | SECRET_KEY_NAME = 'AWS_SECRET_ACCESS_KEY'
|
---|
18 | AWS_HEADERS = 'AWS_HEADERS'
|
---|
19 |
|
---|
20 | #ACCESS_KEY_NAME = settings.AWS_ACCESS_KEY_NAME
|
---|
21 | #SECRET_KEY_NAME = settings.AWS_SECRET_KEY_NAME
|
---|
22 | #AWS_HEADERS = settings.AWS_HEADERS
|
---|
23 |
|
---|
24 | try:
|
---|
25 | from S3 import AWSAuthConnection, QueryStringAuthGenerator
|
---|
26 | except ImportError:
|
---|
27 | raise ImproperlyConfigured, "Could not load amazon's S3 bindings.\
|
---|
28 | \nSee http://developer.amazonwebservices.com/connect/entry.jspa?externalID=134"
|
---|
29 |
|
---|
30 |
|
---|
31 | class S3Storage(Storage):
|
---|
32 | """Amazon Simple Storage Service"""
|
---|
33 |
|
---|
34 | def __init__(self, bucket=settings.AWS_STORAGE_BUCKET_NAME,
|
---|
35 | access_key=None, secret_key=None, acl='public-read',
|
---|
36 | calling_format=settings.AWS_CALLING_FORMAT, encrypt=False):
|
---|
37 | self.bucket = bucket
|
---|
38 | self.acl = acl
|
---|
39 | self.encrypt = encrypt
|
---|
40 |
|
---|
41 | if not access_key and not secret_key:
|
---|
42 | access_key, secret_key = self._get_access_keys()
|
---|
43 |
|
---|
44 | self.connection = AWSAuthConnection(access_key, secret_key,
|
---|
45 | calling_format=calling_format)
|
---|
46 | self.generator = QueryStringAuthGenerator(access_key, secret_key,
|
---|
47 | calling_format=calling_format, is_secure=False)
|
---|
48 |
|
---|
49 | self.headers = getattr(settings, AWS_HEADERS, {})
|
---|
50 |
|
---|
51 | def _get_access_keys(self):
|
---|
52 | access_key = getattr(settings, ACCESS_KEY_NAME, None)
|
---|
53 | secret_key = getattr(settings, SECRET_KEY_NAME, None)
|
---|
54 | if (access_key or secret_key) and (not access_key or not secret_key):
|
---|
55 | access_key = os.environ.get(ACCESS_KEY_NAME)
|
---|
56 | secret_key = os.environ.get(SECRET_KEY_NAME)
|
---|
57 |
|
---|
58 | if access_key and secret_key:
|
---|
59 | # Both were provided, so use them
|
---|
60 | return access_key, secret_key
|
---|
61 |
|
---|
62 | return None, None
|
---|
63 |
|
---|
64 | def _get_connection(self):
|
---|
65 | return AWSAuthConnection(*self._get_access_keys())
|
---|
66 |
|
---|
67 | def _put_file(self, name, content):
|
---|
68 | if self.encrypt == True:
|
---|
69 |
|
---|
70 | # Create a key object
|
---|
71 | k = ezPyCrypto.key()
|
---|
72 |
|
---|
73 | # Read in a public key
|
---|
74 | fd = open(settings.CRYPTO_KEYS_PUB, "rb")
|
---|
75 | pubkey = fd.read()
|
---|
76 | fd.close()
|
---|
77 |
|
---|
78 | # import this public key
|
---|
79 | k.importKey(pubkey)
|
---|
80 |
|
---|
81 | # Now encrypt some text against this public key
|
---|
82 | content = k.encString(content)
|
---|
83 |
|
---|
84 | content_type = guess_type(name)[0] or "application/x-octet-stream"
|
---|
85 | self.headers.update({'x-amz-acl': self.acl, 'Content-Type': content_type})
|
---|
86 | response = self.connection.put(self.bucket, name, content, self.headers)
|
---|
87 |
|
---|
88 | def _open(self, name, mode='rb'):
|
---|
89 | remote_file = S3StorageFile(name, self, mode=mode)
|
---|
90 | return remote_file
|
---|
91 |
|
---|
92 | def _read(self, name, start_range=None, end_range=None):
|
---|
93 | if start_range is None:
|
---|
94 | headers = {}
|
---|
95 | else:
|
---|
96 | headers = {'Range': 'bytes=%s-%s' % (start_range, end_range)}
|
---|
97 | response = self.connection.get(self.bucket, name, headers)
|
---|
98 | headers = response.http_response.msg
|
---|
99 |
|
---|
100 | if self.encrypt == True:
|
---|
101 | # Read in a private key
|
---|
102 | fd = open(settings.CRYPTO_KEYS_PRIV, "rb")
|
---|
103 | pubprivkey = fd.read()
|
---|
104 | fd.close()
|
---|
105 |
|
---|
106 | # Create a key object, and auto-import private key
|
---|
107 | k = ezPyCrypto.key(pubprivkey)
|
---|
108 |
|
---|
109 | # Decrypt this file
|
---|
110 | response.object.data = k.decString(response.object.data)
|
---|
111 |
|
---|
112 | return response.object.data, headers['etag'], headers.get('content-range', None)
|
---|
113 |
|
---|
114 | def _save(self, name, content):
|
---|
115 | content.open()
|
---|
116 | if hasattr(content, 'chunks'):
|
---|
117 | content_str = ''.join(chunk for chunk in content.chunks())
|
---|
118 | else:
|
---|
119 | content_str = content.read()
|
---|
120 | self._put_file(name, content_str)
|
---|
121 | return name
|
---|
122 |
|
---|
123 | def delete(self, name):
|
---|
124 | self.connection.delete(self.bucket, name)
|
---|
125 |
|
---|
126 | def exists(self, name):
|
---|
127 | response = self.connection._make_request('HEAD', self.bucket, name)
|
---|
128 | return response.status == 200
|
---|
129 |
|
---|
130 | def size(self, name):
|
---|
131 | response = self.connection._make_request('HEAD', self.bucket, name)
|
---|
132 | content_length = response.getheader('Content-Length')
|
---|
133 | return content_length and int(content_length) or 0
|
---|
134 |
|
---|
135 | def url(self, name):
|
---|
136 | return self.generator.make_bare_url(self.bucket, name)
|
---|
137 |
|
---|
138 | ## UNCOMMENT BELOW IF NECESSARY
|
---|
139 | #def get_available_name(self, name):
|
---|
140 | # """ Overwrite existing file with the same name. """
|
---|
141 | # return name
|
---|
142 |
|
---|
143 |
|
---|
144 | class S3StorageFile(File):
|
---|
145 | def __init__(self, name, storage, mode):
|
---|
146 | self._name = name
|
---|
147 | self._storage = storage
|
---|
148 | self._mode = mode
|
---|
149 | self._is_dirty = False
|
---|
150 | self.file = StringIO()
|
---|
151 | self.start_range = 0
|
---|
152 |
|
---|
153 | @property
|
---|
154 | def size(self):
|
---|
155 | if not hasattr(self, '_size'):
|
---|
156 | self._size = self._storage.size(self._name)
|
---|
157 | return self._size
|
---|
158 |
|
---|
159 | def read(self, num_bytes=None):
|
---|
160 | if num_bytes is None:
|
---|
161 | args = []
|
---|
162 | self.start_range = 0
|
---|
163 | else:
|
---|
164 | args = [self.start_range, self.start_range+num_bytes-1]
|
---|
165 | data, etags, content_range = self._storage._read(self._name, *args)
|
---|
166 | if content_range is not None:
|
---|
167 | current_range, size = content_range.split(' ', 1)[1].split('/', 1)
|
---|
168 | start_range, end_range = current_range.split('-', 1)
|
---|
169 | self._size, self.start_range = int(size), int(end_range)+1
|
---|
170 | self.file = StringIO(data)
|
---|
171 | return self.file.getvalue()
|
---|
172 |
|
---|
173 | def write(self, content):
|
---|
174 | if 'w' not in self._mode:
|
---|
175 | raise AttributeError("File was opened for read-only access.")
|
---|
176 | self.file = StringIO(content)
|
---|
177 | self._is_dirty = True
|
---|
178 |
|
---|
179 | def close(self):
|
---|
180 | if self._is_dirty:
|
---|
181 | self._storage._put_file(self._name, self.file.getvalue())
|
---|
182 | self.file.close()
|
---|