diff --git a/cmd/neofs-rest-gw/integration_test.go b/cmd/neofs-rest-gw/integration_test.go index 230c5e3..1d4e33b 100644 --- a/cmd/neofs-rest-gw/integration_test.go +++ b/cmd/neofs-rest-gw/integration_test.go @@ -227,13 +227,17 @@ func restObjectPut(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnr attrKey: attrValue, } - req := operations.PutObjectBody{ + req := &models.ObjectUpload{ ContainerID: handlers.NewString(cnrID.String()), FileName: handlers.NewString("newFile.txt"), Payload: base64.StdEncoding.EncodeToString([]byte(content)), + Attributes: []*models.Attribute{{ + Key: &attrKey, + Value: &attrValue, + }}, } - body, err := json.Marshal(&req) + body, err := json.Marshal(req) require.NoError(t, err) query := make(url.Values) @@ -242,7 +246,6 @@ func restObjectPut(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnr request, err := http.NewRequest(http.MethodPut, testHost+"/v1/objects?"+query.Encode(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) - request.Header.Add("X-Attribute-"+attrKey, attrValue) addr := &models.Address{} doRequest(t, httpClient, request, http.StatusOK, addr) diff --git a/gen/models/object_upload.go b/gen/models/object_upload.go new file mode 100644 index 0000000..dbd67c4 --- /dev/null +++ b/gen/models/object_upload.go @@ -0,0 +1,155 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ObjectUpload object upload +// Example: {"attributes":[{"key":"User-Attribute","value":"some-value"}],"containerId":"5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv","fileName":"myFile.txt","payload":"Y29udGVudCBvZiBmaWxl"} +// +// swagger:model ObjectUpload +type ObjectUpload struct { + + // attributes + Attributes []*Attribute `json:"attributes"` + + // container Id + // Required: true + ContainerID *string `json:"containerId"` + + // file name + // Required: true + FileName *string `json:"fileName"` + + // payload + Payload string `json:"payload,omitempty"` +} + +// Validate validates this object upload +func (m *ObjectUpload) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAttributes(formats); err != nil { + res = append(res, err) + } + + if err := m.validateContainerID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateFileName(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ObjectUpload) validateAttributes(formats strfmt.Registry) error { + if swag.IsZero(m.Attributes) { // not required + return nil + } + + for i := 0; i < len(m.Attributes); i++ { + if swag.IsZero(m.Attributes[i]) { // not required + continue + } + + if m.Attributes[i] != nil { + if err := m.Attributes[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("attributes" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("attributes" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +func (m *ObjectUpload) validateContainerID(formats strfmt.Registry) error { + + if err := validate.Required("containerId", "body", m.ContainerID); err != nil { + return err + } + + return nil +} + +func (m *ObjectUpload) validateFileName(formats strfmt.Registry) error { + + if err := validate.Required("fileName", "body", m.FileName); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this object upload based on the context it is used +func (m *ObjectUpload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAttributes(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ObjectUpload) contextValidateAttributes(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Attributes); i++ { + + if m.Attributes[i] != nil { + if err := m.Attributes[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("attributes" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("attributes" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ObjectUpload) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ObjectUpload) UnmarshalBinary(b []byte) error { + var res ObjectUpload + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/restapi/embedded_spec.go b/gen/restapi/embedded_spec.go index cf3a9d2..40c9f1d 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -340,27 +340,7 @@ func init() { "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "containerId", - "fileName" - ], - "properties": { - "containerId": { - "type": "string" - }, - "fileName": { - "type": "string" - }, - "payload": { - "type": "string" - } - }, - "example": { - "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", - "fileName": "myFile.txt", - "payload": "Y29udGVudCBvZiBmaWxl" - } + "$ref": "#/definitions/ObjectUpload" } } ], @@ -817,6 +797,41 @@ func init() { } } }, + "ObjectUpload": { + "type": "object", + "required": [ + "containerId", + "fileName" + ], + "properties": { + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Attribute" + } + }, + "containerId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "payload": { + "type": "string" + } + }, + "example": { + "attributes": [ + { + "key": "User-Attribute", + "value": "some-value" + } + ], + "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", + "fileName": "myFile.txt", + "payload": "Y29udGVudCBvZiBmaWxl" + } + }, "Operation": { "type": "string", "enum": [ @@ -1424,27 +1439,7 @@ func init() { "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "containerId", - "fileName" - ], - "properties": { - "containerId": { - "type": "string" - }, - "fileName": { - "type": "string" - }, - "payload": { - "type": "string" - } - }, - "example": { - "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", - "fileName": "myFile.txt", - "payload": "Y29udGVudCBvZiBmaWxl" - } + "$ref": "#/definitions/ObjectUpload" } } ], @@ -1952,6 +1947,41 @@ func init() { } } }, + "ObjectUpload": { + "type": "object", + "required": [ + "containerId", + "fileName" + ], + "properties": { + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Attribute" + } + }, + "containerId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "payload": { + "type": "string" + } + }, + "example": { + "attributes": [ + { + "key": "User-Attribute", + "value": "some-value" + } + ], + "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", + "fileName": "myFile.txt", + "payload": "Y29udGVudCBvZiBmaWxl" + } + }, "Operation": { "type": "string", "enum": [ diff --git a/gen/restapi/operations/put_object.go b/gen/restapi/operations/put_object.go index 0f72c84..bf4098d 100644 --- a/gen/restapi/operations/put_object.go +++ b/gen/restapi/operations/put_object.go @@ -6,14 +6,9 @@ package operations // Editing this file might prove futile when you re-run the generate command import ( - "context" "net/http" - "github.com/go-openapi/errors" "github.com/go-openapi/runtime/middleware" - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag" - "github.com/go-openapi/validate" "github.com/nspcc-dev/neofs-rest-gw/gen/models" ) @@ -74,80 +69,3 @@ func (o *PutObject) ServeHTTP(rw http.ResponseWriter, r *http.Request) { o.Context.Respond(rw, r, route.Produces, route, res) } - -// PutObjectBody put object body -// Example: {"containerId":"5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv","fileName":"myFile.txt","payload":"Y29udGVudCBvZiBmaWxl"} -// -// swagger:model PutObjectBody -type PutObjectBody struct { - - // container Id - // Required: true - ContainerID *string `json:"containerId"` - - // file name - // Required: true - FileName *string `json:"fileName"` - - // payload - Payload string `json:"payload,omitempty"` -} - -// Validate validates this put object body -func (o *PutObjectBody) Validate(formats strfmt.Registry) error { - var res []error - - if err := o.validateContainerID(formats); err != nil { - res = append(res, err) - } - - if err := o.validateFileName(formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (o *PutObjectBody) validateContainerID(formats strfmt.Registry) error { - - if err := validate.Required("object"+"."+"containerId", "body", o.ContainerID); err != nil { - return err - } - - return nil -} - -func (o *PutObjectBody) validateFileName(formats strfmt.Registry) error { - - if err := validate.Required("object"+"."+"fileName", "body", o.FileName); err != nil { - return err - } - - return nil -} - -// ContextValidate validates this put object body based on context it is used -func (o *PutObjectBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (o *PutObjectBody) MarshalBinary() ([]byte, error) { - if o == nil { - return nil, nil - } - return swag.WriteJSON(o) -} - -// UnmarshalBinary interface implementation -func (o *PutObjectBody) UnmarshalBinary(b []byte) error { - var res PutObjectBody - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *o = res - return nil -} diff --git a/gen/restapi/operations/put_object_parameters.go b/gen/restapi/operations/put_object_parameters.go index 7880d5b..80eb093 100644 --- a/gen/restapi/operations/put_object_parameters.go +++ b/gen/restapi/operations/put_object_parameters.go @@ -16,6 +16,8 @@ import ( "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" + + "github.com/nspcc-dev/neofs-rest-gw/gen/models" ) // NewPutObjectParams creates a new PutObjectParams object @@ -56,7 +58,7 @@ type PutObjectParams struct { Required: true In: body */ - Object PutObjectBody + Object *models.ObjectUpload /*Use wallect connect signature scheme or not In: query Default: false @@ -85,7 +87,7 @@ func (o *PutObjectParams) BindRequest(r *http.Request, route *middleware.Matched if runtime.HasBody(r) { defer r.Body.Close() - var body PutObjectBody + var body models.ObjectUpload if err := route.Consumer.Consume(r.Body, &body); err != nil { if err == io.EOF { res = append(res, errors.Required("object", "body", "")) @@ -104,7 +106,7 @@ func (o *PutObjectParams) BindRequest(r *http.Request, route *middleware.Matched } if len(res) == 0 { - o.Object = body + o.Object = &body } } } else { diff --git a/handlers/objects.go b/handlers/objects.go index f991ab2..66499ad 100644 --- a/handlers/objects.go +++ b/handlers/objects.go @@ -5,6 +5,9 @@ import ( "crypto/ecdsa" "encoding/base64" "fmt" + "io" + "strings" + "github.com/go-openapi/runtime/middleware" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-api-go/v2/acl" @@ -19,8 +22,6 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/pool" "github.com/nspcc-dev/neofs-sdk-go/token" "go.uber.org/zap" - "io" - "strings" ) // PutObjects handler that uploads object to NeoFS. @@ -48,7 +49,7 @@ func (a *API) PutObjects(params operations.PutObjectParams, principal *models.Pr DefaultTimestamp: a.defaultTimestamp, DefaultFileName: *params.Object.FileName, } - attributes, err := GetObjectAttributes(ctx, params.HTTPRequest.Header, a.pool, prm) + attributes, err := GetObjectAttributes(ctx, a.pool, params.Object.Attributes, prm) if err != nil { return errorResponse.WithPayload(models.Error(err.Error())) } diff --git a/handlers/util.go b/handlers/util.go index dc4bede..36a4753 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -87,35 +87,38 @@ func filterHeaders(header http.Header) map[string]string { } // GetObjectAttributes forms object attributes from request headers. -func GetObjectAttributes(ctx context.Context, header http.Header, pool *pool.Pool, prm PrmAttributes) ([]object.Attribute, error) { - filtered := filterHeaders(header) - if needParseExpiration(filtered) { +func GetObjectAttributes(ctx context.Context, pool *pool.Pool, attrs []*models.Attribute, prm PrmAttributes) ([]object.Attribute, error) { + headers := make(map[string]string, len(attrs)) + + for _, attr := range attrs { + headers[*attr.Key] = *attr.Value + } + delete(headers, object.AttributeFileName) + + if needParseExpiration(headers) { epochDuration, err := getEpochDurations(ctx, pool) if err != nil { return nil, fmt.Errorf("could not get epoch durations from network info: %w", err) } - if err = prepareExpirationHeader(filtered, epochDuration); err != nil { + if err = prepareExpirationHeader(headers, epochDuration); err != nil { return nil, fmt.Errorf("could not prepare expiration header: %w", err) } } - attributes := make([]object.Attribute, 0, len(filtered)) - // prepares attributes from filtered headers - for key, val := range filtered { + attributes := make([]object.Attribute, 0, len(headers)) + for key, val := range headers { attribute := object.NewAttribute() attribute.SetKey(key) attribute.SetValue(val) attributes = append(attributes, *attribute) } - // sets FileName attribute if it wasn't set from header - if _, ok := filtered[object.AttributeFileName]; !ok && prm.DefaultFileName != "" { - filename := object.NewAttribute() - filename.SetKey(object.AttributeFileName) - filename.SetValue(prm.DefaultFileName) - attributes = append(attributes, *filename) - } - // sets Timestamp attribute if it wasn't set from header and enabled by settings - if _, ok := filtered[object.AttributeTimestamp]; !ok && prm.DefaultTimestamp { + + filename := object.NewAttribute() + filename.SetKey(object.AttributeFileName) + filename.SetValue(prm.DefaultFileName) + attributes = append(attributes, *filename) + + if _, ok := headers[object.AttributeTimestamp]; !ok && prm.DefaultTimestamp { timestamp := object.NewAttribute() timestamp.SetKey(object.AttributeTimestamp) timestamp.SetValue(strconv.FormatInt(time.Now().Unix(), 10)) diff --git a/spec/rest.yaml b/spec/rest.yaml index 4658933..d2a8657 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -107,21 +107,7 @@ paths: name: object description: Object info to upload schema: - type: object - properties: - containerId: - type: string - fileName: - type: string - payload: - type: string - required: - - containerId - - fileName - example: - containerId: 5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv - fileName: myFile.txt - payload: Y29udGVudCBvZiBmaWxl + $ref: '#/definitions/ObjectUpload' consumes: - application/json produces: @@ -618,6 +604,29 @@ definitions: type: string required: - address + ObjectUpload: + type: object + properties: + containerId: + type: string + fileName: + type: string + payload: + type: string + attributes: + type: array + items: + $ref: '#/definitions/Attribute' + required: + - containerId + - fileName + example: + containerId: 5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv + fileName: myFile.txt + payload: Y29udGVudCBvZiBmaWxl + attributes: + - key: User-Attribute + value: some-value ObjectInfo: type: object properties: