Skip to content

Commit 06e26ad

Browse files
committed
Merge pull request #823 from dhermes/decouple-connection-in-blob
Decouple connection from Blob
2 parents 40d84e9 + 60d43ac commit 06e26ad

File tree

7 files changed

+236
-146
lines changed

7 files changed

+236
-146
lines changed

gcloud/storage/_helpers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
from Crypto.Hash import MD5
2121
import base64
2222

23+
from gcloud.storage._implicit_environ import get_default_connection
24+
from gcloud.storage.batch import Batch
25+
2326

2427
class _PropertyMixin(object):
2528
"""Abstract mixin for cloud storage classes with associated propertties.
@@ -101,6 +104,30 @@ def patch(self):
101104
self._set_properties(api_response)
102105

103106

107+
def _require_connection(connection=None):
108+
"""Infer a connection from the environment, if not passed explicitly.
109+
110+
:type connection: :class:`gcloud.storage.connection.Connection`
111+
:param connection: Optional.
112+
113+
:rtype: :class:`gcloud.storage.connection.Connection`
114+
:returns: A connection based on the current environment.
115+
:raises: :class:`EnvironmentError` if ``connection`` is ``None``, and
116+
cannot be inferred from the environment.
117+
"""
118+
# NOTE: We use current Batch directly since it inherits from Connection.
119+
if connection is None:
120+
connection = Batch.current()
121+
122+
if connection is None:
123+
connection = get_default_connection()
124+
125+
if connection is None:
126+
raise EnvironmentError('Connection could not be inferred.')
127+
128+
return connection
129+
130+
104131
def _scalar_property(fieldname):
105132
"""Create a property descriptor around the :class:`_PropertyMixin` helpers.
106133
"""

gcloud/storage/api.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020

2121
from gcloud.exceptions import NotFound
2222
from gcloud._helpers import get_default_project
23-
from gcloud.storage._implicit_environ import get_default_connection
24-
from gcloud.storage.batch import Batch
23+
from gcloud.storage._helpers import _require_connection
2524
from gcloud.storage.bucket import Bucket
2625
from gcloud.storage.iterator import Iterator
2726

@@ -227,27 +226,3 @@ def get_items_from_response(self, response):
227226
bucket = Bucket(name, connection=self.connection)
228227
bucket._set_properties(item)
229228
yield bucket
230-
231-
232-
def _require_connection(connection=None):
233-
"""Infer a connection from the environment, if not passed explicitly.
234-
235-
:type connection: :class:`gcloud.storage.connection.Connection`
236-
:param connection: Optional.
237-
238-
:rtype: :class:`gcloud.storage.connection.Connection`
239-
:returns: A connection based on the current environment.
240-
:raises: :class:`EnvironmentError` if ``connection`` is ``None``, and
241-
cannot be inferred from the environment.
242-
"""
243-
# NOTE: We use current Batch directly since it inherits from Connection.
244-
if connection is None:
245-
connection = Batch.current()
246-
247-
if connection is None:
248-
connection = get_default_connection()
249-
250-
if connection is None:
251-
raise EnvironmentError('Connection could not be inferred.')
252-
253-
return connection

gcloud/storage/blob.py

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from gcloud.credentials import generate_signed_url
3232
from gcloud.exceptions import NotFound
3333
from gcloud.storage._helpers import _PropertyMixin
34+
from gcloud.storage._helpers import _require_connection
3435
from gcloud.storage._helpers import _scalar_property
3536
from gcloud.storage import _implicit_environ
3637
from gcloud.storage.acl import ObjectACL
@@ -164,7 +165,8 @@ def public_url(self):
164165
bucket_name=self.bucket.name,
165166
quoted_name=quote(self.name, safe=''))
166167

167-
def generate_signed_url(self, expiration, method='GET'):
168+
def generate_signed_url(self, expiration, method='GET',
169+
connection=None, credentials=None):
168170
"""Generates a signed URL for this blob.
169171
170172
If you have a blob that you want to allow access to for a set
@@ -181,6 +183,15 @@ def generate_signed_url(self, expiration, method='GET'):
181183
:type method: string
182184
:param method: The HTTP verb that will be used when requesting the URL.
183185
186+
:type connection: :class:`gcloud.storage.connection.Connection` or
187+
``NoneType``
188+
:param connection: Optional. The connection to use when sending
189+
requests. If not provided, falls back to default.
190+
191+
:type credentials: :class:`oauth2client.client.OAuth2Credentials` or
192+
:class:`NoneType`
193+
:param credentials: The OAuth2 credentials to use to sign the URL.
194+
184195
:rtype: string
185196
:returns: A signed URL you can use to access the resource
186197
until expiration.
@@ -189,23 +200,33 @@ def generate_signed_url(self, expiration, method='GET'):
189200
bucket_name=self.bucket.name,
190201
quoted_name=quote(self.name, safe=''))
191202

203+
if credentials is None:
204+
connection = _require_connection(connection)
205+
credentials = connection.credentials
206+
192207
return generate_signed_url(
193-
self.connection.credentials, resource=resource,
208+
credentials, resource=resource,
194209
api_access_endpoint=_API_ACCESS_ENDPOINT,
195210
expiration=expiration, method=method)
196211

197-
def exists(self):
212+
def exists(self, connection=None):
198213
"""Determines whether or not this blob exists.
199214
215+
:type connection: :class:`gcloud.storage.connection.Connection` or
216+
``NoneType``
217+
:param connection: Optional. The connection to use when sending
218+
requests. If not provided, falls back to default.
219+
200220
:rtype: boolean
201221
:returns: True if the blob exists in Cloud Storage.
202222
"""
223+
connection = _require_connection(connection)
203224
try:
204225
# We only need the status code (200 or not) so we seek to
205226
# minimize the returned payload.
206227
query_params = {'fields': 'name'}
207-
self.connection.api_request(method='GET', path=self.path,
208-
query_params=query_params)
228+
connection.api_request(method='GET', path=self.path,
229+
query_params=query_params)
209230
return True
210231
except NotFound:
211232
return False
@@ -242,15 +263,20 @@ def delete(self):
242263
"""
243264
return self.bucket.delete_blob(self.name)
244265

245-
def download_to_file(self, file_obj):
266+
def download_to_file(self, file_obj, connection=None):
246267
"""Download the contents of this blob into a file-like object.
247268
248269
:type file_obj: file
249270
:param file_obj: A file handle to which to write the blob's data.
250271
272+
:type connection: :class:`gcloud.storage.connection.Connection` or
273+
``NoneType``
274+
:param connection: Optional. The connection to use when sending
275+
requests. If not provided, falls back to default.
276+
251277
:raises: :class:`gcloud.exceptions.NotFound`
252278
"""
253-
279+
connection = _require_connection(connection)
254280
download_url = self.media_link
255281

256282
# Use apitools 'Download' facility.
@@ -261,41 +287,51 @@ def download_to_file(self, file_obj):
261287
headers['Range'] = 'bytes=0-%d' % (self.chunk_size - 1,)
262288
request = http_wrapper.Request(download_url, 'GET', headers)
263289

264-
download.InitializeDownload(request, self.connection.http)
290+
download.InitializeDownload(request, connection.http)
265291

266292
# Should we be passing callbacks through from caller? We can't
267293
# pass them as None, because apitools wants to print to the console
268294
# by default.
269295
download.StreamInChunks(callback=lambda *args: None,
270296
finish_callback=lambda *args: None)
271297

272-
def download_to_filename(self, filename):
298+
def download_to_filename(self, filename, connection=None):
273299
"""Download the contents of this blob into a named file.
274300
275301
:type filename: string
276302
:param filename: A filename to be passed to ``open``.
277303
304+
:type connection: :class:`gcloud.storage.connection.Connection` or
305+
``NoneType``
306+
:param connection: Optional. The connection to use when sending
307+
requests. If not provided, falls back to default.
308+
278309
:raises: :class:`gcloud.exceptions.NotFound`
279310
"""
280311
with open(filename, 'wb') as file_obj:
281-
self.download_to_file(file_obj)
312+
self.download_to_file(file_obj, connection=connection)
282313

283314
mtime = time.mktime(self.updated.timetuple())
284315
os.utime(file_obj.name, (mtime, mtime))
285316

286-
def download_as_string(self):
317+
def download_as_string(self, connection=None):
287318
"""Download the contents of this blob as a string.
288319
320+
:type connection: :class:`gcloud.storage.connection.Connection` or
321+
``NoneType``
322+
:param connection: Optional. The connection to use when sending
323+
requests. If not provided, falls back to default.
324+
289325
:rtype: bytes
290326
:returns: The data stored in this blob.
291327
:raises: :class:`gcloud.exceptions.NotFound`
292328
"""
293329
string_buffer = BytesIO()
294-
self.download_to_file(string_buffer)
330+
self.download_to_file(string_buffer, connection=connection)
295331
return string_buffer.getvalue()
296332

297333
def upload_from_file(self, file_obj, rewind=False, size=None,
298-
content_type=None, num_retries=6):
334+
content_type=None, num_retries=6, connection=None):
299335
"""Upload the contents of this blob from a file-like object.
300336
301337
The content type of the upload will either be
@@ -331,7 +367,13 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
331367
332368
:type num_retries: integer
333369
:param num_retries: Number of upload retries. Defaults to 6.
370+
371+
:type connection: :class:`gcloud.storage.connection.Connection` or
372+
``NoneType``
373+
:param connection: Optional. The connection to use when sending
374+
requests. If not provided, falls back to default.
334375
"""
376+
connection = _require_connection(connection)
335377
content_type = (content_type or self._properties.get('contentType') or
336378
'application/octet-stream')
337379

@@ -341,11 +383,10 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
341383

342384
# Get the basic stats about the file.
343385
total_bytes = size or os.fstat(file_obj.fileno()).st_size
344-
conn = self.connection
345386
headers = {
346387
'Accept': 'application/json',
347388
'Accept-Encoding': 'gzip, deflate',
348-
'User-Agent': conn.USER_AGENT,
389+
'User-Agent': connection.USER_AGENT,
349390
}
350391

351392
upload = transfer.Upload(file_obj, content_type, total_bytes,
@@ -357,20 +398,20 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
357398
upload_config = _UploadConfig()
358399

359400
# Temporary URL, until we know simple vs. resumable.
360-
base_url = conn.API_BASE_URL + '/upload'
361-
upload_url = conn.build_api_url(api_base_url=base_url,
362-
path=self.bucket.path + '/o')
401+
base_url = connection.API_BASE_URL + '/upload'
402+
upload_url = connection.build_api_url(api_base_url=base_url,
403+
path=self.bucket.path + '/o')
363404

364405
# Use apitools 'Upload' facility.
365406
request = http_wrapper.Request(upload_url, 'POST', headers)
366407

367408
upload.ConfigureRequest(upload_config, request, url_builder)
368409
query_params = url_builder.query_params
369-
base_url = conn.API_BASE_URL + '/upload'
370-
request.url = conn.build_api_url(api_base_url=base_url,
371-
path=self.bucket.path + '/o',
372-
query_params=query_params)
373-
upload.InitializeUpload(request, conn.http)
410+
base_url = connection.API_BASE_URL + '/upload'
411+
request.url = connection.build_api_url(api_base_url=base_url,
412+
path=self.bucket.path + '/o',
413+
query_params=query_params)
414+
upload.InitializeUpload(request, connection.http)
374415

375416
# Should we be passing callbacks through from caller? We can't
376417
# pass them as None, because apitools wants to print to the console
@@ -380,15 +421,16 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
380421
callback=lambda *args: None,
381422
finish_callback=lambda *args: None)
382423
else:
383-
http_response = http_wrapper.MakeRequest(conn.http, request,
424+
http_response = http_wrapper.MakeRequest(connection.http, request,
384425
retries=num_retries)
385426
response_content = http_response.content
386427
if not isinstance(response_content,
387428
six.string_types): # pragma: NO COVER Python3
388429
response_content = response_content.decode('utf-8')
389430
self._set_properties(json.loads(response_content))
390431

391-
def upload_from_filename(self, filename, content_type=None):
432+
def upload_from_filename(self, filename, content_type=None,
433+
connection=None):
392434
"""Upload this blob's contents from the content of a named file.
393435
394436
The content type of the upload will either be
@@ -412,15 +454,22 @@ def upload_from_filename(self, filename, content_type=None):
412454
413455
:type content_type: string or ``NoneType``
414456
:param content_type: Optional type of content being uploaded.
457+
458+
:type connection: :class:`gcloud.storage.connection.Connection` or
459+
``NoneType``
460+
:param connection: Optional. The connection to use when sending
461+
requests. If not provided, falls back to default.
415462
"""
416463
content_type = content_type or self._properties.get('contentType')
417464
if content_type is None:
418465
content_type, _ = mimetypes.guess_type(filename)
419466

420467
with open(filename, 'rb') as file_obj:
421-
self.upload_from_file(file_obj, content_type=content_type)
468+
self.upload_from_file(file_obj, content_type=content_type,
469+
connection=connection)
422470

423-
def upload_from_string(self, data, content_type='text/plain'):
471+
def upload_from_string(self, data, content_type='text/plain',
472+
connection=None):
424473
"""Upload contents of this blob from the provided string.
425474
426475
.. note::
@@ -437,14 +486,19 @@ def upload_from_string(self, data, content_type='text/plain'):
437486
:type data: bytes or text
438487
:param data: The data to store in this blob. If the value is
439488
text, it will be encoded as UTF-8.
489+
490+
:type connection: :class:`gcloud.storage.connection.Connection` or
491+
``NoneType``
492+
:param connection: Optional. The connection to use when sending
493+
requests. If not provided, falls back to default.
440494
"""
441495
if isinstance(data, six.text_type):
442496
data = data.encode('utf-8')
443497
string_buffer = BytesIO()
444498
string_buffer.write(data)
445499
self.upload_from_file(file_obj=string_buffer, rewind=True,
446-
size=len(data),
447-
content_type=content_type)
500+
size=len(data), content_type=content_type,
501+
connection=connection)
448502

449503
def make_public(self):
450504
"""Make this blob public giving all users read access."""

0 commit comments

Comments
 (0)