Skip to content

Commit 4c63b2a

Browse files
committed
Merge pull request #814 from dhermes/fix-546
Make storage upload/download have no chunk size by default.
2 parents c689c3f + e442cd2 commit 4c63b2a

File tree

2 files changed

+100
-18
lines changed

2 files changed

+100
-18
lines changed

gcloud/storage/blob.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,21 @@ class Blob(_PropertyMixin):
5151
:param bucket: The bucket to which this blob belongs. Required, unless the
5252
implicit default bucket has been set.
5353
54+
:type chunk_size: integer
55+
:param chunk_size: The size of a chunk of data whenever iterating (1 MB).
56+
This must be a multiple of 256 KB per the API
57+
specification.
58+
5459
:type properties: dict
5560
:param properties: All the other data provided by Cloud Storage.
5661
"""
5762

58-
CHUNK_SIZE = 1024 * 1024 # 1 MB.
59-
"""The size of a chunk of data whenever iterating (1 MB).
63+
_chunk_size = None # Default value for each instance.
6064

61-
This must be a multiple of 256 KB per the API specification.
62-
"""
65+
_CHUNK_SIZE_MULTIPLE = 256 * 1024
66+
"""Number (256 KB, in bytes) that must divide the chunk size."""
6367

64-
def __init__(self, name, bucket=None):
68+
def __init__(self, name, bucket=None, chunk_size=None):
6569
if bucket is None:
6670
bucket = _implicit_environ.get_default_bucket()
6771

@@ -70,9 +74,34 @@ def __init__(self, name, bucket=None):
7074

7175
super(Blob, self).__init__(name=name)
7276

77+
self.chunk_size = chunk_size # Check that setter accepts value.
7378
self.bucket = bucket
7479
self._acl = ObjectACL(self)
7580

81+
@property
82+
def chunk_size(self):
83+
"""Get the blob's default chunk size.
84+
85+
:rtype: integer or ``NoneType``
86+
:returns: The current blob's chunk size, if it is set.
87+
"""
88+
return self._chunk_size
89+
90+
@chunk_size.setter
91+
def chunk_size(self, value):
92+
"""Set the blob's default chunk size.
93+
94+
:type value: integer or ``NoneType``
95+
:param value: The current blob's chunk size, if it is set.
96+
97+
:raises: :class:`ValueError` if ``value`` is not ``None`` and is not a
98+
multiple of 256 KB.
99+
"""
100+
if value is not None and value % self._CHUNK_SIZE_MULTIPLE != 0:
101+
raise ValueError('Chunk size must be a multiple of %d.' % (
102+
self._CHUNK_SIZE_MULTIPLE,))
103+
self._chunk_size = value
104+
76105
@staticmethod
77106
def path_helper(bucket_path, blob_name):
78107
"""Relative URL path for a blob.
@@ -226,8 +255,10 @@ def download_to_file(self, file_obj):
226255

227256
# Use apitools 'Download' facility.
228257
download = transfer.Download.FromStream(file_obj, auto_transfer=False)
229-
download.chunksize = self.CHUNK_SIZE
230-
headers = {'Range': 'bytes=0-%d' % (self.CHUNK_SIZE - 1)}
258+
headers = {}
259+
if self.chunk_size is not None:
260+
download.chunksize = self.chunk_size
261+
headers['Range'] = 'bytes=0-%d' % (self.chunk_size - 1,)
231262
request = http_wrapper.Request(download_url, 'GET', headers)
232263

233264
download.InitializeDownload(request, self.connection.http)
@@ -319,7 +350,7 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
319350

320351
upload = transfer.Upload(file_obj, content_type, total_bytes,
321352
auto_transfer=False,
322-
chunksize=self.CHUNK_SIZE)
353+
chunksize=self.chunk_size)
323354

324355
url_builder = _UrlBuilder(bucket_name=self.bucket.name,
325356
object_name=self.name)

gcloud/storage/test_blob.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,41 @@ def test_ctor_explicit(self):
6969
self.assertFalse(blob._acl.loaded)
7070
self.assertTrue(blob._acl.blob is blob)
7171

72+
def test_chunk_size_ctor(self):
73+
from gcloud.storage.blob import Blob
74+
BLOB_NAME = 'blob-name'
75+
BUCKET = object()
76+
chunk_size = 10 * Blob._CHUNK_SIZE_MULTIPLE
77+
blob = self._makeOne(BLOB_NAME, bucket=BUCKET, chunk_size=chunk_size)
78+
self.assertEqual(blob._chunk_size, chunk_size)
79+
80+
def test_chunk_size_getter(self):
81+
BLOB_NAME = 'blob-name'
82+
BUCKET = object()
83+
blob = self._makeOne(BLOB_NAME, bucket=BUCKET)
84+
self.assertEqual(blob.chunk_size, None)
85+
VALUE = object()
86+
blob._chunk_size = VALUE
87+
self.assertTrue(blob.chunk_size is VALUE)
88+
89+
def test_chunk_size_setter(self):
90+
BLOB_NAME = 'blob-name'
91+
BUCKET = object()
92+
blob = self._makeOne(BLOB_NAME, bucket=BUCKET)
93+
self.assertEqual(blob._chunk_size, None)
94+
blob._CHUNK_SIZE_MULTIPLE = 10
95+
blob.chunk_size = 20
96+
self.assertEqual(blob._chunk_size, 20)
97+
98+
def test_chunk_size_setter_bad_value(self):
99+
BLOB_NAME = 'blob-name'
100+
BUCKET = object()
101+
blob = self._makeOne(BLOB_NAME, bucket=BUCKET)
102+
self.assertEqual(blob._chunk_size, None)
103+
blob._CHUNK_SIZE_MULTIPLE = 10
104+
with self.assertRaises(ValueError):
105+
blob.chunk_size = 11
106+
72107
def test_acl_property(self):
73108
from gcloud.storage.acl import ObjectACL
74109
FAKE_BUCKET = _Bucket(None)
@@ -242,7 +277,7 @@ def test_delete(self):
242277
blob.delete()
243278
self.assertFalse(blob.exists())
244279

245-
def test_download_to_file(self):
280+
def _download_to_file_helper(self, chunk_size=None):
246281
from six.moves.http_client import OK
247282
from six.moves.http_client import PARTIAL_CONTENT
248283
from io import BytesIO
@@ -259,11 +294,19 @@ def test_download_to_file(self):
259294
MEDIA_LINK = 'http://example.com/media/'
260295
properties = {'mediaLink': MEDIA_LINK}
261296
blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties)
262-
blob.CHUNK_SIZE = 3
297+
if chunk_size is not None:
298+
blob._CHUNK_SIZE_MULTIPLE = 1
299+
blob.chunk_size = chunk_size
263300
fh = BytesIO()
264301
blob.download_to_file(fh)
265302
self.assertEqual(fh.getvalue(), b'abcdef')
266303

304+
def test_download_to_file_default(self):
305+
self._download_to_file_helper()
306+
307+
def test_download_to_file_with_chunk_size(self):
308+
self._download_to_file_helper(chunk_size=3)
309+
267310
def test_download_to_filename(self):
268311
import os
269312
import time
@@ -284,7 +327,8 @@ def test_download_to_filename(self):
284327
properties = {'mediaLink': MEDIA_LINK,
285328
'updated': '2014-12-06T13:13:50.690Z'}
286329
blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties)
287-
blob.CHUNK_SIZE = 3
330+
blob._CHUNK_SIZE_MULTIPLE = 1
331+
blob.chunk_size = 3
288332
with NamedTemporaryFile() as f:
289333
blob.download_to_filename(f.name)
290334
f.flush()
@@ -311,7 +355,8 @@ def test_download_as_string(self):
311355
MEDIA_LINK = 'http://example.com/media/'
312356
properties = {'mediaLink': MEDIA_LINK}
313357
blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties)
314-
blob.CHUNK_SIZE = 3
358+
blob._CHUNK_SIZE_MULTIPLE = 1
359+
blob.chunk_size = 3
315360
fetched = blob.download_as_string()
316361
self.assertEqual(fetched, b'abcdef')
317362

@@ -330,7 +375,8 @@ def _upload_from_file_simple_test_helper(self, properties=None,
330375
)
331376
bucket = _Bucket(connection)
332377
blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties)
333-
blob.CHUNK_SIZE = 5
378+
blob._CHUNK_SIZE_MULTIPLE = 1
379+
blob.chunk_size = 5
334380
with NamedTemporaryFile() as fh:
335381
fh.write(DATA)
336382
fh.flush()
@@ -398,7 +444,8 @@ def test_upload_from_file_resumable(self):
398444
)
399445
bucket = _Bucket(connection)
400446
blob = self._makeOne(BLOB_NAME, bucket=bucket)
401-
blob.CHUNK_SIZE = 5
447+
blob._CHUNK_SIZE_MULTIPLE = 1
448+
blob.chunk_size = 5
402449
# Set the threshhold low enough that we force a resumable uploada.
403450
with _Monkey(transfer, _RESUMABLE_UPLOAD_THRESHOLD=5):
404451
with NamedTemporaryFile() as fh:
@@ -455,7 +502,8 @@ def test_upload_from_file_w_slash_in_name(self):
455502
)
456503
bucket = _Bucket(connection)
457504
blob = self._makeOne(BLOB_NAME, bucket=bucket)
458-
blob.CHUNK_SIZE = 5
505+
blob._CHUNK_SIZE_MULTIPLE = 1
506+
blob.chunk_size = 5
459507
with NamedTemporaryFile() as fh:
460508
fh.write(DATA)
461509
fh.flush()
@@ -502,7 +550,8 @@ def _upload_from_filename_test_helper(self, properties=None,
502550
bucket = _Bucket(connection)
503551
blob = self._makeOne(BLOB_NAME, bucket=bucket,
504552
properties=properties)
505-
blob.CHUNK_SIZE = 5
553+
blob._CHUNK_SIZE_MULTIPLE = 1
554+
blob.chunk_size = 5
506555
with NamedTemporaryFile(suffix='.jpeg') as fh:
507556
fh.write(DATA)
508557
fh.flush()
@@ -565,7 +614,8 @@ def test_upload_from_string_w_bytes(self):
565614
)
566615
bucket = _Bucket(connection)
567616
blob = self._makeOne(BLOB_NAME, bucket=bucket)
568-
blob.CHUNK_SIZE = 5
617+
blob._CHUNK_SIZE_MULTIPLE = 1
618+
blob.chunk_size = 5
569619
blob.upload_from_string(DATA)
570620
rq = connection.http._requested
571621
self.assertEqual(len(rq), 1)
@@ -603,7 +653,8 @@ def test_upload_from_string_w_text(self):
603653
)
604654
bucket = _Bucket(connection)
605655
blob = self._makeOne(BLOB_NAME, bucket=bucket)
606-
blob.CHUNK_SIZE = 5
656+
blob._CHUNK_SIZE_MULTIPLE = 1
657+
blob.chunk_size = 5
607658
blob.upload_from_string(DATA)
608659
rq = connection.http._requested
609660
self.assertEqual(len(rq), 1)

0 commit comments

Comments
 (0)