Skip to content

Commit f433a48

Browse files
tseaverlandrito
authored andcommitted
Pass 'user_project' if set for blob downloads w/ 'mediaLink' set (googleapis#3500)
1 parent 222165a commit f433a48

File tree

2 files changed

+88
-11
lines changed

2 files changed

+88
-11
lines changed

storage/google/cloud/storage/blob.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
import warnings
3636

3737
import httplib2
38+
from six.moves.urllib.parse import parse_qsl
3839
from six.moves.urllib.parse import quote
40+
from six.moves.urllib.parse import urlencode
41+
from six.moves.urllib.parse import urlsplit
42+
from six.moves.urllib.parse import urlunsplit
3943

4044
import google.auth.transport.requests
4145
from google import resumable_media
@@ -403,15 +407,19 @@ def _get_download_url(self):
403407
:rtype: str
404408
:returns: The download URL for the current blob.
405409
"""
410+
name_value_pairs = []
406411
if self.media_link is None:
407-
download_url = _DOWNLOAD_URL_TEMPLATE.format(path=self.path)
412+
base_url = _DOWNLOAD_URL_TEMPLATE.format(path=self.path)
408413
if self.generation is not None:
409-
download_url += u'&generation={:d}'.format(self.generation)
410-
if self.user_project is not None:
411-
download_url += u'&userProject={}'.format(self.user_project)
412-
return download_url
414+
name_value_pairs.append(
415+
('generation', '{:d}'.format(self.generation)))
413416
else:
414-
return self.media_link
417+
base_url = self.media_link
418+
419+
if self.user_project is not None:
420+
name_value_pairs.append(('userProject', self.user_project))
421+
422+
return _add_query_parameters(base_url, name_value_pairs)
415423

416424
def _do_download(self, transport, file_obj, download_url, headers):
417425
"""Perform a download without any error handling.
@@ -658,12 +666,14 @@ def _do_multipart_upload(self, client, stream, content_type,
658666
info = self._get_upload_arguments(content_type)
659667
headers, object_metadata, content_type = info
660668

661-
upload_url = _MULTIPART_URL_TEMPLATE.format(
669+
base_url = _MULTIPART_URL_TEMPLATE.format(
662670
bucket_path=self.bucket.path)
671+
name_value_pairs = []
663672

664673
if self.user_project is not None:
665-
upload_url += '&userProject={}'.format(self.user_project)
674+
name_value_pairs.append(('userProject', self.user_project))
666675

676+
upload_url = _add_query_parameters(base_url, name_value_pairs)
667677
upload = MultipartUpload(upload_url, headers=headers)
668678

669679
if num_retries is not None:
@@ -734,12 +744,14 @@ def _initiate_resumable_upload(self, client, stream, content_type,
734744
if extra_headers is not None:
735745
headers.update(extra_headers)
736746

737-
upload_url = _RESUMABLE_URL_TEMPLATE.format(
747+
base_url = _RESUMABLE_URL_TEMPLATE.format(
738748
bucket_path=self.bucket.path)
749+
name_value_pairs = []
739750

740751
if self.user_project is not None:
741-
upload_url += '&userProject={}'.format(self.user_project)
752+
name_value_pairs.append(('userProject', self.user_project))
742753

754+
upload_url = _add_query_parameters(base_url, name_value_pairs)
743755
upload = ResumableUpload(upload_url, chunk_size, headers=headers)
744756

745757
if num_retries is not None:
@@ -1676,3 +1688,24 @@ def _raise_from_invalid_response(error, error_info=None):
16761688
faux_response = httplib2.Response({'status': response.status_code})
16771689
raise make_exception(faux_response, response.content,
16781690
error_info=error_info, use_json=False)
1691+
1692+
1693+
def _add_query_parameters(base_url, name_value_pairs):
1694+
"""Add one query parameter to a base URL.
1695+
1696+
:type base_url: string
1697+
:param base_url: Base URL (may already contain query parameters)
1698+
1699+
:type name_value_pairs: list of (string, string) tuples.
1700+
:param name_value_pairs: Names and values of the query parameters to add
1701+
1702+
:rtype: string
1703+
:returns: URL with additional query strings appended.
1704+
"""
1705+
if len(name_value_pairs) == 0:
1706+
return base_url
1707+
1708+
scheme, netloc, path, query, frag = urlsplit(base_url)
1709+
query = parse_qsl(query)
1710+
query.extend(name_value_pairs)
1711+
return urlunsplit((scheme, netloc, path, urlencode(query), frag))

storage/tests/unit/test_blob.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def test__make_transport(self, fake_session_factory):
366366

367367
def test__get_download_url_with_media_link(self):
368368
blob_name = 'something.txt'
369-
bucket = mock.Mock(spec=[])
369+
bucket = _Bucket(name='IRRELEVANT')
370370
blob = self._make_one(blob_name, bucket=bucket)
371371
media_link = 'http://test.invalid'
372372
# Set the media link on the blob
@@ -375,6 +375,19 @@ def test__get_download_url_with_media_link(self):
375375
download_url = blob._get_download_url()
376376
self.assertEqual(download_url, media_link)
377377

378+
def test__get_download_url_with_media_link_w_user_project(self):
379+
blob_name = 'something.txt'
380+
user_project = 'user-project-123'
381+
bucket = _Bucket(name='IRRELEVANT', user_project=user_project)
382+
blob = self._make_one(blob_name, bucket=bucket)
383+
media_link = 'http://test.invalid'
384+
# Set the media link on the blob
385+
blob._properties['mediaLink'] = media_link
386+
387+
download_url = blob._get_download_url()
388+
self.assertEqual(
389+
download_url, '{}?userProject={}'.format(media_link, user_project))
390+
378391
def test__get_download_url_on_the_fly(self):
379392
blob_name = 'bzzz-fly.txt'
380393
bucket = _Bucket(name='buhkit')
@@ -2430,6 +2443,37 @@ def test_with_error_info(self):
24302443
self.assertEqual(exc_info.exception.errors, [])
24312444

24322445

2446+
class Test__add_query_parameters(unittest.TestCase):
2447+
2448+
@staticmethod
2449+
def _call_fut(*args, **kwargs):
2450+
from google.cloud.storage.blob import _add_query_parameters
2451+
2452+
return _add_query_parameters(*args, **kwargs)
2453+
2454+
def test_w_empty_list(self):
2455+
BASE_URL = 'https://test.example.com/base'
2456+
self.assertEqual(self._call_fut(BASE_URL, []), BASE_URL)
2457+
2458+
def test_wo_existing_qs(self):
2459+
BASE_URL = 'https://test.example.com/base'
2460+
NV_LIST = [('one', 'One'), ('two', 'Two')]
2461+
expected = '&'.join([
2462+
'{}={}'.format(name, value) for name, value in NV_LIST])
2463+
self.assertEqual(
2464+
self._call_fut(BASE_URL, NV_LIST),
2465+
'{}?{}'.format(BASE_URL, expected))
2466+
2467+
def test_w_existing_qs(self):
2468+
BASE_URL = 'https://test.example.com/base?one=Three'
2469+
NV_LIST = [('one', 'One'), ('two', 'Two')]
2470+
expected = '&'.join([
2471+
'{}={}'.format(name, value) for name, value in NV_LIST])
2472+
self.assertEqual(
2473+
self._call_fut(BASE_URL, NV_LIST),
2474+
'{}&{}'.format(BASE_URL, expected))
2475+
2476+
24332477
class _Connection(object):
24342478

24352479
API_BASE_URL = 'http://example.com'

0 commit comments

Comments
 (0)