-
-
Notifications
You must be signed in to change notification settings - Fork 867
/
Copy pathgcloud.py
277 lines (230 loc) · 9.71 KB
/
gcloud.py
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
import mimetypes
from datetime import timedelta
from tempfile import SpooledTemporaryFile
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from django.core.files.base import File
from django.core.files.storage import Storage
from django.utils import timezone
from django.utils.deconstruct import deconstructible
from django.utils.encoding import force_bytes, smart_str
from storages.utils import (
check_location, clean_name, get_available_overwrite_name, safe_join,
setting,
)
try:
from google.cloud.storage.client import Client
from google.cloud.storage.blob import Blob
from google.cloud.exceptions import NotFound
except ImportError:
raise ImproperlyConfigured("Could not load Google Cloud Storage bindings.\n"
"See https://github.com/GoogleCloudPlatform/gcloud-python")
class GoogleCloudFile(File):
def __init__(self, name, mode, storage):
self.name = name
self.mime_type = mimetypes.guess_type(name)[0]
self._mode = mode
self._storage = storage
self.blob = storage.bucket.get_blob(name)
if not self.blob and 'w' in mode:
self.blob = Blob(self.name, storage.bucket)
self._file = None
self._is_dirty = False
@property
def size(self):
return self.blob.size
def _get_file(self):
if self._file is None:
self._file = SpooledTemporaryFile(
max_size=self._storage.max_memory_size,
suffix=".GSStorageFile",
dir=setting("FILE_UPLOAD_TEMP_DIR")
)
if 'r' in self._mode:
self._is_dirty = False
self.blob.download_to_file(self._file)
self._file.seek(0)
return self._file
def _set_file(self, value):
self._file = value
file = property(_get_file, _set_file)
def read(self, num_bytes=None):
if 'r' not in self._mode:
raise AttributeError("File was not opened in read mode.")
if num_bytes is None:
num_bytes = -1
return super(GoogleCloudFile, self).read(num_bytes)
def write(self, content):
if 'w' not in self._mode:
raise AttributeError("File was not opened in write mode.")
self._is_dirty = True
return super(GoogleCloudFile, self).write(force_bytes(content))
def close(self):
if self._file is not None:
if self._is_dirty:
self.blob.upload_from_file(self.file, rewind=True,
content_type=self.mime_type)
self._file.close()
self._file = None
@deconstructible
class GoogleCloudStorage(Storage):
project_id = setting('GS_PROJECT_ID')
credentials = setting('GS_CREDENTIALS')
bucket_name = setting('GS_BUCKET_NAME')
location = setting('GS_LOCATION', '')
auto_create_bucket = setting('GS_AUTO_CREATE_BUCKET', False)
auto_create_acl = setting('GS_AUTO_CREATE_ACL', 'projectPrivate')
default_acl = setting('GS_DEFAULT_ACL')
expiration = setting('GS_EXPIRATION', timedelta(seconds=86400))
file_name_charset = setting('GS_FILE_NAME_CHARSET', 'utf-8')
file_overwrite = setting('GS_FILE_OVERWRITE', True)
cache_control = setting('GS_CACHE_CONTROL')
# The max amount of memory a returned file can take up before being
# rolled over into a temporary file on disk. Default is 0: Do not roll over.
max_memory_size = setting('GS_MAX_MEMORY_SIZE', 0)
def __init__(self, **settings):
# check if some of the settings we've provided as class attributes
# need to be overwritten with values passed in here
for name, value in settings.items():
if hasattr(self, name):
setattr(self, name, value)
check_location(self)
self._bucket = None
self._client = None
@property
def client(self):
if self._client is None:
self._client = Client(
project=self.project_id,
credentials=self.credentials
)
return self._client
@property
def bucket(self):
if self._bucket is None:
self._bucket = self._get_or_create_bucket(self.bucket_name)
return self._bucket
def _get_or_create_bucket(self, name):
"""
Retrieves a bucket if it exists, otherwise creates it.
"""
try:
return self.client.get_bucket(name)
except NotFound:
if self.auto_create_bucket:
bucket = self.client.create_bucket(name)
bucket.acl.save_predefined(self.auto_create_acl)
return bucket
raise ImproperlyConfigured("Bucket %s does not exist. Buckets "
"can be automatically created by "
"setting GS_AUTO_CREATE_BUCKET to "
"``True``." % name)
def _normalize_name(self, name):
"""
Normalizes the name so that paths like /path/to/ignored/../something.txt
and ./file.txt work. Note that clean_name adds ./ to some paths so
they need to be fixed here. We check to make sure that the path pointed
to is not outside the directory specified by the LOCATION setting.
"""
try:
return safe_join(self.location, name)
except ValueError:
raise SuspiciousOperation("Attempted access to '%s' denied." %
name)
def _encode_name(self, name):
return smart_str(name, encoding=self.file_name_charset)
def _open(self, name, mode='rb'):
name = self._normalize_name(clean_name(name))
file_object = GoogleCloudFile(name, mode, self)
if not file_object.blob:
raise IOError(u'File does not exist: %s' % name)
return file_object
def _save(self, name, content):
cleaned_name = clean_name(name)
name = self._normalize_name(cleaned_name)
content.name = cleaned_name
encoded_name = self._encode_name(name)
file = GoogleCloudFile(encoded_name, 'rw', self)
file.blob.cache_control = self.cache_control
if self.default_acl:
file.blob.upload_from_file(
content, rewind=True, size=content.size,
content_type=file.mime_type, predefined_acl=self.default_acl)
else:
file.blob.upload_from_file(
content, rewind=True, size=content.size,
content_type=file.mime_type)
return cleaned_name
def delete(self, name):
name = self._normalize_name(clean_name(name))
self.bucket.delete_blob(self._encode_name(name))
def exists(self, name):
if not name: # root element aka the bucket
try:
self.bucket
return True
except ImproperlyConfigured:
return False
name = self._normalize_name(clean_name(name))
return bool(self.bucket.get_blob(self._encode_name(name)))
def listdir(self, name):
name = self._normalize_name(clean_name(name))
# For bucket.list_blobs and logic below name needs to end in /
# but for the root path "" we leave it as an empty string
if name and not name.endswith('/'):
name += '/'
iterator = self.bucket.list_blobs(prefix=self._encode_name(name), delimiter='/')
blobs = list(iterator)
prefixes = iterator.prefixes
files = []
dirs = []
for blob in blobs:
parts = blob.name.split("/")
files.append(parts[-1])
for folder_path in prefixes:
parts = folder_path.split("/")
dirs.append(parts[-2])
return list(dirs), files
def _get_blob(self, name):
# Wrap google.cloud.storage's blob to raise if the file doesn't exist
blob = self.bucket.get_blob(name)
if blob is None:
raise NotFound(u'File does not exist: {}'.format(name))
return blob
def size(self, name):
name = self._normalize_name(clean_name(name))
blob = self._get_blob(self._encode_name(name))
return blob.size
def modified_time(self, name):
name = self._normalize_name(clean_name(name))
blob = self._get_blob(self._encode_name(name))
return timezone.make_naive(blob.updated)
def get_modified_time(self, name):
name = self._normalize_name(clean_name(name))
blob = self._get_blob(self._encode_name(name))
updated = blob.updated
return updated if setting('USE_TZ') else timezone.make_naive(updated)
def get_created_time(self, name):
"""
Return the creation time (as a datetime) of the file specified by name.
The datetime will be timezone-aware if USE_TZ=True.
"""
name = self._normalize_name(clean_name(name))
blob = self._get_blob(self._encode_name(name))
created = blob.time_created
return created if setting('USE_TZ') else timezone.make_naive(created)
def url(self, name):
"""
Return public url or a signed url for the Blob.
This DOES NOT check for existance of Blob - that makes codes too slow
for many use cases.
"""
name = self._normalize_name(clean_name(name))
blob = self.bucket.blob(self._encode_name(name))
if self.default_acl == 'publicRead':
return blob.public_url
return blob.generate_signed_url(self.expiration)
def get_available_name(self, name, max_length=None):
name = clean_name(name)
if self.file_overwrite:
return get_available_overwrite_name(name, max_length)
return super(GoogleCloudStorage, self).get_available_name(name, max_length)