Skip to content

Commit 83c0fc1

Browse files
committed
Uses Blob's current content type (in an upload) when present.
This is instead of using the mimetype for all files (the Python mimetypes.guess_type method is a bit limited). Fixes #536.
1 parent f73ad7e commit 83c0fc1

2 files changed

Lines changed: 92 additions & 15 deletions

File tree

gcloud/storage/blob.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
274274
content_type=None, num_retries=6):
275275
"""Upload the contents of this blob from a file-like object.
276276
277+
The content type of the upload will either be
278+
- The value passed in to the function (if any)
279+
- The value stored on the current blob
280+
- The default value of 'application/octet-stream'
281+
277282
.. note::
278283
The effect of uploading to an existing blob depends on the
279284
"versioning" and "lifecycle" policies defined on the blob's
@@ -296,7 +301,16 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
296301
:param size: The number of bytes to read from the file handle.
297302
If not provided, we'll try to guess the size using
298303
:func:`os.fstat`
304+
305+
:type content_type: string or ``NoneType``
306+
:param content_type: Optional type of content being uploaded.
307+
308+
:type num_retries: integer
309+
:param num_retries: Number of upload retries. Defaults to 6.
299310
"""
311+
content_type = (content_type or self._properties.get('contentType') or
312+
'application/octet-stream')
313+
300314
# Rewind the file if desired.
301315
if rewind:
302316
file_obj.seek(0, os.SEEK_SET)
@@ -310,9 +324,8 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
310324
'User-Agent': conn.USER_AGENT,
311325
}
312326

313-
upload = transfer.Upload(file_obj,
314-
content_type or 'application/unknown',
315-
total_bytes, auto_transfer=False,
327+
upload = transfer.Upload(file_obj, content_type, total_bytes,
328+
auto_transfer=False,
316329
chunksize=self.CHUNK_SIZE)
317330

318331
url_builder = _UrlBuilder(bucket_name=self.bucket.name,
@@ -342,9 +355,14 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
342355
else:
343356
http_wrapper.MakeRequest(conn.http, request, retries=num_retries)
344357

345-
def upload_from_filename(self, filename):
358+
def upload_from_filename(self, filename, content_type=None):
346359
"""Upload this blob's contents from the content of a named file.
347360
361+
The content type of the upload will either be
362+
- The value passed in to the function (if any)
363+
- The value stored on the current blob
364+
- The value given by mimetypes.guess_type
365+
348366
.. note::
349367
The effect of uploading to an existing blob depends on the
350368
"versioning" and "lifecycle" policies defined on the blob's
@@ -358,8 +376,13 @@ def upload_from_filename(self, filename):
358376
359377
:type filename: string
360378
:param filename: The path to the file.
379+
380+
:type content_type: string or ``NoneType``
381+
:param content_type: Optional type of content being uploaded.
361382
"""
362-
content_type, _ = mimetypes.guess_type(filename)
383+
content_type = content_type or self._properties.get('contentType')
384+
if content_type is None:
385+
content_type, _ = mimetypes.guess_type(filename)
363386

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

gcloud/storage/test_blob.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,9 @@ def test_download_as_string(self):
327327
fetched = blob.download_as_string()
328328
self.assertEqual(fetched, b'abcdef')
329329

330-
def test_upload_from_file_simple(self):
330+
def _upload_from_file_simple_test_helper(self, properties=None,
331+
content_type_arg=None,
332+
expected_content_type=None):
331333
from six.moves.http_client import OK
332334
from six.moves.urllib.parse import parse_qsl
333335
from six.moves.urllib.parse import urlsplit
@@ -339,12 +341,13 @@ def test_upload_from_file_simple(self):
339341
(response, b''),
340342
)
341343
bucket = _Bucket(connection)
342-
blob = self._makeOne(BLOB_NAME, bucket=bucket)
344+
blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties)
343345
blob.CHUNK_SIZE = 5
344346
with NamedTemporaryFile() as fh:
345347
fh.write(DATA)
346348
fh.flush()
347-
blob.upload_from_file(fh, rewind=True)
349+
blob.upload_from_file(fh, rewind=True,
350+
content_type=content_type_arg)
348351
rq = connection.http._requested
349352
self.assertEqual(len(rq), 1)
350353
self.assertEqual(rq[0]['method'], 'POST')
@@ -358,7 +361,31 @@ def test_upload_from_file_simple(self):
358361
headers = dict(
359362
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
360363
self.assertEqual(headers['Content-Length'], '6')
361-
self.assertEqual(headers['Content-Type'], 'application/unknown')
364+
self.assertEqual(headers['Content-Type'], expected_content_type)
365+
366+
def test_upload_from_file_simple(self):
367+
self._upload_from_file_simple_test_helper(
368+
expected_content_type='application/octet-stream')
369+
370+
def test_upload_from_file_simple_with_content_type(self):
371+
EXPECTED_CONTENT_TYPE = 'foo/bar'
372+
self._upload_from_file_simple_test_helper(
373+
properties={'contentType': EXPECTED_CONTENT_TYPE},
374+
expected_content_type=EXPECTED_CONTENT_TYPE)
375+
376+
def test_upload_from_file_simple_with_content_type_passed(self):
377+
EXPECTED_CONTENT_TYPE = 'foo/bar'
378+
self._upload_from_file_simple_test_helper(
379+
content_type_arg=EXPECTED_CONTENT_TYPE,
380+
expected_content_type=EXPECTED_CONTENT_TYPE)
381+
382+
def test_upload_from_file_simple_both_content_type_sources(self):
383+
EXPECTED_CONTENT_TYPE = 'foo/bar'
384+
ALT_CONTENT_TYPE = 'foo/baz'
385+
self._upload_from_file_simple_test_helper(
386+
properties={'contentType': ALT_CONTENT_TYPE},
387+
content_type_arg=EXPECTED_CONTENT_TYPE,
388+
expected_content_type=EXPECTED_CONTENT_TYPE)
362389

363390
def test_upload_from_file_resumable(self):
364391
from six.moves.http_client import OK
@@ -403,7 +430,7 @@ def test_upload_from_file_resumable(self):
403430
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
404431
self.assertEqual(headers['X-Upload-Content-Length'], '6')
405432
self.assertEqual(headers['X-Upload-Content-Type'],
406-
'application/unknown')
433+
'application/octet-stream')
407434
self.assertEqual(rq[1]['method'], 'PUT')
408435
self.assertEqual(rq[1]['uri'], UPLOAD_URL)
409436
headers = dict(
@@ -457,9 +484,11 @@ def test_upload_from_file_w_slash_in_name(self):
457484
headers = dict(
458485
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
459486
self.assertEqual(headers['Content-Length'], '6')
460-
self.assertEqual(headers['Content-Type'], 'application/unknown')
487+
self.assertEqual(headers['Content-Type'], 'application/octet-stream')
461488

462-
def test_upload_from_filename(self):
489+
def _upload_from_filename_test_helper(self, properties=None,
490+
content_type_arg=None,
491+
expected_content_type=None):
463492
from six.moves.http_client import OK
464493
from six.moves.urllib.parse import parse_qsl
465494
from six.moves.urllib.parse import urlsplit
@@ -478,12 +507,13 @@ def test_upload_from_filename(self):
478507
(chunk2_response, ''),
479508
)
480509
bucket = _Bucket(connection)
481-
blob = self._makeOne(BLOB_NAME, bucket=bucket)
510+
blob = self._makeOne(BLOB_NAME, bucket=bucket,
511+
properties=properties)
482512
blob.CHUNK_SIZE = 5
483513
with NamedTemporaryFile(suffix='.jpeg') as fh:
484514
fh.write(DATA)
485515
fh.flush()
486-
blob.upload_from_filename(fh.name)
516+
blob.upload_from_filename(fh.name, content_type=content_type_arg)
487517
rq = connection.http._requested
488518
self.assertEqual(len(rq), 1)
489519
self.assertEqual(rq[0]['method'], 'POST')
@@ -497,7 +527,31 @@ def test_upload_from_filename(self):
497527
headers = dict(
498528
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
499529
self.assertEqual(headers['Content-Length'], '6')
500-
self.assertEqual(headers['Content-Type'], 'image/jpeg')
530+
self.assertEqual(headers['Content-Type'], expected_content_type)
531+
532+
def test_upload_from_filename(self):
533+
self._upload_from_filename_test_helper(
534+
expected_content_type='image/jpeg')
535+
536+
def test_upload_from_filename_with_content_type(self):
537+
EXPECTED_CONTENT_TYPE = 'foo/bar'
538+
self._upload_from_filename_test_helper(
539+
properties={'contentType': EXPECTED_CONTENT_TYPE},
540+
expected_content_type=EXPECTED_CONTENT_TYPE)
541+
542+
def test_upload_from_filename_with_content_type_passed(self):
543+
EXPECTED_CONTENT_TYPE = 'foo/bar'
544+
self._upload_from_filename_test_helper(
545+
content_type_arg=EXPECTED_CONTENT_TYPE,
546+
expected_content_type=EXPECTED_CONTENT_TYPE)
547+
548+
def test_upload_from_filename_both_content_type_sources(self):
549+
EXPECTED_CONTENT_TYPE = 'foo/bar'
550+
ALT_CONTENT_TYPE = 'foo/baz'
551+
self._upload_from_filename_test_helper(
552+
properties={'contentType': ALT_CONTENT_TYPE},
553+
content_type_arg=EXPECTED_CONTENT_TYPE,
554+
expected_content_type=EXPECTED_CONTENT_TYPE)
501555

502556
def test_upload_from_string_w_bytes(self):
503557
from six.moves.http_client import OK

0 commit comments

Comments
 (0)