Repository: fsouza/fake-gcs-server
Pull request #141 (2019-11-26 11:25:29)

Metadata headers
Added support for Object metadata headers

idanya: @fsouza Can you please review?
fsouza: @idanya sorry about the ridiculous delay. I'm catching up with PRs and will take a look at this one some time this week!

Thanks for your patience :D 

Patch:
diff --git a/fakestorage/object.go b/fakestorage/object.go
index 99d1e14..55ba836 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -31,10 +31,11 @@ type Object struct {
 	ACL     []storage.ACLRule `json:"acl,omitempty"`
 	// Dates and generation can be manually injected, so you can do assertions on them,
 	// or let us fill these fields for you
-	Created    time.Time `json:"created,omitempty"`
-	Updated    time.Time `json:"updated,omitempty"`
-	Deleted    time.Time `json:"deleted,omitempty"`
-	Generation int64     `json:"generation,omitempty"`
+	Created    time.Time         `json:"created,omitempty"`
+	Updated    time.Time         `json:"updated,omitempty"`
+	Deleted    time.Time         `json:"deleted,omitempty"`
+	Generation int64             `json:"generation,omitempty"`
+	Metadata   map[string]string `json:"metadata,omitempty"`
 }
 
 func (o *Object) id() string {
@@ -125,6 +126,7 @@ func toBackendObjects(objects []Object) []backend.Object {
 			Deleted:         o.Deleted.Format(time.RFC3339),
 			Updated:         getCurrentIfZero(o.Updated).Format(time.RFC3339),
 			Generation:      o.Generation,
+			Metadata:        o.Metadata,
 		})
 	}
 	return backendObjects
@@ -146,6 +148,7 @@ func fromBackendObjects(objects []backend.Object) []Object {
 			Deleted:         convertTimeWithoutError(o.Deleted),
 			Updated:         convertTimeWithoutError(o.Updated),
 			Generation:      o.Generation,
+			Metadata:        o.Metadata,
 		})
 	}
 	return backendObjects
@@ -307,6 +310,7 @@ func (s *Server) rewriteObject(w http.ResponseWriter, r *http.Request) {
 		Md5Hash:     obj.Md5Hash,
 		ContentType: obj.ContentType,
 		ACL:         obj.ACL,
+		Metadata:    obj.Metadata,
 	}
 	s.CreateObject(newObject)
 	w.Header().Set("Content-Type", "application/json")
diff --git a/fakestorage/response.go b/fakestorage/response.go
index 62f26be..c9e990b 100644
--- a/fakestorage/response.go
+++ b/fakestorage/response.go
@@ -77,6 +77,7 @@ type objectResponse struct {
 	TimeDeleted     string                         `json:"timeDeleted,omitempty"`
 	Updated         string                         `json:"updated,omitempty"`
 	Generation      int64                          `json:"generation,string"`
+	Metadata        map[string]string              `json:"metadata,omitempty"`
 }
 
 func newObjectResponse(obj Object) objectResponse {
@@ -93,6 +94,7 @@ func newObjectResponse(obj Object) objectResponse {
 		Crc32c:          obj.Crc32c,
 		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),
diff --git a/internal/backend/object.go b/internal/backend/object.go
index 16d71ba..7da10c5 100644
--- a/internal/backend/object.go
+++ b/internal/backend/object.go
@@ -21,6 +21,7 @@ type Object struct {
 	Crc32c          string
 	Md5Hash         string
 	ACL             []storage.ACLRule
+	Metadata        map[string]string
 	Created         string
 	Deleted         string
 	Updated         string


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index dacf976..794c657 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -8,6 +8,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/binary"
+	"errors"
 	"fmt"
 	"hash/crc32"
 	"io/ioutil"
@@ -42,6 +43,7 @@ func getObjectTestCases() objectTestCases {
 		content         = "some nice content"
 		contentType     = "text/plain; charset=utf-8"
 		contentEncoding = "gzip"
+		metaValue       = "MetaValue"
 	)
 	testInitExecTime := time.Now()
 	checksum := uint32Checksum([]byte(content))
@@ -60,6 +62,10 @@ func getObjectTestCases() objectTestCases {
 			"object with creation, modification dates, and generation",
 			Object{BucketName: bucketName, Name: "img/low-res/party-02.jpg", Content: []byte(content), ContentType: contentType, Crc32c: encodedChecksum(uint32ToBytes(checksum)), Md5Hash: encodedHash(hash), Created: testInitExecTime, Updated: testInitExecTime, Generation: testInitExecTime.UnixNano()},
 		},
+		{
+			"object with everything",
+			Object{BucketName: bucketName, Name: "img/location/meta.jpg", Content: []byte(content), ContentType: contentType, ContentEncoding: contentEncoding, Crc32c: encodedChecksum(uint32ToBytes(checksum)), Md5Hash: encodedHash(hash), Metadata: map[string]string{"MetaHeader": metaValue}},
+		},
 		{
 			"object with no contents neither dates",
 			Object{BucketName: bucketName, Name: "video/hi-res/best_video_1080p.mp4", ContentType: "text/html; charset=utf-8"},
@@ -111,6 +117,11 @@ func checkObjectAttrs(testObj Object, attrs *storage.ObjectAttrs, t *testing.T)
 	if testObj.Content != nil && !bytes.Equal(attrs.MD5, md5Hash(testObj.Content)) {
 		t.Errorf("wrong hash returned\nwant %d\ngot   %d", md5Hash(testObj.Content), attrs.MD5)
 	}
+	if testObj.Metadata != nil {
+		if val, err := getMetadataHeaderFromAttrs(attrs, "MetaHeader"); err != nil || val != testObj.Metadata["MetaHeader"] {
+			t.Errorf("wrong MetaHeader returned\nwant %s\ngot %v", testObj.Metadata["MetaHeader"], val)
+		}
+	}
 }
 
 func TestServerClientObjectAttrs(t *testing.T) {
@@ -155,9 +166,10 @@ func TestServerClientObjectAttrsAfterOverwriteWithVersioning(t *testing.T) {
 			content     = "some nice content"
 			content2    = "some nice content x2"
 			contentType = "text/plain; charset=utf-8"
+			metaValue   = "MetaValue"
 		)
 		server.CreateBucketWithOpts(CreateBucketOpts{Name: bucketName, VersioningEnabled: true})
-		initialObj := Object{BucketName: bucketName, Name: "img/low-res/party-01.jpg", Content: []byte(content), ContentType: contentType, Crc32c: encodedChecksum(uint32ToBytes(uint32Checksum([]byte(content)))), Md5Hash: encodedHash(md5Hash([]byte(content)))}
+		initialObj := Object{BucketName: bucketName, Name: "img/low-res/party-01.jpg", Content: []byte(content), ContentType: contentType, Crc32c: encodedChecksum(uint32ToBytes(uint32Checksum([]byte(content)))), Md5Hash: encodedHash(md5Hash([]byte(content))), Metadata: map[string]string{"MetaHeader": metaValue}}
 		server.CreateObject(initialObj)
 		client := server.Client()
 		objHandle := client.Bucket(bucketName).Object(initialObj.Name)
@@ -189,9 +201,23 @@ func TestServerClientObjectAttrsAfterOverwriteWithVersioning(t *testing.T) {
 		if originalObjAttrsAfterOverwrite.Deleted.IsZero() || originalObjAttrsAfterOverwrite.Deleted.Before(originalObjAttrsAfterOverwrite.Created) {
 			t.Errorf("unexpected delete time, %v", originalObjAttrsAfterOverwrite.Deleted)
 		}
+
+		if val, err := getMetadataHeaderFromAttrs(originalObjAttrsAfterOverwrite, "MetaHeader"); err != nil || val != metaValue {
+			t.Errorf("wrong MetaHeader returned\nwant %s\ngot %v", metaValue, val)
+		}
 	})
 }
 
+func getMetadataHeaderFromAttrs(attrs *storage.ObjectAttrs, headerName string) (string, error) {
+	if attrs.Metadata != nil {
+		if val, ok := attrs.Metadata[headerName]; ok {
+			return val, nil
+		}
+	}
+
+	return "", errors.New("header does not exists")
+}
+
 func TestServerClientObjectAttrsErrors(t *testing.T) {
 	objs := []Object{
 		{BucketName: "some-bucket", Name: "img/hi-res/party-01.jpg"},