Repository: googleapis/google-cloud-python

Issue:
public_url returning quoted_name with '/'
i'm adding my files inside folders, so i give the blob name as `"UUID/file.ext"`, but when i request the [public_url()](https://github.com/GoogleCloudPlatform/google-cloud-python/blob/d0cfeb772fd612179984a191be1d8189b369a2c5/storage/google/cloud/storage/blob.py#L224) i get the fullname as [quoted](https://github.com/GoogleCloudPlatform/google-cloud-python/blob/d0cfeb772fd612179984a191be1d8189b369a2c5/storage/google/cloud/storage/blob.py#L233) example: `".../MYBUCKET/UUID%2Ffile.ext"` 

both `"%2F"` and `"/"` in the link downloads the same file, but with different names


Discussion:
Hi @jlvcm,
Thanks for reporting. We will look into this.
Just to add, this also breaks `generate_signed_url` for objects with `/` characters.  The signed string contains `%2F` instead of `/`, so the signature doesn't match the one generated by the server.  

The problem seems to be this line: [https://github.com/GoogleCloudPlatform/google-cloud-python/blob/master/storage/google/cloud/storage/blob.py#L1579][1]

The `safe=''` overrides the default behavior of `urllib.parse.quote`, which is to leave `/` characters alone.  If you remove that param it should fix the issue, although you might need to confirm that other callers aren't expecting slashes to be escaped.

[1]: https://github.com/GoogleCloudPlatform/google-cloud-python/blob/66b5a31d95f6b39684c7f01dd72f64b3a945a71b/storage/google/cloud/storage/blob.py#L1579
Hitting this bug as well. 
Is there an ETA for the fix?

Resolving patch:
diff --git a/storage/google/cloud/storage/blob.py b/storage/google/cloud/storage/blob.py
--- a/storage/google/cloud/storage/blob.py
+++ b/storage/google/cloud/storage/blob.py
@@ -244,7 +244,7 @@ def public_url(self):
         return '{storage_base_url}/{bucket_name}/{quoted_name}'.format(
             storage_base_url=_API_ACCESS_ENDPOINT,
             bucket_name=self.bucket.name,
-            quoted_name=_quote(self.name))
+            quoted_name=quote(self.name.encode('utf-8')))
 
     def generate_signed_url(self, expiration, method='GET',
                             content_type=None,
@@ -315,7 +315,7 @@ def generate_signed_url(self, expiration, method='GET',
         """
         resource = '/{bucket_name}/{quoted_name}'.format(
             bucket_name=self.bucket.name,
-            quoted_name=_quote(self.name))
+            quoted_name=quote(self.name))
 
         if credentials is None:
             client = self._require_client(client)


Test patch:
diff --git a/storage/tests/system.py b/storage/tests/system.py
--- a/storage/tests/system.py
+++ b/storage/tests/system.py
@@ -554,20 +554,24 @@ def setUpClass(cls):
         # Make sure bucket empty before beginning.
         _empty_bucket(cls.bucket)
 
+        cls.suite_blobs_to_delete = []
         simple_path = cls.FILES['simple']['path']
-        blob = storage.Blob(cls.FILENAMES[0], bucket=cls.bucket)
-        blob.upload_from_filename(simple_path)
-        cls.suite_blobs_to_delete = [blob]
-        for filename in cls.FILENAMES[1:]:
-            new_blob = retry_bad_copy(cls.bucket.copy_blob)(
-                blob, cls.bucket, filename)
-            cls.suite_blobs_to_delete.append(new_blob)
+        for filename in cls.FILENAMES:
+            blob = storage.Blob(filename, bucket=cls.bucket)
+            blob.upload_from_filename(simple_path)
+            cls.suite_blobs_to_delete.append(blob)
 
     @classmethod
     def tearDownClass(cls):
         for blob in cls.suite_blobs_to_delete:
             blob.delete()
 
+    @RetryErrors(unittest.TestCase.failureException)
+    def test_blob_get_w_delimiter(self):
+        for filename in self.FILENAMES:
+            blob = self.bucket.blob(filename)
+            self.assertTrue(blob.exists(), filename)
+
     @RetryErrors(unittest.TestCase.failureException)
     def test_root_level_w_delimiter(self):
         iterator = self.bucket.list_blobs(delimiter='/')
diff --git a/storage/tests/unit/test_blob.py b/storage/tests/unit/test_blob.py
--- a/storage/tests/unit/test_blob.py
+++ b/storage/tests/unit/test_blob.py
@@ -172,7 +172,7 @@ def test_public_url_w_slash_in_name(self):
         blob = self._make_one(BLOB_NAME, bucket=bucket)
         self.assertEqual(
             blob.public_url,
-            'https://storage.googleapis.com/name/parent%2Fchild')
+            'https://storage.googleapis.com/name/parent/child')
 
     def test_public_url_with_non_ascii(self):
         blob_name = u'winter \N{snowman}'
@@ -275,7 +275,7 @@ def test_generate_signed_url_w_slash_in_name(self):
             'api_access_endpoint': 'https://storage.googleapis.com',
             'expiration': EXPIRATION,
             'method': 'GET',
-            'resource': '/name/parent%2Fchild',
+            'resource': '/name/parent/child',
             'content_type': None,
             'response_type': None,
             'response_disposition': None,