Skip to content

Commit 2a306fe

Browse files
committed
Use 'Upload.ConfigureRequest' to decide 'simple' vs. 'resumable'.
1 parent 08e0c9e commit 2a306fe

File tree

2 files changed

+118
-89
lines changed

2 files changed

+118
-89
lines changed

gcloud/storage/key.py

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
from StringIO import StringIO
77
import urllib
88

9-
from _gcloud_vendor.apitools.base.py.http_wrapper import Request
10-
from _gcloud_vendor.apitools.base.py.transfer import Upload
11-
from _gcloud_vendor.apitools.base.py.transfer import Download
12-
from _gcloud_vendor.apitools.base.py.transfer import _RESUMABLE_UPLOAD
9+
from _gcloud_vendor.apitools.base.py import http_wrapper
10+
from _gcloud_vendor.apitools.base.py import transfer
1311

1412
from gcloud.storage._helpers import _PropertyMixin
1513
from gcloud.storage._helpers import _scalar_property
@@ -212,12 +210,12 @@ def download_to_file(self, file_obj):
212210
:raises: :class:`gcloud.storage.exceptions.NotFound`
213211
"""
214212
# Use apitools 'Download' facility.
215-
download = Download.FromStream(file_obj, auto_transfer=False)
213+
download = transfer.Download.FromStream(file_obj, auto_transfer=False)
216214
download.chunksize = self.CHUNK_SIZE
217215
download_url = self.connection.build_api_url(
218216
path=self.path, query_params={'alt': 'media'})
219217
headers = {'Range': 'bytes=0-%d' % (self.CHUNK_SIZE - 1)}
220-
request = Request(download_url, 'POST', headers)
218+
request = http_wrapper.Request(download_url, 'POST', headers)
221219

222220
download.InitializeDownload(request, self.connection.http)
223221

@@ -259,7 +257,7 @@ def download_as_string(self):
259257
get_contents_as_string = download_as_string
260258

261259
def upload_from_file(self, file_obj, rewind=False, size=None,
262-
content_type=None):
260+
content_type=None, num_retries=6):
263261
"""Upload the contents of this key from a file-like object.
264262

265263
.. note::
@@ -292,41 +290,43 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
292290

293291
# Get the basic stats about the file.
294292
total_bytes = size or os.fstat(file_obj.fileno()).st_size
293+
conn = self.connection
295294
headers = {
296-
# Base headers
297295
'Accept': 'application/json',
298296
'Accept-Encoding': 'gzip, deflate',
299-
'User-Agent': self.connection.USER_AGENT,
300-
# resumable upload headers.
301-
'X-Upload-Content-Type': content_type or 'application/unknown',
302-
'X-Upload-Content-Length': total_bytes,
297+
'User-Agent': conn.USER_AGENT,
303298
}
304299

305-
query_params = {
306-
'uploadType': 'resumable',
307-
'name': self.name,
308-
}
300+
upload = transfer.Upload(file_obj,
301+
content_type or 'application/unknown',
302+
total_bytes, auto_transfer=False,
303+
chunksize=self.CHUNK_SIZE)
309304

310-
upload_url = self.connection.build_api_url(
311-
path=self.bucket.path + '/o',
312-
query_params=query_params,
313-
api_base_url=self.connection.API_BASE_URL + '/upload')
305+
url_builder = _UrlBuilder(bucket_name=self.bucket.name,
306+
object_name=self.name)
307+
upload_config = _UploadConfig()
314308

315-
# Use apitools 'Upload' facility.
316-
request = Request(upload_url, 'POST', headers)
309+
# Temporary URL, until we know simple vs. resumable.
310+
upload_url = conn.build_api_url(
311+
path=self.bucket.path + '/o')
317312

318-
upload = Upload(file_obj, content_type or 'application/unknown',
319-
total_bytes, auto_transfer=False,
320-
chunksize=self.CHUNK_SIZE)
321-
upload.strategy = _RESUMABLE_UPLOAD
313+
# Use apitools 'Upload' facility.
314+
request = http_wrapper.Request(upload_url, 'POST', headers)
322315

323-
upload.InitializeUpload(request, self.connection.http)
316+
upload.ConfigureRequest(upload_config, request, url_builder)
317+
path = url_builder.relative_path.format(bucket=self.bucket.name)
318+
query_params = url_builder.query_params
319+
request.url = conn.build_api_url(path=path, query_params=query_params)
320+
upload.InitializeUpload(request, conn.http)
324321

325322
# Should we be passing callbacks through from caller? We can't
326323
# pass them as None, because apitools wants to print to the console
327324
# by default.
328-
upload.StreamInChunks(callback=lambda *args: None,
329-
finish_callback=lambda *args: None)
325+
if upload.strategy == transfer._RESUMABLE_UPLOAD:
326+
upload.StreamInChunks(callback=lambda *args: None,
327+
finish_callback=lambda *args: None)
328+
else:
329+
http_wrapper.MakeRequest(conn.http, request, retries=num_retries)
330330

331331
# NOTE: Alias for boto-like API.
332332
set_contents_from_file = upload_from_file
@@ -606,3 +606,34 @@ def updated(self):
606606
:returns: timestamp in RFC 3339 format.
607607
"""
608608
return self.properties['updated']
609+
610+
611+
class _UploadConfig(object):
612+
""" Faux message FBO apitools' 'ConfigureRequest'.
613+
614+
Values extracted from apitools
615+
'samples/storage_sample/storage/storage_v1_client.py'
616+
"""
617+
accept = ['*/*']
618+
max_size = None
619+
resumable_multipart = True
620+
resumable_path = u'/resumable/upload/storage/v1/b/{bucket}/o'
621+
simple_multipart = True
622+
simple_path = u'/upload/storage/v1/b/{bucket}/o'
623+
624+
625+
class _UrlBuilder(object):
626+
"""Faux builder FBO apitools' 'ConfigureRequest'
627+
"""
628+
def __init__(self, bucket_name, object_name):
629+
self.query_params = {'name': object_name}
630+
self._bucket_name = bucket_name
631+
self._relative_path = ''
632+
633+
@property
634+
def relative_path(self):
635+
return self._relative_path.format(bucket=self._bucket_name)
636+
637+
@relative_path.setter
638+
def relative_path(self, value):
639+
self._relative_path = value

gcloud/storage/test_key.py

Lines changed: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,47 @@ def test_download_as_string(self):
236236
fetched = key.download_as_string()
237237
self.assertEqual(fetched, 'abcdef')
238238

239-
def test_upload_from_file(self):
239+
def test_upload_from_file_simple(self):
240240
import httplib
241241
from tempfile import NamedTemporaryFile
242242
from urlparse import parse_qsl
243243
from urlparse import urlsplit
244+
KEY = 'key'
245+
DATA = 'ABCDEF'
246+
response = {'status': httplib.OK}
247+
connection = _Connection(
248+
(response, ''),
249+
)
250+
bucket = _Bucket(connection)
251+
key = self._makeOne(bucket, KEY)
252+
key.CHUNK_SIZE = 5
253+
with NamedTemporaryFile() as fh:
254+
fh.write(DATA)
255+
fh.flush()
256+
key.upload_from_file(fh, rewind=True)
257+
rq = connection.http._requested
258+
self.assertEqual(len(rq), 1)
259+
self.assertEqual(rq[0]['method'], 'POST')
260+
uri = rq[0]['uri']
261+
scheme, netloc, path, qs, _ = urlsplit(uri)
262+
self.assertEqual(scheme, 'http')
263+
self.assertEqual(netloc, 'example.com')
264+
self.assertEqual(path, '/upload/storage/v1/b/name/o')
265+
self.assertEqual(dict(parse_qsl(qs)),
266+
{'uploadType': 'media', 'name': 'key'})
267+
headers = dict(
268+
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
269+
self.assertEqual(headers['Content-Length'], '6')
270+
self.assertEqual(headers['Content-Type'], 'application/unknown')
271+
272+
def test_upload_from_file_resumable(self):
273+
import httplib
274+
from tempfile import NamedTemporaryFile
275+
from urlparse import parse_qsl
276+
from urlparse import urlsplit
277+
from gcloud._testing import _Monkey
244278
from _gcloud_vendor.apitools.base.py import http_wrapper
279+
from _gcloud_vendor.apitools.base.py import transfer
245280
KEY = 'key'
246281
UPLOAD_URL = 'http://example.com/upload/name/key'
247282
DATA = 'ABCDEF'
@@ -257,18 +292,20 @@ def test_upload_from_file(self):
257292
bucket = _Bucket(connection)
258293
key = self._makeOne(bucket, KEY)
259294
key.CHUNK_SIZE = 5
260-
with NamedTemporaryFile() as fh:
261-
fh.write(DATA)
262-
fh.flush()
263-
key.upload_from_file(fh, rewind=True)
295+
# Set the threshhold low enough that we force a resumable uploada.
296+
with _Monkey(transfer, _RESUMABLE_UPLOAD_THRESHOLD=5):
297+
with NamedTemporaryFile() as fh:
298+
fh.write(DATA)
299+
fh.flush()
300+
key.upload_from_file(fh, rewind=True)
264301
rq = connection.http._requested
265302
self.assertEqual(len(rq), 3)
266303
self.assertEqual(rq[0]['method'], 'POST')
267304
uri = rq[0]['uri']
268305
scheme, netloc, path, qs, _ = urlsplit(uri)
269306
self.assertEqual(scheme, 'http')
270307
self.assertEqual(netloc, 'example.com')
271-
self.assertEqual(path, '/b/name/o')
308+
self.assertEqual(path, '/resumable/upload/storage/v1/b/name/o')
272309
self.assertEqual(dict(parse_qsl(qs)),
273310
{'uploadType': 'resumable', 'name': 'key'})
274311
headers = dict(
@@ -317,32 +354,19 @@ def test_upload_from_file_w_slash_in_name(self):
317354
fh.flush()
318355
key.upload_from_file(fh, rewind=True)
319356
rq = connection.http._requested
320-
self.assertEqual(len(rq), 3)
357+
self.assertEqual(len(rq), 1)
321358
self.assertEqual(rq[0]['method'], 'POST')
322359
uri = rq[0]['uri']
323360
scheme, netloc, path, qs, _ = urlsplit(uri)
324361
self.assertEqual(scheme, 'http')
325362
self.assertEqual(netloc, 'example.com')
326-
self.assertEqual(path, '/b/name/o')
363+
self.assertEqual(path, '/upload/storage/v1/b/name/o')
327364
self.assertEqual(dict(parse_qsl(qs)),
328-
{'uploadType': 'resumable', 'name': 'parent/child'})
365+
{'uploadType': 'media', 'name': 'parent/child'})
329366
headers = dict(
330367
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
331-
self.assertEqual(headers['X-Upload-Content-Length'], '6')
332-
self.assertEqual(headers['X-Upload-Content-Type'],
333-
'application/unknown')
334-
self.assertEqual(rq[1]['method'], 'PUT')
335-
self.assertEqual(rq[1]['uri'], UPLOAD_URL)
336-
self.assertEqual(rq[1]['body'], DATA[:5])
337-
headers = dict(
338-
[(x.title(), str(y)) for x, y in rq[1]['headers'].items()])
339-
self.assertEqual(headers['Content-Range'], 'bytes 0-4/6')
340-
self.assertEqual(rq[2]['method'], 'PUT')
341-
self.assertEqual(rq[2]['uri'], UPLOAD_URL)
342-
self.assertEqual(rq[2]['body'], DATA[5:])
343-
headers = dict(
344-
[(x.title(), str(y)) for x, y in rq[2]['headers'].items()])
345-
self.assertEqual(headers['Content-Range'], 'bytes 5-5/6')
368+
self.assertEqual(headers['Content-Length'], '6')
369+
self.assertEqual(headers['Content-Type'], 'application/unknown')
346370

347371
def test_upload_from_filename(self):
348372
import httplib
@@ -370,32 +394,19 @@ def test_upload_from_filename(self):
370394
fh.flush()
371395
key.upload_from_filename(fh.name)
372396
rq = connection.http._requested
373-
self.assertEqual(len(rq), 3)
397+
self.assertEqual(len(rq), 1)
374398
self.assertEqual(rq[0]['method'], 'POST')
375399
uri = rq[0]['uri']
376400
scheme, netloc, path, qs, _ = urlsplit(uri)
377401
self.assertEqual(scheme, 'http')
378402
self.assertEqual(netloc, 'example.com')
379-
self.assertEqual(path, '/b/name/o')
403+
self.assertEqual(path, '/upload/storage/v1/b/name/o')
380404
self.assertEqual(dict(parse_qsl(qs)),
381-
{'uploadType': 'resumable', 'name': 'key'})
405+
{'uploadType': 'media', 'name': 'key'})
382406
headers = dict(
383407
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
384-
self.assertEqual(headers['X-Upload-Content-Length'], '6')
385-
self.assertEqual(headers['X-Upload-Content-Type'],
386-
'image/jpeg')
387-
self.assertEqual(rq[1]['method'], 'PUT')
388-
self.assertEqual(rq[1]['uri'], UPLOAD_URL)
389-
self.assertEqual(rq[1]['body'], DATA[:5])
390-
headers = dict(
391-
[(x.title(), str(y)) for x, y in rq[1]['headers'].items()])
392-
self.assertEqual(headers['Content-Range'], 'bytes 0-4/6')
393-
self.assertEqual(rq[2]['method'], 'PUT')
394-
self.assertEqual(rq[2]['uri'], UPLOAD_URL)
395-
self.assertEqual(rq[2]['body'], DATA[5:])
396-
headers = dict(
397-
[(x.title(), str(y)) for x, y in rq[2]['headers'].items()])
398-
self.assertEqual(headers['Content-Range'], 'bytes 5-5/6')
408+
self.assertEqual(headers['Content-Length'], '6')
409+
self.assertEqual(headers['Content-Type'], 'image/jpeg')
399410

400411
def test_upload_from_string(self):
401412
import httplib
@@ -419,32 +430,19 @@ def test_upload_from_string(self):
419430
key.CHUNK_SIZE = 5
420431
key.upload_from_string(DATA)
421432
rq = connection.http._requested
422-
self.assertEqual(len(rq), 3)
433+
self.assertEqual(len(rq), 1)
423434
self.assertEqual(rq[0]['method'], 'POST')
424435
uri = rq[0]['uri']
425436
scheme, netloc, path, qs, _ = urlsplit(uri)
426437
self.assertEqual(scheme, 'http')
427438
self.assertEqual(netloc, 'example.com')
428-
self.assertEqual(path, '/b/name/o')
439+
self.assertEqual(path, '/upload/storage/v1/b/name/o')
429440
self.assertEqual(dict(parse_qsl(qs)),
430-
{'uploadType': 'resumable', 'name': 'key'})
441+
{'uploadType': 'media', 'name': 'key'})
431442
headers = dict(
432443
[(x.title(), str(y)) for x, y in rq[0]['headers'].items()])
433-
self.assertEqual(headers['X-Upload-Content-Length'], '6')
434-
self.assertEqual(headers['X-Upload-Content-Type'],
435-
'text/plain')
436-
self.assertEqual(rq[1]['method'], 'PUT')
437-
self.assertEqual(rq[1]['uri'], UPLOAD_URL)
438-
self.assertEqual(rq[1]['body'], DATA[:5])
439-
headers = dict(
440-
[(x.title(), str(y)) for x, y in rq[1]['headers'].items()])
441-
self.assertEqual(headers['Content-Range'], 'bytes 0-4/6')
442-
self.assertEqual(rq[2]['method'], 'PUT')
443-
self.assertEqual(rq[2]['uri'], UPLOAD_URL)
444-
self.assertEqual(rq[2]['body'], DATA[5:])
445-
headers = dict(
446-
[(x.title(), str(y)) for x, y in rq[2]['headers'].items()])
447-
self.assertEqual(headers['Content-Range'], 'bytes 5-5/6')
444+
self.assertEqual(headers['Content-Length'], '6')
445+
self.assertEqual(headers['Content-Type'], 'text/plain')
448446

449447
def test_make_public(self):
450448
from gcloud.storage.acl import _ACLEntity

0 commit comments

Comments
 (0)