Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple connection from Blob #823

Merged
merged 5 commits into from
Apr 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions gcloud/storage/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
from Crypto.Hash import MD5
import base64

from gcloud.storage._implicit_environ import get_default_connection
from gcloud.storage.batch import Batch


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


def _require_connection(connection=None):
"""Infer a connection from the environment, if not passed explicitly.

:type connection: :class:`gcloud.storage.connection.Connection`
:param connection: Optional.

:rtype: :class:`gcloud.storage.connection.Connection`
:returns: A connection based on the current environment.
:raises: :class:`EnvironmentError` if ``connection`` is ``None``, and
cannot be inferred from the environment.
"""
# NOTE: We use current Batch directly since it inherits from Connection.
if connection is None:
connection = Batch.current()

if connection is None:
connection = get_default_connection()

if connection is None:
raise EnvironmentError('Connection could not be inferred.')

return connection


def _scalar_property(fieldname):
"""Create a property descriptor around the :class:`_PropertyMixin` helpers.
"""
Expand Down
27 changes: 1 addition & 26 deletions gcloud/storage/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@

from gcloud.exceptions import NotFound
from gcloud._helpers import get_default_project
from gcloud.storage._implicit_environ import get_default_connection
from gcloud.storage.batch import Batch
from gcloud.storage._helpers import _require_connection
from gcloud.storage.bucket import Bucket
from gcloud.storage.iterator import Iterator

Expand Down Expand Up @@ -227,27 +226,3 @@ def get_items_from_response(self, response):
bucket = Bucket(name, connection=self.connection)
bucket._set_properties(item)
yield bucket


def _require_connection(connection=None):
"""Infer a connection from the environment, if not passed explicitly.

:type connection: :class:`gcloud.storage.connection.Connection`
:param connection: Optional.

:rtype: :class:`gcloud.storage.connection.Connection`
:returns: A connection based on the current environment.
:raises: :class:`EnvironmentError` if ``connection`` is ``None``, and
cannot be inferred from the environment.
"""
# NOTE: We use current Batch directly since it inherits from Connection.
if connection is None:
connection = Batch.current()

if connection is None:
connection = get_default_connection()

if connection is None:
raise EnvironmentError('Connection could not be inferred.')

return connection
112 changes: 83 additions & 29 deletions gcloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from gcloud.credentials import generate_signed_url
from gcloud.exceptions import NotFound
from gcloud.storage._helpers import _PropertyMixin
from gcloud.storage._helpers import _require_connection
from gcloud.storage._helpers import _scalar_property
from gcloud.storage import _implicit_environ
from gcloud.storage.acl import ObjectACL
Expand Down Expand Up @@ -164,7 +165,8 @@ def public_url(self):
bucket_name=self.bucket.name,
quoted_name=quote(self.name, safe=''))

def generate_signed_url(self, expiration, method='GET'):
def generate_signed_url(self, expiration, method='GET',
connection=None, credentials=None):
"""Generates a signed URL for this blob.

If you have a blob that you want to allow access to for a set
Expand All @@ -181,6 +183,15 @@ def generate_signed_url(self, expiration, method='GET'):
:type method: string
:param method: The HTTP verb that will be used when requesting the URL.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.

:type credentials: :class:`oauth2client.client.OAuth2Credentials` or
:class:`NoneType`
:param credentials: The OAuth2 credentials to use to sign the URL.

:rtype: string
:returns: A signed URL you can use to access the resource
until expiration.
Expand All @@ -189,23 +200,33 @@ def generate_signed_url(self, expiration, method='GET'):
bucket_name=self.bucket.name,
quoted_name=quote(self.name, safe=''))

if credentials is None:
connection = _require_connection(connection)
credentials = connection.credentials

return generate_signed_url(
self.connection.credentials, resource=resource,
credentials, resource=resource,
api_access_endpoint=_API_ACCESS_ENDPOINT,
expiration=expiration, method=method)

def exists(self):
def exists(self, connection=None):
"""Determines whether or not this blob exists.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.

:rtype: boolean
:returns: True if the blob exists in Cloud Storage.
"""
connection = _require_connection(connection)
try:
# We only need the status code (200 or not) so we seek to
# minimize the returned payload.
query_params = {'fields': 'name'}
self.connection.api_request(method='GET', path=self.path,
query_params=query_params)
connection.api_request(method='GET', path=self.path,
query_params=query_params)
return True
except NotFound:
return False
Expand Down Expand Up @@ -242,15 +263,20 @@ def delete(self):
"""
return self.bucket.delete_blob(self.name)

def download_to_file(self, file_obj):
def download_to_file(self, file_obj, connection=None):
"""Download the contents of this blob into a file-like object.

:type file_obj: file
:param file_obj: A file handle to which to write the blob's data.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.

:raises: :class:`gcloud.exceptions.NotFound`
"""

connection = _require_connection(connection)
download_url = self.media_link

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

download.InitializeDownload(request, self.connection.http)
download.InitializeDownload(request, connection.http)

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

def download_to_filename(self, filename):
def download_to_filename(self, filename, connection=None):
"""Download the contents of this blob into a named file.

:type filename: string
:param filename: A filename to be passed to ``open``.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.

:raises: :class:`gcloud.exceptions.NotFound`
"""
with open(filename, 'wb') as file_obj:
self.download_to_file(file_obj)
self.download_to_file(file_obj, connection=connection)

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

def download_as_string(self):
def download_as_string(self, connection=None):
"""Download the contents of this blob as a string.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.

:rtype: bytes
:returns: The data stored in this blob.
:raises: :class:`gcloud.exceptions.NotFound`
"""
string_buffer = BytesIO()
self.download_to_file(string_buffer)
self.download_to_file(string_buffer, connection=connection)
return string_buffer.getvalue()

def upload_from_file(self, file_obj, rewind=False, size=None,
content_type=None, num_retries=6):
content_type=None, num_retries=6, connection=None):
"""Upload the contents of this blob from a file-like object.

The content type of the upload will either be
Expand Down Expand Up @@ -331,7 +367,13 @@ def upload_from_file(self, file_obj, rewind=False, size=None,

:type num_retries: integer
:param num_retries: Number of upload retries. Defaults to 6.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.
"""
connection = _require_connection(connection)
content_type = (content_type or self._properties.get('contentType') or
'application/octet-stream')

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

# Get the basic stats about the file.
total_bytes = size or os.fstat(file_obj.fileno()).st_size
conn = self.connection
headers = {
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate',
'User-Agent': conn.USER_AGENT,
'User-Agent': connection.USER_AGENT,
}

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

# Temporary URL, until we know simple vs. resumable.
base_url = conn.API_BASE_URL + '/upload'
upload_url = conn.build_api_url(api_base_url=base_url,
path=self.bucket.path + '/o')
base_url = connection.API_BASE_URL + '/upload'
upload_url = connection.build_api_url(api_base_url=base_url,
path=self.bucket.path + '/o')

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

upload.ConfigureRequest(upload_config, request, url_builder)
query_params = url_builder.query_params
base_url = conn.API_BASE_URL + '/upload'
request.url = conn.build_api_url(api_base_url=base_url,
path=self.bucket.path + '/o',
query_params=query_params)
upload.InitializeUpload(request, conn.http)
base_url = connection.API_BASE_URL + '/upload'
request.url = connection.build_api_url(api_base_url=base_url,
path=self.bucket.path + '/o',
query_params=query_params)
upload.InitializeUpload(request, connection.http)

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

def upload_from_filename(self, filename, content_type=None):
def upload_from_filename(self, filename, content_type=None,
connection=None):
"""Upload this blob's contents from the content of a named file.

The content type of the upload will either be
Expand All @@ -412,15 +454,22 @@ def upload_from_filename(self, filename, content_type=None):

:type content_type: string or ``NoneType``
:param content_type: Optional type of content being uploaded.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.
"""
content_type = content_type or self._properties.get('contentType')
if content_type is None:
content_type, _ = mimetypes.guess_type(filename)

with open(filename, 'rb') as file_obj:
self.upload_from_file(file_obj, content_type=content_type)
self.upload_from_file(file_obj, content_type=content_type,
connection=connection)

def upload_from_string(self, data, content_type='text/plain'):
def upload_from_string(self, data, content_type='text/plain',
connection=None):
"""Upload contents of this blob from the provided string.

.. note::
Expand All @@ -437,14 +486,19 @@ def upload_from_string(self, data, content_type='text/plain'):
:type data: bytes or text
:param data: The data to store in this blob. If the value is
text, it will be encoded as UTF-8.

:type connection: :class:`gcloud.storage.connection.Connection` or
``NoneType``
:param connection: Optional. The connection to use when sending
requests. If not provided, falls back to default.
"""
if isinstance(data, six.text_type):
data = data.encode('utf-8')
string_buffer = BytesIO()
string_buffer.write(data)
self.upload_from_file(file_obj=string_buffer, rewind=True,
size=len(data),
content_type=content_type)
size=len(data), content_type=content_type,
connection=connection)

def make_public(self):
"""Make this blob public giving all users read access."""
Expand Down
Loading