8000 Merge pull request #364 from tseaver/354-support_slash_in_key_names · googleapis/google-cloud-python@bc8b1c4 · GitHub
[go: up one dir, main page]

Skip to content

Commit bc8b1c4

Browse files
committed
Merge pull request #364 from tseaver/354-support_slash_in_key_names
Fix #354: support slash in key names
2 parents e3f6ae4 + a7898da commit bc8b1c4
8000

File tree

2 files changed

+87
-8
lines changed

2 files changed

+87
-8
lines changed

gcloud/storage/key.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import mimetypes
55
import os
66
from StringIO import StringIO
7+
import urllib
78

89
from gcloud.storage._helpers import _PropertyMixin
910
from gcloud.storage._helpers import _scalar_property
@@ -116,7 +117,7 @@ def path(self):
116117
elif not self.name:
117118
raise ValueError('Cannot determine path without a key name.')
118119

119-
return self.bucket.path + '/o/' + self.name
120+
return self.bucket.path + '/o/' + urllib.quote(self.name, safe='')
120121

121122
@property
122123
def public_url(self):
@@ -125,11 +126,10 @@ def public_url(self):
125126
:rtype: `string`
126127
:returns: The public URL for this key.
127128
"""
128-
return '{storage_base_url}/{bucket_name}/{key_name}'.format(
129+
return '{storage_base_url}/{bucket_name}/{quoted_name}'.format(
129130
storage_base_url='http://commondatastorage.googleapis.com',
130-
key_name=self.name,
131131
bucket_name=self.bucket.name,
132-
)
132+
quoted_name=urllib.quote(self.name, safe=''))
133133

134134
def generate_signed_url(self, expiration, method='GET'):
135135
"""Generates a signed URL for this key.
@@ -152,10 +152,9 @@ def generate_signed_url(self, expiration, method='GET'):
152152
:returns: A signed URL you can use to access the resource
153153
until expiration.
154154
"""
155-
resource = '/{bucket_name}/{key_name}'.format(
156-
key_name=self.name,
155+
resource = '/{bucket_name}/{quoted_name}'.format(
157156
bucket_name=self.bucket.name,
158-
)
157+
quoted_name=urllib.quote(self.name, safe=''))
159158
return self.connection.generate_signed_url(resource=resource,
160159
expiration=expiration,
161160
method=method)
@@ -284,9 +283,14 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
284283
'X-Upload-Content-Length': total_bytes,
285284
}
286285

286+
query_params = {
287+
'uploadType': 'resumable',
288+
'name': urllib.quote_plus(self.name),
289+
}
290+
287291
upload_url = self.connection.build_api_url(
288292
path=self.bucket.path + '/o',
289-
query_params={'uploadType': 'resumable', 'name': self.name},
293+
query_params=query_params,
290294
api_base_url=self.connection.API_BASE_URL + '/upload')
291295

292296
response, _ = self.connection.make_request(

gcloud/storage/test_key.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ def test_path_normal(self):
7878
key = self._makeOne(bucket, KEY)
7979
self.assertEqual(key.path, '/b/name/o/%s' % KEY)
8080

81+
def test_path_w_slash_in_name(self):
82+
KEY = 'parent/child'
83+
connection = _Connection()
84+
bucket = _Bucket(connection)
85+
key = self._makeOne(bucket, KEY)
86+
self.assertEqual(key.path, '/b/name/o/parent%2Fchild')
87+
8188
def test_public_url(self):
8289
KEY = 'key'
8390
connection = _Connection()
@@ -87,6 +94,15 @@ def test_public_url(self):
8794
'http://commondatastorage.googleapis.com/name/%s' %
8895
KEY)
8996

97+
def test_public_url_w_slash_in_name(self):
98+
KEY = 'parent/child'
99+
connection = _Connection()
100+
bucket = _Bucket(connection)
101+
key = self._makeOne(bucket, KEY)
102+
self.assertEqual(
103+
key.public_url,
104+
'http://commondatastorage.googleapis.com/name/parent%2Fchild')
105+
90106
def test_generate_signed_url_w_default_met 67E6 hod(self):
91107
KEY = 'key'
92108
EXPIRATION = '2014-10-16T20:34:37Z'
@@ -99,6 +115,19 @@ def test_generate_signed_url_w_default_method(self):
99115
self.assertEqual(connection._signed,
100116
[('/name/key', EXPIRATION, {'method': 'GET'})])
101117

118+
def test_generate_signed_url_w_slash_in_name(self):
119+
KEY = 'parent/child'
120+
EXPIRATION = '2014-10-16T20:34:37Z'
121+
connection = _Connection()
122+
bucket = _Bucket(connection)
123+
key = self._makeOne(bucket, KEY)
124+
self.assertEqual(key.generate_signed_url(EXPIRATION),
125+
'http://example.com/abucket/akey?Signature=DEADBEEF'
126+
'&Expiration=2014-10-16T20:34:37Z')
127+
self.assertEqual(connection._signed,
128+
[('/name/parent%2Fchild',
129+
EXPIRATION, {'method': 'GET'})])
130+
102131
def test_generate_signed_url_w_explicit_method(self):
103132
KEY = 'key'
104133
EXPIRATION = '2014-10-16T20:34:37Z'
@@ -238,6 +267,52 @@ def test_upload_from_file(self):
238267
self.assertEqual(rq[2]['data'], DATA[5:])
239268
self.assertEqual(rq[2]['headers'], {'Content-Range': 'bytes 5-5/6'})
240269

270+
def test_upload_from_file_w_slash_in_name(self):
271+
from tempfile import NamedTemporaryFile
272+
from urlparse import parse_qsl
273+
from urlparse import urlsplit
274+
KEY = 'parent/child'
275+
UPLOAD_URL = 'http://example.com/upload/name/parent%2Fchild'
276+
DATA = 'ABCDEF'
277+
loc_response = {'location': UPLOAD_URL}
278+
chunk1_response = {}
279+
chunk2_response = {}
280+
connection = _Connection(
281+
(loc_response, ''),
282+
(chunk1_response, ''),
283+
(chunk2_response, ''),
284+
)
285+
bucket = _Bucket(connection)
286+
key = self._makeOne(bucket, KEY)
287+
key.CHUNK_SIZE = 5
288+
with NamedTemporaryFile() as fh:
289+
fh.write(DATA)
290+
fh.flush()
291+
key.upload_from_file(fh, rewind=True)
292+
rq = connection._requested
293+
self.assertEqual(len(rq), 3)
294+
self.assertEqual(rq[0]['method'], 'POST')
295+
uri = rq[0]['url']
296+
scheme, netloc, path, qs, _ = urlsplit(uri)
297+
self.assertEqual(scheme, 'http')
298+
self.assertEqual(netloc, 'example.com')
299+
self.assertEqual(path, '/b/name/o')
300+
self.assertEqual(dict(parse_qsl(qs)),
301+
{'uploadType': 'resumable', 'name': 'parent%2Fchild'})
302+
self.assertEqual(rq[0]['headers'],
303+
{'X-Upload-Content-Length': 6,
304+
'X-Upload-Content-Type': 'application/unknown'})
305+
self.assertEqual(rq[1]['method'], 'POST')
306+
self.assertEqual(rq[1]['url'], UPLOAD_URL)
307+
self.assertEqual(rq[1]['content_type'], 'text/plain')
308+
self.assertEqual(rq[1]['data'], DATA[:5])
309+
self.assertEqual(rq[1]['headers'], {'Content-Range': 'bytes 0-4/6'})
310+
self.assertEqual(rq[2]['method'], 'POST')
311+
self.assertEqual(rq[2]['url'], UPLOAD_URL)
312+
self.assertEqual(rq[2]['content_type'], 'text/plain')
313+
self.assertEqual(rq[2]['data'], DATA[5:])
314+
self.assertEqual(rq[2]['headers'], {'Content-Range': 'bytes 5-5/6'})
315+
241316
def test_upload_from_filename(self):
242317
from tempfile import NamedTemporaryFile
243318
from urlparse import parse_qsl

0 commit comments

Comments
 (0)
0