From 33d9972e4f9c4e56a9bf41a722ae1f820ac8e356 Mon Sep 17 00:00:00 2001 From: Tatiana Nesterenko Date: Fri, 2 Feb 2024 12:55:06 +0000 Subject: [PATCH] api: fix `upload` to handle attributes `FilePath`, `FileName` When we receive headers, they are automatically formatted from any case to only the first letter being uppercase. For example, `FilePath` transforms into `Filepath`. However, NEOFS is case-sensitive for these attributes, which is why we intentionally change them to CamelCase style. We hope this is a temporary workaround and will be removed asap. Connected to https://github.com/nspcc-dev/neofs-http-gw/issues/255. Signed-off-by: Tatiana Nesterenko --- gen/restapi/embedded_spec.go | 24 +++++++++ .../upload_container_object_parameters.go | 50 +++++++++++++++++++ handlers/objects.go | 3 ++ handlers/util.go | 15 ++++++ handlers/util_test.go | 14 ++++-- spec/rest.yaml | 10 ++++ 6 files changed, 113 insertions(+), 3 deletions(-) diff --git a/gen/restapi/embedded_spec.go b/gen/restapi/embedded_spec.go index 08c6037..7cf986f 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -1008,6 +1008,18 @@ func init() { "description": "The file to upload. If no file is present in this field, any other field name will be accepted, except for an empty one.", "name": "payload", "in": "formData" + }, + { + "type": "string", + "description": "This attribute, in any combination of upper/lower case, will be added to the object as the ` + "`" + `FileName` + "`" + ` attribute. It will also be returned as the ` + "`" + `FileName` + "`" + ` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) and the ` + "`" + `name` + "`" + ` in POST call search in a container (/objects/{containerId}/search).", + "name": "X-Attribute-Filename", + "in": "header" + }, + { + "type": "string", + "description": "This attribute, in any combination of upper/lower case, will be added to the object as the ` + "`" + `FilePath` + "`" + ` attribute. It will also be returned as the ` + "`" + `FilePath` + "`" + ` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) or the ` + "`" + `filePath` + "`" + ` in POST call search in a container (/objects/{containerId}/search).", + "name": "X-Attribute-Filepath", + "in": "header" } ], "responses": { @@ -3312,6 +3324,18 @@ func init() { "description": "The file to upload. If no file is present in this field, any other field name will be accepted, except for an empty one.", "name": "payload", "in": "formData" + }, + { + "type": "string", + "description": "This attribute, in any combination of upper/lower case, will be added to the object as the ` + "`" + `FileName` + "`" + ` attribute. It will also be returned as the ` + "`" + `FileName` + "`" + ` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) and the ` + "`" + `name` + "`" + ` in POST call search in a container (/objects/{containerId}/search).", + "name": "X-Attribute-Filename", + "in": "header" + }, + { + "type": "string", + "description": "This attribute, in any combination of upper/lower case, will be added to the object as the ` + "`" + `FilePath` + "`" + ` attribute. It will also be returned as the ` + "`" + `FilePath` + "`" + ` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) or the ` + "`" + `filePath` + "`" + ` in POST call search in a container (/objects/{containerId}/search).", + "name": "X-Attribute-Filepath", + "in": "header" } ], "responses": { diff --git a/gen/restapi/operations/upload_container_object_parameters.go b/gen/restapi/operations/upload_container_object_parameters.go index 36a38dc..86e4e6a 100644 --- a/gen/restapi/operations/upload_container_object_parameters.go +++ b/gen/restapi/operations/upload_container_object_parameters.go @@ -40,6 +40,14 @@ type UploadContainerObjectParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /*This attribute, in any combination of upper/lower case, will be added to the object as the `FileName` attribute. It will also be returned as the `FileName` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) and the `name` in POST call search in a container (/objects/{containerId}/search). + In: header + */ + XAttributeFilename *string + /*This attribute, in any combination of upper/lower case, will be added to the object as the `FilePath` attribute. It will also be returned as the `FilePath` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) or the `filePath` in POST call search in a container (/objects/{containerId}/search). + In: header + */ + XAttributeFilepath *string /*Base58 encoded container id. Required: true In: path @@ -68,6 +76,14 @@ func (o *UploadContainerObjectParams) BindRequest(r *http.Request, route *middle } } + if err := o.bindXAttributeFilename(r.Header[http.CanonicalHeaderKey("X-Attribute-Filename")], true, route.Formats); err != nil { + res = append(res, err) + } + + if err := o.bindXAttributeFilepath(r.Header[http.CanonicalHeaderKey("X-Attribute-Filepath")], true, route.Formats); err != nil { + res = append(res, err) + } + rContainerID, rhkContainerID, _ := route.Params.GetOK("containerId") if err := o.bindContainerID(rContainerID, rhkContainerID, route.Formats); err != nil { res = append(res, err) @@ -89,6 +105,40 @@ func (o *UploadContainerObjectParams) BindRequest(r *http.Request, route *middle return nil } +// bindXAttributeFilename binds and validates parameter XAttributeFilename from header. +func (o *UploadContainerObjectParams) bindXAttributeFilename(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.XAttributeFilename = &raw + + return nil +} + +// bindXAttributeFilepath binds and validates parameter XAttributeFilepath from header. +func (o *UploadContainerObjectParams) bindXAttributeFilepath(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.XAttributeFilepath = &raw + + return nil +} + // bindContainerID binds and validates parameter ContainerID from path. func (o *UploadContainerObjectParams) bindContainerID(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/handlers/objects.go b/handlers/objects.go index 72c50ad..133c861 100644 --- a/handlers/objects.go +++ b/handlers/objects.go @@ -42,6 +42,9 @@ const ( attributeFilePath = "FilePath" sizeToDetectType = 512 userAttributeHeaderPrefix = "X-Attribute-" + + attributeFilepathHTTP = "Filepath" + attributeFilenameHTTP = "Filename" ) type readCloser struct { diff --git a/handlers/util.go b/handlers/util.go index e5f6757..e45d992 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -291,6 +291,7 @@ func filterHeaders(l *zap.Logger, header http.Header) (map[string]string, error) continue } + clearKey = formatSpecialAttribute(clearKey) result[clearKey] = value l.Debug("add attribute to result object", @@ -299,3 +300,17 @@ func filterHeaders(l *zap.Logger, header http.Header) (map[string]string, error) } return result, nil } + +// formatSpecialAttribute checks if a key-string is one of the special NEOFS +// attributes and returns the string in the correct case. +// For example: "Filepath" -> "FilePath". +func formatSpecialAttribute(s string) string { + switch s { + case attributeFilepathHTTP: + return attributeFilePath + case attributeFilenameHTTP: + return object.AttributeFileName + default: + return s + } +} diff --git a/handlers/util_test.go b/handlers/util_test.go index acd9560..1b693f8 100644 --- a/handlers/util_test.go +++ b/handlers/util_test.go @@ -191,16 +191,24 @@ func TestFilter(t *testing.T) { req.Set("X-Attribute-Neofs-Expiration-Epoch1", "101") req.Set("X-Attribute-NEOFS-Expiration-Epoch2", "102") req.Set("X-Attribute-neofs-Expiration-Epoch3", "103") + req.Set("X-Attribute-FileName", "FileName") // This one will be overridden. + req.Set("X-Attribute-filename", "filename") + req.Set("X-Attribute-fIlePaTh", "fIlePaTh/") // This one will be overridden. + req.Set("X-Attribute-Filepath", "Filepath/") + req.Set("X-Attribute-FilePath1", "FilePath/1") req.Set("X-Attribute-My-Attribute", "value") req.Set("X-Attribute-MyAttribute", "value2") - req.Set("X-Attribute-Empty-Value", "") - req.Set("X-Attribute-", "prefix only") - req.Set("No-Prefix", "value") + req.Set("X-Attribute-Empty-Value", "") // This one will be skipped. + req.Set("X-Attribute-", "prefix only") // This one will be skipped. + req.Set("No-Prefix", "value") // This one will be skipped. expected := map[string]string{ "__NEOFS__EXPIRATION_EPOCH1": "101", "__NEOFS__EXPIRATION_EPOCH2": "102", "__NEOFS__EXPIRATION_EPOCH3": "103", + "FileName": "filename", + "FilePath": "Filepath/", + "Filepath1": "FilePath/1", "My-Attribute": "value", "Myattribute": "value2", } diff --git a/spec/rest.yaml b/spec/rest.yaml index e80ea22..8c7d23b 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -644,6 +644,16 @@ paths: required: false type: file description: The file to upload. If no file is present in this field, any other field name will be accepted, except for an empty one. + - name: X-Attribute-Filename + in: header + required: false + type: string + description: This attribute, in any combination of upper/lower case, will be added to the object as the `FileName` attribute. It will also be returned as the `FileName` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) and the `name` in POST call search in a container (/objects/{containerId}/search). + - name: X-Attribute-Filepath + in: header + required: false + type: string + description: This attribute, in any combination of upper/lower case, will be added to the object as the `FilePath` attribute. It will also be returned as the `FilePath` attribute in GET/HEAD API calls for the object (/get/{containerId}/{objectId}) or the `filePath` in POST call search in a container (/objects/{containerId}/search). responses: 200: headers: