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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
|
"""
Custom storage for django with Mosso Cloud Files backend.
Created by Rich Leland <rich@richleland.com>.
"""
import os
import warnings
warnings.simplefilter('always', PendingDeprecationWarning)
warnings.warn("The mosso module will be deprecated in version 1.2 of "
"django-storages. The CloudFiles code has been moved into"
"django-cumulus at http://github.com/richleland/django-cumulus.",
PendingDeprecationWarning)
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.files import File
from django.core.files.storage import Storage
from django.utils.text import get_valid_filename
try:
from cStringIO import StringIO
except:
from StringIO import StringIO
try:
import cloudfiles
from cloudfiles.errors import NoSuchObject
except ImportError:
raise ImproperlyConfigured("Could not load cloudfiles dependency. See "
"http://www.mosso.com/cloudfiles.jsp.")
# TODO: implement TTL into cloudfiles methods
TTL = getattr(settings, 'CLOUDFILES_TTL', 600)
CONNECTION_KWARGS = getattr(settings, 'CLOUDFILES_CONNECTION_KWARGS', {})
SSL = getattr(settings, 'CLOUDFILES_SSL', False)
def cloudfiles_upload_to(self, filename):
"""
Simple, custom upload_to because Cloud Files doesn't support
nested containers (directories).
Actually found this out from @minter:
@richleland The Cloud Files APIs do support pseudo-subdirectories, by
creating zero-byte files with type application/directory.
May implement in a future version.
"""
return get_valid_filename(filename)
class CloudFilesStorage(Storage):
"""
Custom storage for Mosso Cloud Files.
"""
default_quick_listdir = True
def __init__(self,
username=settings.CLOUDFILES_USERNAME,
api_key=settings.CLOUDFILES_API_KEY,
container=settings.CLOUDFILES_CONTAINER,
connection_kwargs=CONNECTION_KWARGS):
"""
Initialize the settings for the connection and container.
"""
self.username = username
self.api_key = api_key
self.container_name = container
self.connection_kwargs = connection_kwargs
def __getstate__(self):
"""
Return a picklable representation of the storage.
"""
return dict(username=self.username,
api_key=self.api_key,
container_name=self.container_name,
connection_kwargs=self.connection_kwargs)
def _get_connection(self):
if not hasattr(self, '_connection'):
self._connection = cloudfiles.get_connection(self.username,
self.api_key, **self.connection_kwargs)
return self._connection
def _set_connection(self, value):
self._connection = value
connection = property(_get_connection, _set_connection)
def _get_container(self):
if not hasattr(self, '_container'):
self.container = self.connection.get_container(
self.container_name)
return self._container
def _set_container(self, container):
"""
Set the container, making it publicly available (on Limelight CDN) if
it is not already.
"""
if not container.is_public():
container.make_public()
if hasattr(self, '_container_public_uri'):
delattr(self, '_container_public_uri')
self._container = container
container = property(_get_container, _set_container)
def _get_container_url(self):
if not hasattr(self, '_container_public_uri'):
if SSL:
self._container_public_uri = self.container.public_ssl_uri()
else:
self._container_public_uri = self.container.public_uri()
return self._container_public_uri
container_url = property(_get_container_url)
def _get_cloud_obj(self, name):
"""
Helper function to get retrieve the requested Cloud Files Object.
"""
return self.container.get_object(name)
def _open(self, name, mode='rb'):
"""
Return the CloudFilesStorageFile.
"""
return CloudFilesStorageFile(storage=self, name=name)
def _save(self, name, content):
"""
Use the Cloud Files service to write ``content`` to a remote file
(called ``name``).
"""
(path, last) = os.path.split(name)
if path:
try:
self.container.get_object(path)
except NoSuchObject:
self._save(path, CloudStorageDirectory(path))
cloud_obj = self.container.create_object(name)
cloud_obj.size = content.size
content.open()
# If the content type is available, pass it in directly rather than
# getting the cloud object to try to guess.
if hasattr(content.file, 'content_type'):
cloud_obj.content_type = content.file.content_type
cloud_obj.send(content)
content.close()
return name
def delete(self, name):
"""
Deletes the specified file from the storage system.
"""
# If the file exists, delete it.
if self.exists(name):
self.container.delete_object(name)
def exists(self, name):
"""
Returns True if a file referenced by the given name already exists in
the storage system, or False if the name is available for a new file.
"""
try:
self._get_cloud_obj(name)
return True
except NoSuchObject:
return False
def listdir(self, path):
"""
Lists the contents of the specified path, returning a 2-tuple; the
first being an empty list of directories (not available for quick-
listing), the second being a list of filenames.
If the list of directories is required, use the full_listdir method.
"""
files = []
if path and not path.endswith('/'):
path = '%s/' % path
path_len = len(path)
for name in self.container.list_objects(path=path):
files.append(name[path_len:])
return ([], files)
def full_listdir(self, path):
"""
Lists the contents of the specified path, returning a 2-tuple of lists;
the first item being directories, the second item being files.
On large containers, this may be a slow operation for root containers
because every single object must be returned (cloudfiles does not
provide an explicit way of listing directories).
"""
dirs = set()
files = []
if path and not path.endswith('/'):
path = '%s/' % path
path_len = len(path)
for name in self.container.list_objects(prefix=path):
name = name[path_len:]
slash = name[1:-1].find('/') + 1
if slash:
dirs.add(name[:slash])
elif name:
files.append(name)
dirs = list(dirs)
dirs.sort()
return (dirs, files)
def size(self, name):
"""
Returns the total size, in bytes, of the file specified by name.
"""
return self._get_cloud_obj(name).size
def url(self, name):
"""
Returns an absolute URL where the file's contents can be accessed
directly by a web browser.
"""
return '%s/%s' % (self.container_url, name)
class CloudStorageDirectory(File):
"""
A File-like object that creates a directory at cloudfiles
"""
def __init__(self, name):
super(CloudStorageDirectory, self).__init__(StringIO(), name=name)
self.file.content_type = 'application/directory'
self.size = 0
def __str__(self):
return 'directory'
def __nonzero__(self):
return True
def open(self, mode=None):
self.seek(0)
def close(self):
pass
class CloudFilesStorageFile(File):
closed = False
def __init__(self, storage, name, *args, **kwargs):
self._storage = storage
super(CloudFilesStorageFile, self).__init__(file=None, name=name,
*args, **kwargs)
self._pos = 0
def _get_size(self):
if not hasattr(self, '_size'):
self._size = self._storage.size(self.name)
return self._size
def _set_size(self, size):
self._size = size
size = property(_get_size, _set_size)
def _get_file(self):
if not hasattr(self, '_file'):
self._file = self._storage._get_cloud_obj(self.name)
return self._file
def _set_file(self, value):
if value is None:
if hasattr(self, '_file'):
del self._file
else:
self._file = value
file = property(_get_file, _set_file)
def read(self, num_bytes=None):
if self._pos == self._get_size():
return None
if self._pos + num_bytes > self._get_size():
num_bytes = self._get_size() - self._pos
data = self.file.read(size=num_bytes or -1, offset=self._pos)
self._pos += len(data)
return data
def open(self, *args, **kwargs):
"""
Open the cloud file object.
"""
self.file
self._pos = 0
def close(self, *args, **kwargs):
self._pos = 0
@property
def closed(self):
return not hasattr(self, '_file')
def seek(self, pos):
self._pos = pos
class ThreadSafeCloudFilesStorage(CloudFilesStorage):
"""
Extends CloudFilesStorage to make it thread safer.
As long as you don't pass container or cloud objects
between threads, you'll be thread safe.
Uses one cloudfiles connection per thread.
"""
def __init__(self, *args, **kwargs):
super(ThreadSafeCloudFilesStorage, self).__init__(*args, **kwargs)
import threading
self.local_cache = threading.local()
def _get_connection(self):
if not hasattr(self.local_cache, 'connection'):
connection = cloudfiles.get_connection(self.username,
self.api_key, **self.connection_kwargs)
self.local_cache.connection = connection
return self.local_cache.connection
connection = property(_get_connection, CloudFilesStorage._set_connection)
def _get_container(self):
if not hasattr(self.local_cache, 'container'):
container = self.connection.get_container(self.container_name)
self.local_cache.container = container
return self.local_cache.container
container = property(_get_container, CloudFilesStorage._set_container)
|