Repository: fsouza/fake-gcs-server
Pull request #187 (2020-02-27 12:41:51)

rewriteObject: don't ignore metadata
Server.rewriteObject erroneously copies metadata from the source object, which isn't how GCS behaves. Metadata is provided in the request body, just like when creating objects.

Also, multipartUpload ignored user-defined metadata.


Patch:
diff --git a/fakestorage/object.go b/fakestorage/object.go
index 03bd323..7f9b6f4 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -7,6 +7,7 @@ package fakestorage
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"sort"
 	"strconv"
@@ -318,17 +319,27 @@ func (s *Server) rewriteObject(w http.ResponseWriter, r *http.Request) {
 		http.Error(w, "not found", http.StatusNotFound)
 		return
 	}
+
+	var metadata multipartMetadata
+	err = json.NewDecoder(r.Body).Decode(&metadata)
+	if err != nil && err != io.EOF { // The body is optional
+		http.Error(w, "Invalid metadata", http.StatusBadRequest)
+		return
+	}
+
 	dstBucket := vars["destinationBucket"]
 	newObject := Object{
-		BucketName:  dstBucket,
-		Name:        vars["destinationObject"],
-		Content:     append([]byte(nil), obj.Content...),
-		Crc32c:      obj.Crc32c,
-		Md5Hash:     obj.Md5Hash,
-		ContentType: obj.ContentType,
-		ACL:         obj.ACL,
-		Metadata:    obj.Metadata,
+		BucketName:      dstBucket,
+		Name:            vars["destinationObject"],
+		Content:         append([]byte(nil), obj.Content...),
+		Crc32c:          obj.Crc32c,
+		Md5Hash:         obj.Md5Hash,
+		ACL:             obj.ACL,
+		ContentType:     metadata.ContentType,
+		ContentEncoding: metadata.ContentEncoding,
+		Metadata:        metadata.Metadata,
 	}
+
 	s.CreateObject(newObject)
 	w.Header().Set("Content-Type", "application/json")
 	json.NewEncoder(w).Encode(newObjectRewriteResponse(newObject))
diff --git a/fakestorage/upload.go b/fakestorage/upload.go
index 72d7d63..325bce0 100644
--- a/fakestorage/upload.go
+++ b/fakestorage/upload.go
@@ -26,9 +26,10 @@ import (
 const contentTypeHeader = "Content-Type"
 
 type multipartMetadata struct {
-	ContentType     string `json:"contentType"`
-	ContentEncoding string `json:"contentEncoding"`
-	Name            string `json:"name"`
+	ContentType     string            `json:"contentType"`
+	ContentEncoding string            `json:"contentEncoding"`
+	Name            string            `json:"name"`
+	Metadata        map[string]string `json:"metadata"`
 }
 
 type contentRange struct {
@@ -189,6 +190,7 @@ func (s *Server) multipartUpload(bucketName string, w http.ResponseWriter, r *ht
 		Crc32c:          encodedCrc32cChecksum(content),
 		Md5Hash:         encodedMd5Hash(content),
 		ACL:             getObjectACL(predefinedACL),
+		Metadata:        metadata.Metadata,
 	}
 	err = s.createObject(obj)
 	if err != nil {
@@ -203,8 +205,10 @@ func (s *Server) resumableUpload(bucketName string, w http.ResponseWriter, r *ht
 	objName := r.URL.Query().Get("name")
 	predefinedACL := r.URL.Query().Get("predefinedAcl")
 	contentEncoding := r.URL.Query().Get("contentEncoding")
+	var metadata *multipartMetadata
 	if objName == "" {
-		metadata, err := loadMetadata(r.Body)
+		var err error
+		metadata, err = loadMetadata(r.Body)
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -216,6 +220,7 @@ func (s *Server) resumableUpload(bucketName string, w http.ResponseWriter, r *ht
 		Name:            objName,
 		ContentEncoding: contentEncoding,
 		ACL:             getObjectACL(predefinedACL),
+		Metadata:        metadata.Metadata,
 	}
 	uploadID, err := generateUploadID()
 	if err != nil {


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index 39cf090..25b7df6 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -855,6 +855,7 @@ func TestServiceClientRewriteObject(t *testing.T) {
 			ContentType: contentType,
 			Crc32c:      encodedChecksum(uint32ToBytes(checksum)),
 			Md5Hash:     encodedHash(hash),
+			Metadata:    map[string]string{"foo": "bar"},
 		},
 	}
 
@@ -895,7 +896,10 @@ func TestServiceClientRewriteObject(t *testing.T) {
 				client := server.Client()
 				sourceObject := client.Bucket("first-bucket").Object("files/some-file.txt")
 				dstObject := client.Bucket(test.bucketName).Object(test.objectName)
-				attrs, err := dstObject.CopierFrom(sourceObject).Run(context.TODO())
+				copier := dstObject.CopierFrom(sourceObject)
+				copier.ContentType = contentType
+				copier.Metadata = map[string]string{"baz": "qux"}
+				attrs, err := copier.Run(context.TODO())
 				if err != nil {
 					t.Fatal(err)
 				}
@@ -933,6 +937,9 @@ func TestServiceClientRewriteObject(t *testing.T) {
 				if obj.ContentType != contentType {
 					t.Errorf("wrong content type\nwant %q\ngot  %q", contentType, obj.ContentType)
 				}
+				if !reflect.DeepEqual(obj.Metadata, copier.Metadata) {
+					t.Errorf("wrong meta data\nwant %+v\ngot  %+v", copier.Metadata, obj.Metadata)
+				}
 			})
 		}
 	})
@@ -1015,7 +1022,9 @@ func TestServiceClientRewriteObjectWithGenerations(t *testing.T) {
 				expectedChecksum := uint32Checksum([]byte(overwrittenContent))
 				expectedHash := md5Hash([]byte(overwrittenContent))
 				dstObject := client.Bucket(test.bucketName).Object(test.objectName)
-				attrs, err := dstObject.CopierFrom(sourceObject).Run(context.TODO())
+				copier := dstObject.CopierFrom(sourceObject)
+				copier.ContentType = contentType
+				attrs, err := copier.Run(context.TODO())
 				if err != nil {
 					if test.expectErr {
 						t.Skip("we were expecting an error for this test")
diff --git a/fakestorage/upload_test.go b/fakestorage/upload_test.go
index 06a7b84..07e0d48 100644
--- a/fakestorage/upload_test.go
+++ b/fakestorage/upload_test.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"crypto/tls"
 	"net/http"
+	"reflect"
 	"strings"
 	"testing"
 
@@ -53,8 +54,11 @@ func TestServerClientObjectWriter(t *testing.T) {
 				objHandle := client.Bucket(test.bucketName).Object(test.objectName)
 				w := objHandle.NewWriter(context.Background())
 				w.ChunkSize = test.chunkSize
-				w.Write([]byte(content))
 				w.ContentType = contentType
+				w.Metadata = map[string]string{
+					"foo": "bar",
+				}
+				w.Write([]byte(content))
 				err := w.Close()
 				if err != nil {
 					t.Fatal(err)
@@ -86,6 +90,9 @@ func TestServerClientObjectWriter(t *testing.T) {
 				if obj.ContentType != contentType {
 					t.Errorf("wrong content-type\nwant %q\ngot  %q", contentType, obj.ContentType)
 				}
+				if !reflect.DeepEqual(obj.Metadata, w.Metadata) {
+					t.Errorf("wrong meta data\nwant %+v\ngot  %+v", w.Metadata, obj.Metadata)
+				}
 			})
 		}
 	})