Repository: fsouza/fake-gcs-server
Pull request #1865 (2025-01-06 06:03:17)

Add disposition and language to ComposeObject


Patch:
diff --git a/fakestorage/object.go b/fakestorage/object.go
index 441c4ce..664751e 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -1262,7 +1262,7 @@ func (s *Server) composeObject(r *http.Request) jsonResponse {
 		sourceNames = append(sourceNames, n.Name)
 	}
 
-	backendObj, err := s.backend.ComposeObject(bucketName, sourceNames, destinationObject, composeRequest.Destination.Metadata, composeRequest.Destination.ContentType)
+	backendObj, err := s.backend.ComposeObject(bucketName, sourceNames, destinationObject, composeRequest.Destination.Metadata, composeRequest.Destination.ContentType, composeRequest.Destination.ContentDisposition, composeRequest.Destination.ContentLanguage)
 	if err != nil {
 		return jsonResponse{
 			status:       http.StatusInternalServerError,
diff --git a/internal/backend/fs.go b/internal/backend/fs.go
index 52cbcd1..b8f226d 100644
--- a/internal/backend/fs.go
+++ b/internal/backend/fs.go
@@ -437,7 +437,7 @@ func concatObjectReaders(objects []StreamingObject) io.ReadSeekCloser {
 	return concatenatedContent{io.MultiReader(readers...)}
 }
 
-func (s *storageFS) ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string) (StreamingObject, error) {
+func (s *storageFS) ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string, contentDisposition string, contentLanguage string) (StreamingObject, error) {
 	var sourceObjects []StreamingObject
 	for _, n := range objectNames {
 		obj, err := s.GetObject(bucketName, n)
@@ -451,11 +451,13 @@ func (s *storageFS) ComposeObject(bucketName string, objectNames []string, desti
 	now := time.Now().Format(timestampFormat)
 	dest := StreamingObject{
 		ObjectAttrs: ObjectAttrs{
-			BucketName:  bucketName,
-			Name:        destinationName,
-			ContentType: contentType,
-			Created:     now,
-			Updated:     now,
+			BucketName:         bucketName,
+			Name:               destinationName,
+			ContentType:        contentType,
+			ContentDisposition: contentDisposition,
+			ContentLanguage:    contentLanguage,
+			Created:            now,
+			Updated:            now,
 		},
 	}
 
diff --git a/internal/backend/memory.go b/internal/backend/memory.go
index f202ff3..17b6480 100644
--- a/internal/backend/memory.go
+++ b/internal/backend/memory.go
@@ -344,7 +344,7 @@ func (s *storageMemory) UpdateObject(bucketName, objectName string, attrsToUpdat
 	return obj, nil
 }
 
-func (s *storageMemory) ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string) (StreamingObject, error) {
+func (s *storageMemory) ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string, contentDisposition string, contentLanguage string) (StreamingObject, error) {
 	var data []byte
 	for _, n := range objectNames {
 		obj, err := s.GetObject(bucketName, n)
@@ -364,11 +364,13 @@ func (s *storageMemory) ComposeObject(bucketName string, objectNames []string, d
 		now := time.Now().Format(timestampFormat)
 		dest = Object{
 			ObjectAttrs: ObjectAttrs{
-				BucketName:  bucketName,
-				Name:        destinationName,
-				ContentType: contentType,
-				Created:     now,
-				Updated:     now,
+				BucketName:         bucketName,
+				Name:               destinationName,
+				ContentType:        contentType,
+				ContentDisposition: contentDisposition,
+				ContentLanguage:    contentLanguage,
+				Created:            now,
+				Updated:            now,
 			},
 		}
 	} else {
diff --git a/internal/backend/storage.go b/internal/backend/storage.go
index fd382ae..c428fcc 100644
--- a/internal/backend/storage.go
+++ b/internal/backend/storage.go
@@ -30,7 +30,7 @@ type Storage interface {
 	DeleteObject(bucketName, objectName string) error
 	PatchObject(bucketName, objectName string, attrsToUpdate ObjectAttrs) (StreamingObject, error)
 	UpdateObject(bucketName, objectName string, attrsToUpdate ObjectAttrs) (StreamingObject, error)
-	ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string) (StreamingObject, error)
+	ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string, contentDisposition string, contentLanguage string) (StreamingObject, error)
 	DeleteAllFiles() error
 }
 
diff --git a/internal/grpc/server.go b/internal/grpc/server.go
index 9283445..42df543 100644
--- a/internal/grpc/server.go
+++ b/internal/grpc/server.go
@@ -144,13 +144,13 @@ func (g *Server) PatchObject(ctx context.Context, req *pb.PatchObjectRequest) (*
 	return makeObject(obj), err
 }
 
-// ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string)
+// ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string, contentDisposition string, contentLanguage string)
 func (g *Server) ComposeObject(ctx context.Context, req *pb.ComposeObjectRequest) (*pb.Object, error) {
 	sourceObjNames := make([]string, 2)
 	for i := 0; i < len(req.SourceObjects); i++ {
 		sourceObjNames[i] = req.SourceObjects[i].Name
 	}
-	obj, err := g.backend.ComposeObject(req.DestinationBucket, sourceObjNames, req.DestinationObject, map[string]string{}, "")
+	obj, err := g.backend.ComposeObject(req.DestinationBucket, sourceObjNames, req.DestinationObject, map[string]string{}, "", "", "")
 	return makeObject(obj), err
 }
 


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index 59f7a8a..60fcd44 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -2123,10 +2123,12 @@ func TestParseRangeRequest(t *testing.T) {
 
 func TestServiceClientComposeObject(t *testing.T) {
 	const (
-		source1Content = "some content"
-		source2Content = "other content"
-		source3Content = "third test"
-		contentType    = "text/plain; charset=utf-8"
+		source1Content     = "some content"
+		source2Content     = "other content"
+		source3Content     = "third test"
+		contentDisposition = "attachment; filename=\"replaced.txt\""
+		contentLanguage    = "fr"
+		contentType        = "text/plain; charset=utf-8"
 	)
 	u32Checksum := uint32Checksum([]byte(source1Content))
 	hash := checksum.MD5Hash([]byte(source1Content))
@@ -2135,45 +2137,53 @@ func TestServiceClientComposeObject(t *testing.T) {
 	objs := []Object{
 		{
 			ObjectAttrs: ObjectAttrs{
-				BucketName:  "first-bucket",
-				Name:        "files/source1.txt",
-				ContentType: contentType,
-				Crc32c:      checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
-				Md5Hash:     checksum.EncodedHash(hash),
-				Metadata:    map[string]string{"foo": "bar"},
+				BucketName:         "first-bucket",
+				Name:               "files/source1.txt",
+				ContentDisposition: contentDisposition,
+				ContentLanguage:    contentLanguage,
+				ContentType:        contentType,
+				Crc32c:             checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
+				Md5Hash:            checksum.EncodedHash(hash),
+				Metadata:           map[string]string{"foo": "bar"},
 			},
 			Content: []byte(source1Content),
 		},
 		{
 			ObjectAttrs: ObjectAttrs{
-				BucketName:  "first-bucket",
-				Name:        "files/source2.txt",
-				ContentType: contentType,
-				Crc32c:      checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
-				Md5Hash:     checksum.EncodedHash(hash),
-				Metadata:    map[string]string{"foo": "bar"},
+				BucketName:         "first-bucket",
+				Name:               "files/source2.txt",
+				ContentDisposition: contentDisposition,
+				ContentLanguage:    contentLanguage,
+				ContentType:        contentType,
+				Crc32c:             checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
+				Md5Hash:            checksum.EncodedHash(hash),
+				Metadata:           map[string]string{"foo": "bar"},
 			},
 			Content: []byte(source2Content),
 		},
 		{
 			ObjectAttrs: ObjectAttrs{
-				BucketName:  "first-bucket",
-				Name:        "files/source3.txt",
-				ContentType: contentType,
-				Crc32c:      checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
-				Md5Hash:     checksum.EncodedHash(hash),
-				Metadata:    map[string]string{"foo": "bar"},
+				BucketName:         "first-bucket",
+				Name:               "files/source3.txt",
+				ContentDisposition: contentDisposition,
+				ContentLanguage:    contentLanguage,
+				ContentType:        contentType,
+				Crc32c:             checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
+				Md5Hash:            checksum.EncodedHash(hash),
+				Metadata:           map[string]string{"foo": "bar"},
 			},
 			Content: []byte(source3Content),
 		},
 		{
 			ObjectAttrs: ObjectAttrs{
-				BucketName:  "first-bucket",
-				Name:        "files/destination.txt",
-				ContentType: contentType,
-				Crc32c:      checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
-				Md5Hash:     checksum.EncodedHash(hash),
-				Metadata:    map[string]string{"foo": "bar"},
+				BucketName:         "first-bucket",
+				Name:               "files/destination.txt",
+				ContentDisposition: contentDisposition,
+				ContentLanguage:    contentLanguage,
+				ContentType:        contentType,
+				Crc32c:             checksum.EncodedChecksum(uint32ToBytes(u32Checksum)),
+				Md5Hash:            checksum.EncodedHash(hash),
+				Metadata:           map[string]string{"foo": "bar"},
 			},
 			Content: []byte("test"),
 		},
@@ -2236,6 +2246,8 @@ func TestServiceClientComposeObject(t *testing.T) {
 				dstObject := client.Bucket(test.bucketName).Object(test.destObjectName)
 				composer := dstObject.ComposerFrom(sourceObjects...)
 
+				composer.ContentDisposition = contentDisposition
+				composer.ContentLanguage = contentLanguage
 				composer.ContentType = contentType
 				composer.Metadata = map[string]string{"baz": "qux"}
 				attrs, err := composer.Run(context.TODO())
@@ -2261,6 +2273,12 @@ func TestServiceClientComposeObject(t *testing.T) {
 				if attrs.CRC32C != expectedChecksum {
 					t.Errorf("wrong checksum in compose object attrs\nwant %d\ngot  %d", u32Checksum, attrs.CRC32C)
 				}
+				if attrs.ContentDisposition != contentDisposition {
+					t.Errorf("wrong content disposition\nwant %q\ngot  %q", contentDisposition, attrs.ContentDisposition)
+				}
+				if attrs.ContentLanguage != contentLanguage {
+					t.Errorf("wrong content language\nwant %q\ngot  %q", contentLanguage, attrs.ContentLanguage)
+				}
 				if attrs.ContentType != contentType {
 					t.Errorf("wrong content type\nwant %q\ngot  %q", contentType, attrs.ContentType)
 				}
@@ -2286,6 +2304,12 @@ func TestServiceClientComposeObject(t *testing.T) {
 				if expect := checksum.EncodedHash(expectedHash); expect != obj.Md5Hash {
 					t.Errorf("wrong hash on object\nwant %s\ngot  %s", expect, obj.Md5Hash)
 				}
+				if obj.ContentDisposition != contentDisposition {
+					t.Errorf("wrong content disposition\nwant %q\ngot  %q", contentDisposition, obj.ContentDisposition)
+				}
+				if obj.ContentLanguage != contentLanguage {
+					t.Errorf("wrong content language\nwant %q\ngot  %q", contentLanguage, obj.ContentLanguage)
+				}
 				if obj.ContentType != contentType {
 					t.Errorf("wrong content type\nwant %q\ngot  %q", contentType, obj.ContentType)
 				}