Skip to content

Commit ec8aa13

Browse files
authored
Adding error remapping for Blob.download_to_file(). (#3338)
1 parent df30d1a commit ec8aa13

File tree

2 files changed

+130
-20
lines changed

2 files changed

+130
-20
lines changed

storage/google/cloud/storage/blob.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,36 @@ def _get_download_url(self):
338338
else:
339339
return self.media_link
340340

341+
def _do_download(self, transport, file_obj, download_url, headers):
342+
"""Perform a download without any error handling.
343+
344+
This is intended to be called by :meth:`download_to_file` so it can
345+
be wrapped with error handling / remapping.
346+
347+
:type transport:
348+
:class:`~google.auth.transport.requests.AuthorizedSession`
349+
:param transport: The transport (with credentials) that will
350+
make authenticated requests.
351+
352+
:type file_obj: file
353+
:param file_obj: A file handle to which to write the blob's data.
354+
355+
:type download_url: str
356+
:param download_url: The URL where the media can be accessed.
357+
358+
:type headers: dict
359+
:param headers: Optional headers to be sent with the request(s).
360+
"""
361+
if self.chunk_size is None:
362+
download = resumable_media.Download(download_url, headers=headers)
363+
response = download.consume(transport)
364+
file_obj.write(response.content)
365+
else:
366+
download = resumable_media.ChunkedDownload(
367+
download_url, self.chunk_size, file_obj, headers=headers)
368+
while not download.finished:
369+
download.consume_next_chunk(transport)
370+
341371
def download_to_file(self, file_obj, client=None):
342372
"""Download the contents of this blob into a file-like object.
343373
@@ -378,16 +408,13 @@ def download_to_file(self, file_obj, client=None):
378408
transport = google.auth.transport.requests.AuthorizedSession(
379409
client._credentials)
380410

381-
# Download the content.
382-
if self.chunk_size is None:
383-
download = resumable_media.Download(download_url, headers=headers)
384-
response = download.consume(transport)
385-
file_obj.write(response.content)
386-
else:
387-
download = resumable_media.ChunkedDownload(
388-
download_url, self.chunk_size, file_obj, headers=headers)
389-
while not download.finished:
390-
download.consume_next_chunk(transport)
411+
try:
412+
self._do_download(transport, file_obj, download_url, headers)
413+
except resumable_media.InvalidResponse as exc:
414+
response = exc.response
415+
faux_response = httplib2.Response({'status': response.status_code})
416+
raise make_exception(faux_response, response.content,
417+
error_info=download_url, use_json=False)
391418

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

storage/tests/unit/test_blob.py

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,102 @@ def _check_session_mocks(self, client, fake_session_factory,
381381
'GET', expected_url, data=None, headers=headers)
382382
self.assertEqual(fake_transport.request.mock_calls, [call, call])
383383

384+
def test__do_download_simple(self):
385+
from io import BytesIO
386+
from six.moves import http_client
387+
388+
blob_name = 'blob-name'
389+
# Create a fake client/bucket and use them in the Blob() constructor.
390+
client = mock.Mock(
391+
_credentials=_make_credentials(), spec=['_credentials'])
392+
bucket = _Bucket(client)
393+
blob = self._make_one(blob_name, bucket=bucket)
394+
395+
# Make sure this will not be chunked.
396+
self.assertIsNone(blob.chunk_size)
397+
398+
transport = mock.Mock(spec=['request'])
399+
transport.request.return_value = self._mock_requests_response(
400+
http_client.OK,
401+
{'content-length': '6', 'content-range': 'bytes 0-5/6'},
402+
content=b'abcdef')
403+
file_obj = BytesIO()
404+
download_url = 'http://test.invalid'
405+
headers = {}
406+
blob._do_download(transport, file_obj, download_url, headers)
407+
# Make sure the download was as expected.
408+
self.assertEqual(file_obj.getvalue(), b'abcdef')
409+
410+
transport.request.assert_called_once_with(
411+
'GET', download_url, data=None, headers=headers)
412+
413+
def test__do_download_chunked(self):
414+
from io import BytesIO
415+
416+
blob_name = 'blob-name'
417+
# Create a fake client/bucket and use them in the Blob() constructor.
418+
client = mock.Mock(
419+
_credentials=_make_credentials(), spec=['_credentials'])
420+
bucket = _Bucket(client)
421+
blob = self._make_one(blob_name, bucket=bucket)
422+
423+
# Modify the blob so there there will be 2 chunks of size 3.
424+
blob._CHUNK_SIZE_MULTIPLE = 1
425+
blob.chunk_size = 3
426+
427+
transport = self._mock_transport()
428+
file_obj = BytesIO()
429+
download_url = 'http://test.invalid'
430+
headers = {}
431+
blob._do_download(transport, file_obj, download_url, headers)
432+
# Make sure the download was as expected.
433+
self.assertEqual(file_obj.getvalue(), b'abcdef')
434+
435+
# Check that the transport was called exactly twice.
436+
self.assertEqual(transport.request.call_count, 2)
437+
# ``headers`` was modified (in place) once for each API call.
438+
self.assertEqual(headers, {'range': 'bytes=3-5'})
439+
call = mock.call(
440+
'GET', download_url, data=None, headers=headers)
441+
self.assertEqual(transport.request.mock_calls, [call, call])
442+
443+
@mock.patch('google.auth.transport.requests.AuthorizedSession')
444+
def test_download_to_file_with_failure(self, fake_session_factory):
445+
from io import BytesIO
446+
from six.moves import http_client
447+
from google.cloud import exceptions
448+
449+
blob_name = 'blob-name'
450+
transport = mock.Mock(spec=['request'])
451+
bad_response_headers = {
452+
'Content-Length': '9',
453+
'Content-Type': 'text/html; charset=UTF-8',
454+
}
455+
transport.request.return_value = self._mock_requests_response(
456+
http_client.NOT_FOUND, bad_response_headers, content=b'Not found')
457+
fake_session_factory.return_value = transport
458+
# Create a fake client/bucket and use them in the Blob() constructor.
459+
client = mock.Mock(
460+
_credentials=_make_credentials(), spec=['_credentials'])
461+
bucket = _Bucket(client)
462+
blob = self._make_one(blob_name, bucket=bucket)
463+
# Set the media link on the blob
464+
blob._properties['mediaLink'] = 'http://test.invalid'
465+
466+
file_obj = BytesIO()
467+
with self.assertRaises(exceptions.NotFound):
468+
blob.download_to_file(file_obj)
469+
470+
self.assertEqual(file_obj.tell(), 0)
471+
# Check that exactly one transport was created.
472+
fake_session_factory.assert_called_once_with(client._credentials)
473+
# Check that the transport was called once.
474+
transport.request.assert_called_once_with(
475+
'GET', blob.media_link, data=None, headers={})
476+
384477
@mock.patch('google.auth.transport.requests.AuthorizedSession')
385478
def test_download_to_file_wo_media_link(self, fake_session_factory):
386479
from io import BytesIO
387-
from six.moves.http_client import OK
388-
from six.moves.http_client import PARTIAL_CONTENT
389480

390481
blob_name = 'blob-name'
391482
fake_session_factory.return_value = self._mock_transport()
@@ -413,7 +504,6 @@ def test_download_to_file_wo_media_link(self, fake_session_factory):
413504
def _download_to_file_helper(self, fake_session_factory, use_chunks=False):
414505
from io import BytesIO
415506
from six.moves.http_client import OK
416-
from six.moves.http_client import PARTIAL_CONTENT
417507

418508
blob_name = 'blob-name'
419509
fake_transport = self._mock_transport()
@@ -459,8 +549,6 @@ def test_download_to_file_with_chunk_size(self):
459549
def test_download_to_filename(self, fake_session_factory):
460550
import os
461551
import time
462-
from six.moves.http_client import OK
463-
from six.moves.http_client import PARTIAL_CONTENT
464552
from google.cloud._testing import _NamedTemporaryFile
465553

466554
blob_name = 'blob-name'
@@ -493,8 +581,6 @@ def test_download_to_filename(self, fake_session_factory):
493581
def test_download_to_filename_w_key(self, fake_session_factory):
494582
import os
495583
import time
496-
from six.moves.http_client import OK
497-
from six.moves.http_client import PARTIAL_CONTENT
498584
from google.cloud._testing import _NamedTemporaryFile
499585

500586
blob_name = 'blob-name'
@@ -535,9 +621,6 @@ def test_download_to_filename_w_key(self, fake_session_factory):
535621

536622
@mock.patch('google.auth.transport.requests.AuthorizedSession')
537623
def test_download_as_string(self, fake_session_factory):
538-
from six.moves.http_client import OK
539-
from six.moves.http_client import PARTIAL_CONTENT
540-
541624
blob_name = 'blob-name'
542625
fake_session_factory.return_value = self._mock_transport()
543626
# Create a fake client/bucket and use them in the Blob() constructor.

0 commit comments

Comments
 (0)