diff --git a/gcloud/storage/blob.py b/gcloud/storage/blob.py index c8ced994f856..b39765ef835c 100644 --- a/gcloud/storage/blob.py +++ b/gcloud/storage/blob.py @@ -277,6 +277,11 @@ def delete(self, client=None): def download_to_file(self, file_obj, client=None): """Download the contents of this blob into a file-like object. + .. note:: + + If the server-set property, :attr:`media_link`, is not yet + initialized, makes an additional API request to load it. + :type file_obj: file :param file_obj: A file handle to which to write the blob's data. @@ -287,6 +292,9 @@ def download_to_file(self, file_obj, client=None): :raises: :class:`gcloud.exceptions.NotFound` """ client = self._require_client(client) + if self.media_link is None: # not yet loaded + self.reload() + download_url = self.media_link # Use apitools 'Download' facility. diff --git a/gcloud/storage/test_blob.py b/gcloud/storage/test_blob.py index 868498555587..2d7778357a22 100644 --- a/gcloud/storage/test_blob.py +++ b/gcloud/storage/test_blob.py @@ -260,7 +260,7 @@ def test_generate_signed_url_w_method_arg(self): def test_exists_miss(self): from six.moves.http_client import NOT_FOUND NONESUCH = 'nonesuch' - not_found_response = {'status': NOT_FOUND} + not_found_response = ({'status': NOT_FOUND}, b'') connection = _Connection(not_found_response) client = _Client(connection) bucket = _Bucket(client) @@ -270,7 +270,7 @@ def test_exists_miss(self): def test_exists_hit(self): from six.moves.http_client import OK BLOB_NAME = 'blob-name' - found_response = {'status': OK} + found_response = ({'status': OK}, b'') connection = _Connection(found_response) client = _Client(connection) bucket = _Bucket(client) @@ -281,7 +281,7 @@ def test_exists_hit(self): def test_delete(self): from six.moves.http_client import NOT_FOUND BLOB_NAME = 'blob-name' - not_found_response = {'status': NOT_FOUND} + not_found_response = ({'status': NOT_FOUND}, b'') connection = _Connection(not_found_response) client = _Client(connection) bucket = _Bucket(client) @@ -291,6 +291,32 @@ def test_delete(self): self.assertFalse(blob.exists()) self.assertEqual(bucket._deleted, [(BLOB_NAME, None)]) + def test_download_to_file_wo_media_link(self): + from six.moves.http_client import OK + from six.moves.http_client import PARTIAL_CONTENT + from io import BytesIO + BLOB_NAME = 'blob-name' + MEDIA_LINK = 'http://example.com/media/' + chunk1_response = {'status': PARTIAL_CONTENT, + 'content-range': 'bytes 0-2/6'} + chunk2_response = {'status': OK, + 'content-range': 'bytes 3-5/6'} + connection = _Connection( + (chunk1_response, b'abc'), + (chunk2_response, b'def'), + ) + # Only the 'reload' request hits on this side: the others are done + # through the 'http' object. + reload_response = {'status': OK, 'content-type': 'application/json'} + connection._responses = [(reload_response, {"mediaLink": MEDIA_LINK})] + client = _Client(connection) + bucket = _Bucket(client) + blob = self._makeOne(BLOB_NAME, bucket=bucket) + fh = BytesIO() + blob.download_to_file(fh) + self.assertEqual(fh.getvalue(), b'abcdef') + self.assertEqual(blob.media_link, MEDIA_LINK) + def _download_to_file_helper(self, chunk_size=None): from six.moves.http_client import OK from six.moves.http_client import PARTIAL_CONTENT @@ -749,10 +775,11 @@ def test_upload_from_string_w_text(self): self.assertEqual(rq[0]['body'], ENCODED) def test_make_public(self): + from six.moves.http_client import OK from gcloud.storage.acl import _ACLEntity BLOB_NAME = 'blob-name' permissive = [{'entity': 'allUsers', 'role': _ACLEntity.READER_ROLE}] - after = {'acl': permissive} + after = ({'status': OK}, {'acl': permissive}) connection = _Connection(after) client = _Client(connection) bucket = _Bucket(client=client) @@ -1092,10 +1119,10 @@ def __init__(self, *responses): def api_request(self, **kw): from six.moves.http_client import NOT_FOUND from gcloud.exceptions import NotFound - result = self._respond(**kw) - if result.get('status') == NOT_FOUND: - raise NotFound(result) - return result + info, content = self._respond(**kw) + if info.get('status') == NOT_FOUND: + raise NotFound(info) + return content def build_api_url(self, path, query_params=None, api_base_url=API_BASE_URL):