Repository: fsouza/fake-gcs-server (go)

Issue:
python gcs client will throw ValueError because of wrong timestamp format
The python GCS client somehow takes its format very similar to RFC3339 but not exactly the same so it will throw.

The format it uses is actually:

`"2006-01-02T15:04:05.999999Z07:00"`

not

`time.RFC3339`

 would this be fixed?

Pull request description:
Customize time format for compatibility with Python client
Todo

- [x] Update Python example to use code reported in #199 

Closes #199.

Patch:
diff --git a/examples/python/python.py b/examples/python/python.py
index f5bad12..ed79727 100644
--- a/examples/python/python.py
+++ b/examples/python/python.py
@@ -3,6 +3,7 @@
 # license that can be found in the LICENSE file.
 
 import os
+import tempfile
 
 import requests
 import urllib3
@@ -15,13 +16,13 @@ PUBLIC_HOST = os.getenv("PUBLIC_HOST", "storage.gcs.127.0.0.1.nip.io:4443")
 
 storage.blob._API_ACCESS_ENDPOINT = "https://" + PUBLIC_HOST
 storage.blob._DOWNLOAD_URL_TEMPLATE = (
-    u"%s/download/storage/v1{path}?alt=media" % EXTERNAL_URL
+    "%s/download/storage/v1{path}?alt=media" % EXTERNAL_URL
 )
 storage.blob._BASE_UPLOAD_TEMPLATE = (
-    u"%s/upload/storage/v1{bucket_path}/o?uploadType=" % EXTERNAL_URL
+    "%s/upload/storage/v1{bucket_path}/o?uploadType=" % EXTERNAL_URL
 )
-storage.blob._MULTIPART_URL_TEMPLATE = storage.blob._BASE_UPLOAD_TEMPLATE + u"multipart"
-storage.blob._RESUMABLE_URL_TEMPLATE = storage.blob._BASE_UPLOAD_TEMPLATE + u"resumable"
+storage.blob._MULTIPART_URL_TEMPLATE = storage.blob._BASE_UPLOAD_TEMPLATE + "multipart"
+storage.blob._RESUMABLE_URL_TEMPLATE = storage.blob._BASE_UPLOAD_TEMPLATE + "resumable"
 
 my_http = requests.Session()
 my_http.verify = False  # disable SSL validation
@@ -46,5 +47,7 @@ for bucket in client.list_buckets():
 
         # Print the content of the Blob
         b = bucket.get_blob(blob.name)
-        s = b.download_as_string()
-        print(s, "\n")
+        with tempfile.NamedTemporaryFile() as temp_file:
+            s = b.download_to_filename(temp_file.name)
+            temp_file.seek(0, 0)
+            print(temp_file.read(), "\n")
diff --git a/fakestorage/object.go b/fakestorage/object.go
index 7d10eb6..2547965 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -131,9 +131,9 @@ func toBackendObjects(objects []Object) []backend.Object {
 			Crc32c:          o.Crc32c,
 			Md5Hash:         o.Md5Hash,
 			ACL:             o.ACL,
-			Created:         getCurrentIfZero(o.Created).Format(time.RFC3339),
-			Deleted:         o.Deleted.Format(time.RFC3339),
-			Updated:         getCurrentIfZero(o.Updated).Format(time.RFC3339),
+			Created:         getCurrentIfZero(o.Created).Format(timestampFormat),
+			Deleted:         o.Deleted.Format(timestampFormat),
+			Updated:         getCurrentIfZero(o.Updated).Format(timestampFormat),
 			Generation:      o.Generation,
 			Metadata:        o.Metadata,
 		})
@@ -164,7 +164,7 @@ func fromBackendObjects(objects []backend.Object) []Object {
 }
 
 func convertTimeWithoutError(t string) time.Time {
-	r, _ := time.Parse(time.RFC3339, t)
+	r, _ := time.Parse(timestampFormat, t)
 	return r
 }
 
diff --git a/fakestorage/response.go b/fakestorage/response.go
index e4fcb2d..54f2213 100644
--- a/fakestorage/response.go
+++ b/fakestorage/response.go
@@ -6,13 +6,14 @@ package fakestorage
 
 import (
 	"net/http"
-	"time"
 
 	"github.com/fsouza/fake-gcs-server/internal/backend"
 	"google.golang.org/api/googleapi"
 	"google.golang.org/api/storage/v1"
 )
 
+const timestampFormat = "2006-01-02T15:04:05.999999Z07:00"
+
 type listResponse struct {
 	Kind     string        `json:"kind"`
 	Items    []interface{} `json:"items"`
@@ -48,7 +49,7 @@ func newBucketResponse(bucket backend.Bucket) bucketResponse {
 		ID:          bucket.Name,
 		Name:        bucket.Name,
 		Versioning:  &bucketVersioning{bucket.VersioningEnabled},
-		TimeCreated: bucket.TimeCreated.Format(time.RFC3339),
+		TimeCreated: bucket.TimeCreated.Format(timestampFormat),
 	}
 }
 
@@ -97,9 +98,9 @@ func newObjectResponse(obj Object) objectResponse {
 		Md5Hash:         obj.Md5Hash,
 		ACL:             acl,
 		Metadata:        obj.Metadata,
-		TimeCreated:     obj.Created.Format(time.RFC3339),
-		TimeDeleted:     obj.Deleted.Format(time.RFC3339),
-		Updated:         obj.Updated.Format(time.RFC3339),
+		TimeCreated:     obj.Created.Format(timestampFormat),
+		TimeDeleted:     obj.Deleted.Format(timestampFormat),
+		Updated:         obj.Updated.Format(timestampFormat),
 		Generation:      obj.Generation,
 	}
 }
diff --git a/internal/backend/memory.go b/internal/backend/memory.go
index 933ed5c..bbd6eb3 100644
--- a/internal/backend/memory.go
+++ b/internal/backend/memory.go
@@ -11,6 +11,8 @@ import (
 	"time"
 )
 
+const timestampFormat = "2006-01-02T15:04:05.999999Z07:00"
+
 // storageMemory is an implementation of the backend storage that stores data
 // in memory.
 type storageMemory struct {
@@ -20,6 +22,8 @@ type storageMemory struct {
 
 type bucketInMemory struct {
 	Bucket
+	// maybe we can refactor how the memory backend works? no need to store
+	// Object instances.
 	activeObjects   []Object
 	archivedObjects []Object
 }
@@ -33,7 +37,7 @@ func (bm *bucketInMemory) addObject(obj Object) Object {
 	index := findObject(obj, bm.activeObjects, false)
 	if index >= 0 {
 		if bm.VersioningEnabled {
-			bm.activeObjects[index].Deleted = time.Now().Format(time.RFC3339)
+			bm.activeObjects[index].Deleted = time.Now().Format(timestampFormat)
 			bm.cpToArchive(bm.activeObjects[index])
 		}
 		bm.activeObjects[index] = obj
@@ -57,7 +61,7 @@ func (bm *bucketInMemory) deleteObject(obj Object, matchGeneration bool) {
 		return
 	}
 	if bm.VersioningEnabled {
-		obj.Deleted = time.Now().Format(time.RFC3339)
+		obj.Deleted = time.Now().Format(timestampFormat)
 		bm.mvToArchive(obj)
 	} else {
 		bm.deleteFromObjectList(obj, true)


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index d39235a..64d5fc2 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -45,7 +45,7 @@ func getObjectTestCases() objectTestCases {
 		contentEncoding = "gzip"
 		metaValue       = "MetaValue"
 	)
-	testInitExecTime := time.Now()
+	testInitExecTime := time.Now().Truncate(time.Microsecond)
 	checksum := uint32Checksum([]byte(content))
 	hash := md5Hash([]byte(content))
 
@@ -84,10 +84,10 @@ func checkObjectAttrs(testObj Object, attrs *storage.ObjectAttrs, t *testing.T)
 	if attrs.Name != testObj.Name {
 		t.Errorf("wrong object name\nwant %q\ngot  %q", testObj.Name, attrs.Name)
 	}
-	if !(testObj.Created.IsZero()) && testObj.Created.Equal(attrs.Created) {
+	if !(testObj.Created.IsZero()) && !testObj.Created.Equal(attrs.Created) {
 		t.Errorf("wrong created date\nwant %v\ngot   %v\nname %v", testObj.Created, attrs.Created, attrs.Name)
 	}
-	if !(testObj.Updated.IsZero()) && testObj.Updated.Equal(attrs.Updated) {
+	if !(testObj.Updated.IsZero()) && !testObj.Updated.Equal(attrs.Updated) {
 		t.Errorf("wrong updated date\nwant %v\ngot   %v\nname %v", testObj.Updated, attrs.Updated, attrs.Name)
 	}
 	if testObj.Created.IsZero() && attrs.Created.IsZero() {
@@ -1131,7 +1131,7 @@ func TestServerClientObjectDeleteWithVersioning(t *testing.T) {
 			t.Fatalf("unable to retrieve archived object. err: %v", err)
 		}
 		if objWithGen.Deleted.IsZero() || objWithGen.Deleted.Before(objWithGen.Created) {
-			t.Errorf("unexpected delete time, %v", objWithGen.Deleted)
+			t.Errorf("unexpected delete time.\ndeleted: %v\ncreated: %v", objWithGen.Deleted, objWithGen.Created)
 		}
 	})
 }