Repository: googleapis/google-cloud-python

Issue:
Downloading factory blob fails because media_link isn't available
Creating a blob like this loads the blob's properties, so downloading works (if it exists) since the download_url is available:

``` python
blob = client.bucket(bucket_name).get_blob(blob_name)
blob.download_as_string() # works
```

But this doesn't load properties, so the download fails (even in the blob is valid):

``` python
blob = client.bucket(bucket_name).blob(blob_name) # note .blob() instead of .get_blob()
blob.download_as_string() # fail, no URI
```



Discussion:
Yea, this makes sense. We should fetch the metadata if it's not there already.


Resolving patch:
diff --git a/gcloud/storage/blob.py b/gcloud/storage/blob.py
--- 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.


Test patch:
diff --git a/gcloud/storage/test_blob.py b/gcloud/storage/test_blob.py
--- 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):