diff --git a/services/s3/handler.go b/services/s3/handler.go index 83c3576..c4339f1 100644 --- a/services/s3/handler.go +++ b/services/s3/handler.go @@ -74,6 +74,7 @@ func NewHandler(logger *slog.Logger, s3 *S3) func(w http.ResponseWriter, r *http ServerSideEncryption: r.Form.Get("x-amz-server-side-encryption"), ContentType: r.Form.Get("Content-Type"), Data: f, + Metadata: extractMetadata(r.Header), } logger.Debug("Parsed input", "method", "PutObject", "input", input) output, awserr := s3.PutObject(input) @@ -128,7 +129,11 @@ func NewHandler(logger *slog.Logger, s3 *S3) func(w http.ResponseWriter, r *http if r.Header.Get("x-amz-copy-source") != "" { handle(w, r, logger.With("method", "CopyObject"), s3.CopyObject) } else { - handle(w, r, logger.With("method", "PutObject"), s3.PutObject) + handle(w, r, logger.With("method", "PutObject"), + func(input PutObjectInput) (*PutObjectOutput, *awserrors.Error) { + input.Metadata = extractMetadata(r.Header) + return s3.PutObject(input) + }) } case http.MethodDelete: handle(w, r, logger.With("method", "DeleteObject"), s3.DeleteObject) @@ -140,6 +145,16 @@ func NewHandler(logger *slog.Logger, s3 *S3) func(w http.ResponseWriter, r *http } } +func extractMetadata(header http.Header) map[string]string { + metadata := make(map[string]string) + for k := range header { + if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") { + metadata[k] = header.Get(k) + } + } + return metadata +} + func handle[Input any, Output any]( w http.ResponseWriter, r *http.Request, @@ -238,13 +253,18 @@ func marshal(w http.ResponseWriter, output any, awserr *awserrors.Error) { v := reflect.ValueOf(output).Elem() ty := v.Type() for i := 0; i < ty.NumField(); i++ { - tag := ty.Field(i).Tag.Get("s3") + field := ty.Field(i) + tag := field.Tag.Get("s3") if tag == "body" { reflect.ValueOf(&body).Elem().Set(v.Field(i)) } else if tag == "http-status" { httpStatus = int(v.Field(i).Int()) + } else if tag == "metadata-headers" { + for _, mapKey := range v.Field(i).MapKeys() { + mapValue := v.Field(i).MapIndex(mapKey) + w.Header().Set(mapKey.String(), mapValue.String()) + } } else if h, ok := strings.CutPrefix(tag, "header:"); ok { - field := ty.Field(i) switch field.Type.Kind() { case reflect.Int, reflect.Int64: w.Header().Set(h, strconv.Itoa(int(v.Field(i).Int()))) diff --git a/services/s3/s3.go b/services/s3/s3.go index 5833f9e..207b018 100644 --- a/services/s3/s3.go +++ b/services/s3/s3.go @@ -30,6 +30,8 @@ type Object struct { MD5 []byte ETag string + Metadata map[string]string + ContentType string ContentLength int64 Parts []Part @@ -392,6 +394,7 @@ func (s *S3) getObject(input GetObjectInput, includeBody bool) (*GetObjectOutput SSECustomerAlgorithm: object.SSECustomerAlgorithm, SSECustomerKey: object.SSECustomerKey, SSEKMSKeyId: object.SSEKMSKeyId, + Metadata: object.Metadata, // Bafflingly, This format is expected here. LastModified: time.Now().UTC().Format(timeFormat), HttpStatus: http.StatusOK, @@ -482,6 +485,7 @@ func (s *S3) PutObject(input PutObjectInput) (*PutObjectOutput, *awserrors.Error ContentType: input.ContentType, ContentLength: contentLength, + Metadata: input.Metadata, Tagging: input.Tagging, ServerSideEncryption: input.ServerSideEncryption, SSEKMSKeyId: input.SSEKMSKeyId, diff --git a/services/s3/types.go b/services/s3/types.go index fbbe17a..9777d32 100644 --- a/services/s3/types.go +++ b/services/s3/types.go @@ -118,6 +118,8 @@ type GetObjectOutput struct { SSECustomerAlgorithm string `s3:"header:x-amz-server-side-encryption-customer-algorithm"` SSECustomerKey string `s3:"header:x-amz-server-side-encryption-customer-key"` LastModified string `s3:"header:Last-Modified"` + // Note: Metadata is handled specially + Metadata map[string]string `s3:"metadata-headers"` // TODO: md5 SSEKMSKeyId string `s3:"header:x-amz-server-side-encryption-aws-kms-key-id"` //PartsCount int `s3:"header:x-amz-mp-parts-count"` @@ -135,6 +137,8 @@ type PutObjectInput struct { SSEKMSKeyId string `s3:"header:x-amz-server-side-encryption-aws-kms-key-id"` SSEKMSEncryptionContext string `s3:"header:x-amz-server-side-encryption-context"` SSECustomerAlgorithm string `s3:"header:x-amz-server-side-encryption-customer-algorithm"` + // Note: Metadata is handled specially + Metadata map[string]string // TODO: md5 check SSECustomerKey string `s3:"header:x-amz-server-side-encryption-customer-key"` Tagging string `s3:"header:x-amz-tagging"`