Repository: googleapis/google-cloud-python

Issue:
In storage.Key.get_contents_to_filename, improve error checking and exception handling and add timestamp to file object
From TODO:

``` python
    # TODO: Add good error checking.
    # TODO: Add good exception handling.
    # TODO: Set timestamp? Make optional, default being to set it if possible?
```

Also, consider renaming `get_contents_to_filename` and `get_contents_to_file`. The `get` is a misleading verb, since a `read` into a file is what is happening.

See [`gcloud.storage.Key.get_contents_to_filename`](https://github.com/GoogleCloudPlatform/gcloud-python/blob/1f2aaf3606802f4f02eeb30e22e1082ef7f4594e/gcloud/storage/key.py#L169) for relevant code and #146 for some context.



Discussion:
`git blame` output

```
b7f1b774 (JJ Geewax   2014-01-29 18:02:29 -0500 169)     # TODO: Add good error checking.
b7f1b774 (JJ Geewax   2014-01-29 18:02:29 -0500 170)     # TODO: Add good exception handling.
b7f1b774 (JJ Geewax   2014-01-29 18:02:29 -0500 171)     # TODO: Set timestamp? Make optional, default being to set it if possible?
```

@jgeewax WRT raising / handling errors:  the `open()` call will raise IOError on failure:  ISTM that we can just let that error propagate, as with any error raised by `self.get_contents_to_file()`.

WRT the timestamp:  we open the file in 'wb' mode, which will cause its mtime to be updated by the OS.  Are you suggesting that we set the timestamp to a different value (e.g., based on some metadata?)  We aren't saving any such metadata when uploading.

@dhermes : The naming was based on [boto's names](http://boto.readthedocs.org/en/latest/ref/s3.html#boto.s3.key.Key.get_contents_to_file).

@tseaver : Re [`open()` call](https://github.com/GoogleCloudPlatform/gcloud-python/blob/1f2aaf3606802f4f02eeb30e22e1082ef7f4594e/gcloud/storage/key.py#L172), you mean if the filename is invalid ? i agree it's fine to let that propagate up.. i wouldn't expect that error to get caught.

Re timestamp: I think my thought was that we could set the last modified time of the local file to the last modified time of the object in GCS. i wouldn't mind if we decided to scrap that all together though... 

WRT naming: I would prefer names like `save_contents_to_filename` and `upload_contents_from_filename`, which make the directionality unambiguous.

#311 tracks the enhancement for synchronizing the file's mtime.

WRT naming: I like @tseaver proposal. If we do the change, I think we should make the naming uniform for all the get_contents_\* and save_contents_\* methods (a total of 6).

Do we have any objections to keeping the old names as aliases ? (And noting those aliases in the docs?)

I'd like it to be relatively easy to move from boto -> gcloud -- and aliases don't really hurt... right?


Resolving patch:
diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py
--- a/gcloud/storage/key.py
+++ b/gcloud/storage/key.py
@@ -162,8 +162,8 @@ def delete(self):
         """
         return self.bucket.delete_key(self)
 
-    def get_contents_to_file(self, file_obj):
-        """Gets the contents of this key to a file-like object.
+    def download_to_file(self, file_obj):
+        """Download the contents of this key into a file-like object.
 
         :type file_obj: file
         :param file_obj: A file handle to which to write the key's data.
@@ -173,8 +173,11 @@ def get_contents_to_file(self, file_obj):
         for chunk in _KeyDataIterator(self):
             file_obj.write(chunk)
 
-    def get_contents_to_filename(self, filename):
-        """Get the contents of this key to a file by name.
+    # NOTE: Alias for boto-like API.
+    get_contents_to_file = download_to_file
+
+    def download_to_filename(self, filename):
+        """Download the contents of this key into a named file.
 
         :type filename: string
         :param filename: A filename to be passed to ``open``.
@@ -182,22 +185,28 @@ def get_contents_to_filename(self, filename):
         :raises: :class:`gcloud.storage.exceptions.NotFoundError`
         """
         with open(filename, 'wb') as file_obj:
-            self.get_contents_to_file(file_obj)
+            self.download_to_file(file_obj)
+
+    # NOTE: Alias for boto-like API.
+    get_contents_to_filename = download_to_filename
 
-    def get_contents_as_string(self):
-        """Gets the data stored on this Key as a string.
+    def download_as_string(self):
+        """Download the contents of this key as a string.
 
         :rtype: string
         :returns: The data stored in this key.
         :raises: :class:`gcloud.storage.exceptions.NotFoundError`
         """
         string_buffer = StringIO()
-        self.get_contents_to_file(string_buffer)
+        self.download_to_file(string_buffer)
         return string_buffer.getvalue()
 
-    def set_contents_from_file(self, file_obj, rewind=False, size=None,
-                               content_type=None):
-        """Set the contents of this key to the contents of a file handle.
+    # NOTE: Alias for boto-like API.
+    get_contents_as_string = download_as_string
+
+    def upload_from_file(self, file_obj, rewind=False, size=None,
+                         content_type=None):
+        """Upload the contents of this key from a file-like object.
 
         :type file_obj: file
         :param file_obj: A file handle open for reading.
@@ -255,8 +264,11 @@ def set_contents_from_file(self, file_obj, rewind=False, size=None,
 
             bytes_uploaded += chunk_size
 
-    def set_contents_from_filename(self, filename):
-        """Open a path and set this key's contents to the content of that file.
+    # NOTE: Alias for boto-like API.
+    set_contents_from_file = upload_from_file
+
+    def upload_from_filename(self, filename):
+        """Upload this key's contents from the content of f named file.
 
         :type filename: string
         :param filename: The path to the file.
@@ -264,21 +276,13 @@ def set_contents_from_filename(self, filename):
         content_type, _ = mimetypes.guess_type(filename)
 
         with open(filename, 'rb') as file_obj:
-            self.set_contents_from_file(file_obj, content_type=content_type)
+            self.upload_from_file(file_obj, content_type=content_type)
 
-    def set_contents_from_string(self, data, content_type='text/plain'):
-        """Sets the contents of this key to the provided string.
+    # NOTE: Alias for boto-like API.
+    set_contents_from_filename = upload_from_filename
 
-        You can use this method to quickly set the value of a key::
-
-          >>> from gcloud import storage
-          >>> connection = storage.get_connection(project, email, key_path)
-          >>> bucket = connection.get_bucket(bucket_name)
-          >>> key = bucket.new_key('my_text_file.txt')
-          >>> key.set_contents_from_string('This is the contents of my file!')
-
-        Under the hood this is using a string buffer and calling
-        :func:`gcloud.storage.key.Key.set_contents_from_file`.
+    def upload_from_string(self, data, content_type='text/plain'):
+        """Upload contents of this key from the provided string.
 
         :type data: string
         :param data: The data to store in this key.
@@ -293,6 +297,9 @@ def set_contents_from_string(self, data, content_type='text/plain'):
                                     content_type=content_type)
         return self
 
+    # NOTE: Alias for boto-like API.
+    set_contents_from_string = upload_from_string
+
     def has_metadata(self, field=None):
         """Check if metadata is available locally.
 


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
@@ -142,7 +142,7 @@ def test_delete(self):
         key.delete()
         self.assertFalse(key.exists())
 
-    def test_get_contents_to_file(self):
+    def test_download_to_file(self):
         from StringIO import StringIO
         from gcloud._testing import _Monkey
         from gcloud.storage import key as MUT
@@ -153,10 +153,10 @@ def test_get_contents_to_file(self):
         key = self._makeOne(bucket, KEY)
         fh = StringIO()
         with _Monkey(MUT, _KeyDataIterator=lambda self: iter(_CHUNKS)):
-            key.get_contents_to_file(fh)
+            key.download_to_file(fh)
         self.assertEqual(fh.getvalue(), ''.join(_CHUNKS))
 
-    def test_get_contents_to_filename(self):
+    def test_download_to_filename(self):
         from tempfile import NamedTemporaryFile
         from gcloud._testing import _Monkey
         from gcloud.storage import key as MUT
@@ -167,13 +167,13 @@ def test_get_contents_to_filename(self):
         key = self._makeOne(bucket, KEY)
         with _Monkey(MUT, _KeyDataIterator=lambda self: iter(_CHUNKS)):
             with NamedTemporaryFile() as f:
-                key.get_contents_to_filename(f.name)
+                key.download_to_filename(f.name)
                 f.flush()
                 with open(f.name) as g:
                     wrote = g.read()
         self.assertEqual(wrote, ''.join(_CHUNKS))
 
-    def test_get_contents_as_string(self):
+    def test_download_as_string(self):
         from gcloud._testing import _Monkey
         from gcloud.storage import key as MUT
         _CHUNKS = ['abc', 'def']
@@ -182,10 +182,10 @@ def test_get_contents_as_string(self):
         bucket = _Bucket(connection)
         key = self._makeOne(bucket, KEY)
         with _Monkey(MUT, _KeyDataIterator=lambda self: iter(_CHUNKS)):
-            fetched = key.get_contents_as_string()
+            fetched = key.download_as_string()
         self.assertEqual(fetched, ''.join(_CHUNKS))
 
-    def test_set_contents_from_file(self):
+    def test_upload_from_file(self):
         from tempfile import NamedTemporaryFile
         from urlparse import parse_qsl
         from urlparse import urlsplit
@@ -206,7 +206,7 @@ def test_set_contents_from_file(self):
         with NamedTemporaryFile() as fh:
             fh.write(DATA)
             fh.flush()
-            key.set_contents_from_file(fh, rewind=True)
+            key.upload_from_file(fh, rewind=True)
         rq = connection._requested
         self.assertEqual(len(rq), 3)
         self.assertEqual(rq[0]['method'], 'POST')
@@ -231,7 +231,7 @@ def test_set_contents_from_file(self):
         self.assertEqual(rq[2]['data'], DATA[5:])
         self.assertEqual(rq[2]['headers'], {'Content-Range': 'bytes 5-5/6'})
 
-    def test_set_contents_from_filename(self):
+    def test_upload_from_filename(self):
         from tempfile import NamedTemporaryFile
         from urlparse import parse_qsl
         from urlparse import urlsplit
@@ -252,7 +252,7 @@ def test_set_contents_from_filename(self):
         with NamedTemporaryFile(suffix='.jpeg') as fh:
             fh.write(DATA)
             fh.flush()
-            key.set_contents_from_filename(fh.name)
+            key.upload_from_filename(fh.name)
         rq = connection._requested
         self.assertEqual(len(rq), 3)
         self.assertEqual(rq[0]['method'], 'POST')
@@ -277,7 +277,7 @@ def test_set_contents_from_filename(self):
         self.assertEqual(rq[2]['data'], DATA[5:])
         self.assertEqual(rq[2]['headers'], {'Content-Range': 'bytes 5-5/6'})
 
-    def test_set_contents_from_string(self):
+    def test_upload_from_string(self):
         from urlparse import parse_qsl
         from urlparse import urlsplit
         KEY = 'key'
@@ -294,7 +294,7 @@ def test_set_contents_from_string(self):
         bucket = _Bucket(connection)
         key = self._makeOne(bucket, KEY)
         key.CHUNK_SIZE = 5
-        key.set_contents_from_string(DATA)
+        key.upload_from_string(DATA)
         rq = connection._requested
         self.assertEqual(len(rq), 3)
         self.assertEqual(rq[0]['method'], 'POST')