Repository: googleapis/google-cloud-python

Issue:
Can not "get_contents_as_*" when key is in a folder
Warning : I might miss something fundamental in the "google storage" nomenclature !

The problem is as follows: when ones creates a key in a folder it is impossible to retrieve its data. 
To code below reproduces the issue on version 0.02.2.

``` python
import time
from gcloud import storage
from gcloud.storage.demo import get_connection

c  = storage.demo.get_connection()
b = c.get_bucket('debug-gcloud-%d' % int(time.time()))
k = b.new_key("bare/my-new-file.txt")
k = set_contents_from_string("this is some dataaaaa!")
k = b.get_key("bare/my-new-file.txt")
print k.get_contents_as_string()
```

The error stack is presented below.
This error does not appear when the attribute `name` of the object `key` is url quoted with quote_plus from urlib.

```
Traceback (most recent call last):
  File "test_gcloud.py", line 12, in <module>
    k = b.get_key("bare/my-new-file.txt")
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/gcloud/storage/bucket.py", line 83, in get_key
    response = self.connection.api_request(method='GET', path=key.path)
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/gcloud/storage/connection.py", line 214, in api_request
    raise exceptions.NotFoundError(response, content)
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/gcloud/storage/exceptions.py", line 17, in __init__
    self.message = 'GET %s returned a 404.' % (response.url)
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/httplib2/__init__.py", line 1680, in __getattr__
    raise AttributeError, name
AttributeError: url
```

Can not "get_contents_as_*" when key is in a folder
Warning : I might miss something fundamental in the "google storage" nomenclature !

The problem is as follows: when ones creates a key in a folder it is impossible to retrieve its data. 
To code below reproduces the issue on version 0.02.2.

``` python
import time
from gcloud import storage
from gcloud.storage.demo import get_connection

c  = storage.demo.get_connection()
b = c.get_bucket('debug-gcloud-%d' % int(time.time()))
k = b.new_key("bare/my-new-file.txt")
k = set_contents_from_string("this is some dataaaaa!")
k = b.get_key("bare/my-new-file.txt")
print k.get_contents_as_string()
```

The error stack is presented below.
This error does not appear when the attribute `name` of the object `key` is url quoted with quote_plus from urlib.

```
Traceback (most recent call last):
  File "test_gcloud.py", line 12, in <module>
    k = b.get_key("bare/my-new-file.txt")
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/gcloud/storage/bucket.py", line 83, in get_key
    response = self.connection.api_request(method='GET', path=key.path)
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/gcloud/storage/connection.py", line 214, in api_request
    raise exceptions.NotFoundError(response, content)
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/gcloud/storage/exceptions.py", line 17, in __init__
    self.message = 'GET %s returned a 404.' % (response.url)
  File "/home/user/.virtualenvs/env/local/lib/python2.7/site-packages/httplib2/__init__.py", line 1680, in __getattr__
    raise AttributeError, name
AttributeError: url
```



Discussion:
I just checked:  the [Object Naming spec](https://cloud.google.com/storage/docs/bucketnaming#objectnames) does not disallow the `/` in names.  See #165.

Would it be useful if I issue a PR with the fix I mentioned earlier ?  

@Fkawala yes, that would be great.  Please follow the [contributor guidelines](https://github.com/GoogleCloudPlatform/gcloud-python/blob/master/CONTRIBUTING.rst), including the "Contributor License Agreement" section.

@Fkawala I'll make this fix today (I need it to flesh out the [example app](https://github.com/GoogleCloudPlatform/gcloud-python-expenses-demo) I'm working on).

I just checked:  the [Object Naming spec](https://cloud.google.com/storage/docs/bucketnaming#objectnames) does not disallow the `/` in names.  See #165.

Would it be useful if I issue a PR with the fix I mentioned earlier ?  

@Fkawala yes, that would be great.  Please follow the [contributor guidelines](https://github.com/GoogleCloudPlatform/gcloud-python/blob/master/CONTRIBUTING.rst), including the "Contributor License Agreement" section.

@Fkawala I'll make this fix today (I need it to flesh out the [example app](https://github.com/GoogleCloudPlatform/gcloud-python-expenses-demo) I'm working on).


Resolving patch:
diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py
--- a/gcloud/storage/key.py
+++ b/gcloud/storage/key.py
@@ -4,6 +4,7 @@
 import mimetypes
 import os
 from StringIO import StringIO
+import urllib
 
 from gcloud.storage._helpers import _PropertyMixin
 from gcloud.storage._helpers import _scalar_property
@@ -117,7 +118,7 @@ def path(self):
         elif not self.name:
             raise ValueError('Cannot determine path without a key name.')
 
-        return self.bucket.path + '/o/' + self.name
+        return self.bucket.path + '/o/' + urllib.quote(self.name, safe='')
 
     @property
     def public_url(self):
@@ -126,11 +127,10 @@ def public_url(self):
         :rtype: `string`
         :returns: The public URL for this key.
         """
-        return '{storage_base_url}/{bucket_name}/{key_name}'.format(
+        return '{storage_base_url}/{bucket_name}/{quoted_name}'.format(
             storage_base_url='http://commondatastorage.googleapis.com',
-            key_name=self.name,
             bucket_name=self.bucket.name,
-            )
+            quoted_name=urllib.quote(self.name, safe=''))
 
     def generate_signed_url(self, expiration, method='GET'):
         """Generates a signed URL for this key.
@@ -153,10 +153,9 @@ def generate_signed_url(self, expiration, method='GET'):
         :returns: A signed URL you can use to access the resource
                   until expiration.
         """
-        resource = '/{bucket_name}/{key_name}'.format(
-            key_name=self.name,
+        resource = '/{bucket_name}/{quoted_name}'.format(
             bucket_name=self.bucket.name,
-            )
+            quoted_name=urllib.quote(self.name, safe=''))
         return self.connection.generate_signed_url(resource=resource,
                                                    expiration=expiration,
                                                    method=method)
@@ -285,9 +284,14 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
             'X-Upload-Content-Length': total_bytes,
         }
 
+        query_params = {
+            'uploadType': 'resumable',
+            'name': urllib.quote_plus(self.name),
+        }
+
         upload_url = self.connection.build_api_url(
             path=self.bucket.path + '/o',
-            query_params={'uploadType': 'resumable', 'name': self.name},
+            query_params=query_params,
             api_base_url=self.connection.API_BASE_URL + '/upload')
 
         response, _ = self.connection.make_request(


Test patch:
diff --git a/gcloud/storage/test_key.py b/gcloud/storage/test_key.py
--- a/gcloud/storage/test_key.py
+++ b/gcloud/storage/test_key.py
@@ -78,6 +78,13 @@ def test_path_normal(self):
         key = self._makeOne(bucket, KEY)
         self.assertEqual(key.path, '/b/name/o/%s' % KEY)
 
+    def test_path_w_slash_in_name(self):
+        KEY = 'parent/child'
+        connection = _Connection()
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
+        self.assertEqual(key.path, '/b/name/o/parent%2Fchild')
+
     def test_public_url(self):
         KEY = 'key'
         connection = _Connection()
@@ -87,6 +94,15 @@ def test_public_url(self):
                          'http://commondatastorage.googleapis.com/name/%s' %
                          KEY)
 
+    def test_public_url_w_slash_in_name(self):
+        KEY = 'parent/child'
+        connection = _Connection()
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
+        self.assertEqual(
+            key.public_url,
+            'http://commondatastorage.googleapis.com/name/parent%2Fchild')
+
     def test_generate_signed_url_w_default_method(self):
         KEY = 'key'
         EXPIRATION = '2014-10-16T20:34:37Z'
@@ -99,6 +115,19 @@ def test_generate_signed_url_w_default_method(self):
         self.assertEqual(connection._signed,
                          [('/name/key', EXPIRATION, {'method': 'GET'})])
 
+    def test_generate_signed_url_w_slash_in_name(self):
+        KEY = 'parent/child'
+        EXPIRATION = '2014-10-16T20:34:37Z'
+        connection = _Connection()
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
+        self.assertEqual(key.generate_signed_url(EXPIRATION),
+                         'http://example.com/abucket/akey?Signature=DEADBEEF'
+                         '&Expiration=2014-10-16T20:34:37Z')
+        self.assertEqual(connection._signed,
+                         [('/name/parent%2Fchild',
+                           EXPIRATION, {'method': 'GET'})])
+
     def test_generate_signed_url_w_explicit_method(self):
         KEY = 'key'
         EXPIRATION = '2014-10-16T20:34:37Z'
@@ -238,6 +267,52 @@ def test_upload_from_file(self):
         self.assertEqual(rq[2]['data'], DATA[5:])
         self.assertEqual(rq[2]['headers'], {'Content-Range': 'bytes 5-5/6'})
 
+    def test_upload_from_file_w_slash_in_name(self):
+        from tempfile import NamedTemporaryFile
+        from urlparse import parse_qsl
+        from urlparse import urlsplit
+        KEY = 'parent/child'
+        UPLOAD_URL = 'http://example.com/upload/name/parent%2Fchild'
+        DATA = 'ABCDEF'
+        loc_response = {'location': UPLOAD_URL}
+        chunk1_response = {}
+        chunk2_response = {}
+        connection = _Connection(
+            (loc_response, ''),
+            (chunk1_response, ''),
+            (chunk2_response, ''),
+        )
+        bucket = _Bucket(connection)
+        key = self._makeOne(bucket, KEY)
+        key.CHUNK_SIZE = 5
+        with NamedTemporaryFile() as fh:
+            fh.write(DATA)
+            fh.flush()
+            key.upload_from_file(fh, rewind=True)
+        rq = connection._requested
+        self.assertEqual(len(rq), 3)
+        self.assertEqual(rq[0]['method'], 'POST')
+        uri = rq[0]['url']
+        scheme, netloc, path, qs, _ = urlsplit(uri)
+        self.assertEqual(scheme, 'http')
+        self.assertEqual(netloc, 'example.com')
+        self.assertEqual(path, '/b/name/o')
+        self.assertEqual(dict(parse_qsl(qs)),
+                         {'uploadType': 'resumable', 'name': 'parent%2Fchild'})
+        self.assertEqual(rq[0]['headers'],
+                         {'X-Upload-Content-Length': 6,
+                          'X-Upload-Content-Type': 'application/unknown'})
+        self.assertEqual(rq[1]['method'], 'POST')
+        self.assertEqual(rq[1]['url'], UPLOAD_URL)
+        self.assertEqual(rq[1]['content_type'], 'text/plain')
+        self.assertEqual(rq[1]['data'], DATA[:5])
+        self.assertEqual(rq[1]['headers'], {'Content-Range': 'bytes 0-4/6'})
+        self.assertEqual(rq[2]['method'], 'POST')
+        self.assertEqual(rq[2]['url'], UPLOAD_URL)
+        self.assertEqual(rq[2]['content_type'], 'text/plain')
+        self.assertEqual(rq[2]['data'], DATA[5:])
+        self.assertEqual(rq[2]['headers'], {'Content-Range': 'bytes 5-5/6'})
+
     def test_upload_from_filename(self):
         from tempfile import NamedTemporaryFile
         from urlparse import parse_qsl