Skip to content

Commit 5d6c3c6

Browse files
committed
Merge pull request #1717 from tseaver/1712-storage-upload_drops_40x_errors_on_floor
Raise appropriate exception when upload fails.
2 parents 9ad9a00 + d8729b7 commit 5d6c3c6

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

gcloud/storage/blob.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import os
2222
import time
2323

24+
import httplib2
2425
import six
2526
from six.moves.urllib.parse import quote
2627

2728
from gcloud._helpers import _rfc3339_to_datetime
2829
from gcloud.credentials import generate_signed_url
2930
from gcloud.exceptions import NotFound
31+
from gcloud.exceptions import make_exception
3032
from gcloud.storage._helpers import _PropertyMixin
3133
from gcloud.storage._helpers import _scalar_property
3234
from gcloud.storage.acl import ObjectACL
@@ -346,6 +348,16 @@ def download_as_string(self, client=None):
346348
self.download_to_file(string_buffer, client=client)
347349
return string_buffer.getvalue()
348350

351+
@staticmethod
352+
def _check_response_error(request, http_response):
353+
"""Helper for :meth:`upload_from_file`."""
354+
info = http_response.info
355+
status = int(info['status'])
356+
if not 200 <= status < 300:
357+
faux_response = httplib2.Response({'status': status})
358+
raise make_exception(faux_response, http_response.content,
359+
error_info=request.url)
360+
349361
def upload_from_file(self, file_obj, rewind=False, size=None,
350362
content_type=None, num_retries=6, client=None):
351363
"""Upload the contents of this blob from a file-like object.
@@ -390,7 +402,8 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
390402
to the ``client`` stored on the blob's bucket.
391403
392404
:raises: :class:`ValueError` if size is not passed in and can not be
393-
determined
405+
determined; :class:`gcloud.exceptions.GCloudError` if the
406+
upload response returns an error status.
394407
"""
395408
client = self._require_client(client)
396409
# Use the private ``_connection`` rather than the public
@@ -452,7 +465,10 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
452465
else:
453466
http_response = make_api_request(connection.http, request,
454467
retries=num_retries)
468+
469+
self._check_response_error(request, http_response)
455470
response_content = http_response.content
471+
456472
if not isinstance(response_content,
457473
six.string_types): # pragma: NO COVER Python3
458474
response_content = response_content.decode('utf-8')

gcloud/storage/test_blob.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,18 @@ def test_upload_from_file_size_failure(self):
418418
def _upload_from_file_simple_test_helper(self, properties=None,
419419
content_type_arg=None,
420420
expected_content_type=None,
421-
chunk_size=5):
421+
chunk_size=5,
422+
status=None):
422423
from six.moves.http_client import OK
423424
from six.moves.urllib.parse import parse_qsl
424425
from six.moves.urllib.parse import urlsplit
425426
from gcloud._testing import _NamedTemporaryFile
426427

427428
BLOB_NAME = 'blob-name'
428429
DATA = b'ABCDEF'
429-
response = {'status': OK}
430+
if status is None:
431+
status = OK
432+
response = {'status': status}
430433
connection = _Connection(
431434
(response, b'{}'),
432435
)
@@ -463,6 +466,12 @@ def test_upload_from_file_simple(self):
463466
self._upload_from_file_simple_test_helper(
464467
expected_content_type='application/octet-stream')
465468

469+
def test_upload_from_file_simple_not_found(self):
470+
from six.moves.http_client import NOT_FOUND
471+
from gcloud.exceptions import NotFound
472+
with self.assertRaises(NotFound):
473+
self._upload_from_file_simple_test_helper(status=NOT_FOUND)
474+
466475
def test_upload_from_file_simple_w_chunk_size_None(self):
467476
self._upload_from_file_simple_test_helper(
468477
expected_content_type='application/octet-stream',
@@ -572,6 +581,60 @@ def test_upload_from_file_resumable(self):
572581
'redirections': 5,
573582
})
574583

584+
def test_upload_from_file_resumable_w_error(self):
585+
from six.moves.http_client import NOT_FOUND
586+
from six.moves.urllib.parse import parse_qsl
587+
from six.moves.urllib.parse import urlsplit
588+
from gcloud._testing import _Monkey
589+
from gcloud._testing import _NamedTemporaryFile
590+
from gcloud.streaming import transfer
591+
from gcloud.streaming.exceptions import HttpError
592+
593+
BLOB_NAME = 'blob-name'
594+
DATA = b'ABCDEF'
595+
loc_response = {'status': NOT_FOUND}
596+
connection = _Connection(
597+
(loc_response, b'{"error": "no such bucket"}'),
598+
)
599+
client = _Client(connection)
600+
bucket = _Bucket(client)
601+
blob = self._makeOne(BLOB_NAME, bucket=bucket)
602+
blob._CHUNK_SIZE_MULTIPLE = 1
603+
blob.chunk_size = 5
604+
605+
# Set the threshhold low enough that we force a resumable uploada.
606+
with _Monkey(transfer, RESUMABLE_UPLOAD_THRESHOLD=5):
607+
with _NamedTemporaryFile() as temp:
608+
with open(temp.name, 'wb') as file_obj:
609+
file_obj.write(DATA)
610+
with open(temp.name, 'rb') as file_obj:
611+
with self.assertRaises(HttpError):
612+
blob.upload_from_file(file_obj, rewind=True)
613+
614+
rq = connection.http._requested
615+
self.assertEqual(len(rq), 1)
616+
617+
# Requested[0]
618+
headers = dict(
619+
[(x.title(), str(y)) for x, y in rq[0].pop('headers').items()])
620+
self.assertEqual(headers['X-Upload-Content-Length'], '6')
621+
self.assertEqual(headers['X-Upload-Content-Type'],
622+
'application/octet-stream')
623+
624+
uri = rq[0].pop('uri')
625+
scheme, netloc, path, qs, _ = urlsplit(uri)
626+
self.assertEqual(scheme, 'http')
627+
self.assertEqual(netloc, 'example.com')
628+
self.assertEqual(path, '/b/name/o')
629+
self.assertEqual(dict(parse_qsl(qs)),
630+
{'uploadType': 'resumable', 'name': BLOB_NAME})
631+
self.assertEqual(rq[0], {
632+
'method': 'POST',
633+
'body': '',
634+
'connection_type': None,
635+
'redirections': 5,
636+
})
637+
575638
def test_upload_from_file_w_slash_in_name(self):
576639
from six.moves.http_client import OK
577640
from six.moves.urllib.parse import parse_qsl

0 commit comments

Comments
 (0)