Skip to content

Commit c7d7d48

Browse files
committed
Make storage upload/download have no chunk size by default.
Fixes #546.
1 parent f08e71b commit c7d7d48

File tree

2 files changed

+100
-19
lines changed

2 files changed

+100
-19
lines changed

gcloud/storage/blob.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,19 @@ 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).
60-
61-
This must be a multiple of 256 KB per the API specification.
62-
"""
63+
_CHUNK_SIZE_MULTIPLE = 256
64+
"""Number that must divide the chunk size (in KB)."""
6365

64-
def __init__(self, name, bucket=None):
66+
def __init__(self, name, bucket=None, chunk_size=None):
6567
if bucket is None:
6668
bucket = _implicit_environ.get_default_bucket()
6769

@@ -70,9 +72,35 @@ def __init__(self, name, bucket=None):
7072

7173
super(Blob, self).__init__(name=name)
7274

75+
self._chunk_size = None # Needs to be defined in __init__.
76+
self.chunk_size = chunk_size # Check that setter accepts value.
7377
self.bucket = bucket
7478
self._acl = ObjectACL(self)
7579

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

227255
# Use apitools 'Download' facility.
228256
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)}
257+
headers = {}
258+
if self.chunk_size is not None:
259+
download.chunksize = self.chunk_size
260+
headers['Range'] = 'bytes=0-%d' % (self.chunk_size - 1,)
231261
request = http_wrapper.Request(download_url, 'GET', headers)
232262

233263
download.InitializeDownload(request, self.connection.http)
@@ -319,7 +349,7 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
319349

320350
upload = transfer.Upload(file_obj, content_type, total_bytes,
321351
auto_transfer=False,
322-
chunksize=self.CHUNK_SIZE)
352+
chunksize=self.chunk_size)
323353

324354
url_builder = _UrlBuilder(bucket_name=self.bucket.name,
325355
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)