diff --git a/cmd/neofs-rest-gw/integration_test.go b/cmd/neofs-rest-gw/integration_test.go index 6533a97..5dea114 100644 --- a/cmd/neofs-rest-gw/integration_test.go +++ b/cmd/neofs-rest-gw/integration_test.go @@ -117,10 +117,11 @@ func runTests(ctx context.Context, t *testing.T, key *keys.PrivateKey, version s t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, cnrID) }) t.Run("rest get object "+version, func(t *testing.T) { restObjectGet(ctx, t, clientPool, cnrID) }) t.Run("rest delete object "+version, func(t *testing.T) { restObjectDelete(ctx, t, clientPool, cnrID) }) + t.Run("rest search objects "+version, func(t *testing.T) { restObjectsSearch(ctx, t, clientPool, cnrID) }) t.Run("rest put container "+version, func(t *testing.T) { restContainerPut(ctx, t, clientPool) }) t.Run("rest get container "+version, func(t *testing.T) { restContainerGet(ctx, t, clientPool, cnrID) }) - //t.Run("rest delete container "+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) }) + t.Run("rest delete container "+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) }) t.Run("rest put container eacl "+version, func(t *testing.T) { restContainerEACLPut(ctx, t, clientPool) }) t.Run("rest get container eacl "+version, func(t *testing.T) { restContainerEACLGet(ctx, t, clientPool, cnrID) }) t.Run("rest list containers "+version, func(t *testing.T) { restContainerList(ctx, t, clientPool, cnrID) }) @@ -243,7 +244,7 @@ func restObjectPut(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnr prepareCommonHeaders(request.Header, bearerToken) request.Header.Add("X-Attribute-"+attrKey, attrValue) - addr := &operations.PutObjectOKBody{} + addr := &models.Address{} doRequest(t, httpClient, request, http.StatusOK, addr) var CID cid.ID @@ -352,6 +353,75 @@ func restObjectDelete(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *ci require.Error(t, err) } +func restObjectsSearch(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID) { + userKey, userValue := "User-Attribute", "user-attribute-value" + objectName := "object-name" + headers := map[string]string{ + object.AttributeFileName: objectName, + userKey: userValue, + } + objID := createObject(ctx, t, p, cnrID, headers, []byte("some content")) + headers[userKey] = "dummy" + _ = createObject(ctx, t, p, cnrID, headers, []byte("some content")) + + bearer := &models.Bearer{ + Object: []*models.Record{ + { + Operation: models.NewOperation(models.OperationSEARCH), + Action: models.NewAction(models.ActionALLOW), + Filters: []*models.Filter{}, + Targets: []*models.Target{{Role: models.NewRole(models.RoleOTHERS), Keys: []string{}}}, + }, + { + Operation: models.NewOperation(models.OperationHEAD), + Action: models.NewAction(models.ActionALLOW), + Filters: []*models.Filter{}, + Targets: []*models.Target{{Role: models.NewRole(models.RoleOTHERS), Keys: []string{}}}, + }, + { + Operation: models.NewOperation(models.OperationGET), + Action: models.NewAction(models.ActionALLOW), + Filters: []*models.Filter{}, + Targets: []*models.Target{{Role: models.NewRole(models.RoleOTHERS), Keys: []string{}}}, + }, + }, + } + + httpClient := defaultHTTPClient() + bearerToken := makeAuthObjectTokenRequest(ctx, t, bearer, httpClient) + + search := &models.SearchFilters{ + Filters: []*models.SearchFilter{ + { + Key: handlers.NewString(userKey), + Match: models.NewSearchMatch(models.SearchMatchMatchStringEqual), + Value: handlers.NewString(userValue), + }, + }, + } + + body, err := json.Marshal(search) + require.NoError(t, err) + + query := make(url.Values) + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) + + request, err := http.NewRequest(http.MethodPost, testHost+"/v1/objects/"+cnrID.String()+"/search?"+query.Encode(), bytes.NewReader(body)) + require.NoError(t, err) + prepareCommonHeaders(request.Header, bearerToken) + + resp := &models.ObjectList{} + doRequest(t, httpClient, request, http.StatusOK, resp) + + require.Equal(t, 1, int(*resp.Size)) + require.Len(t, resp.Objects, 1) + + objBaseInfo := resp.Objects[0] + require.Equal(t, cnrID.String(), *objBaseInfo.Address.ContainerID) + require.Equal(t, objID.String(), *objBaseInfo.Address.ObjectID) + require.Equal(t, objectName, objBaseInfo.Name) +} + func doRequest(t *testing.T, httpClient *http.Client, request *http.Request, expectedCode int, model interface{}) { resp, err := httpClient.Do(request) require.NoError(t, err) diff --git a/gen/models/address.go b/gen/models/address.go new file mode 100644 index 0000000..d20c544 --- /dev/null +++ b/gen/models/address.go @@ -0,0 +1,89 @@ +// 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" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// Address address +// Example: {"containerId":"5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv","objectId":"8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd"} +// +// swagger:model Address +type Address struct { + + // container Id + // Required: true + ContainerID *string `json:"containerId"` + + // object Id + // Required: true + ObjectID *string `json:"objectId"` +} + +// Validate validates this address +func (m *Address) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContainerID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateObjectID(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Address) validateContainerID(formats strfmt.Registry) error { + + if err := validate.Required("containerId", "body", m.ContainerID); err != nil { + return err + } + + return nil +} + +func (m *Address) validateObjectID(formats strfmt.Registry) error { + + if err := validate.Required("objectId", "body", m.ObjectID); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this address based on context it is used +func (m *Address) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *Address) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Address) UnmarshalBinary(b []byte) error { + var res Address + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/models/object_base_info.go b/gen/models/object_base_info.go new file mode 100644 index 0000000..d0873e7 --- /dev/null +++ b/gen/models/object_base_info.go @@ -0,0 +1,110 @@ +// 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" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ObjectBaseInfo object base info +// +// swagger:model ObjectBaseInfo +type ObjectBaseInfo struct { + + // address + // Required: true + Address *Address `json:"address"` + + // name + Name string `json:"name,omitempty"` +} + +// Validate validates this object base info +func (m *ObjectBaseInfo) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAddress(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ObjectBaseInfo) validateAddress(formats strfmt.Registry) error { + + if err := validate.Required("address", "body", m.Address); err != nil { + return err + } + + if m.Address != nil { + if err := m.Address.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("address") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("address") + } + return err + } + } + + return nil +} + +// ContextValidate validate this object base info based on the context it is used +func (m *ObjectBaseInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAddress(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ObjectBaseInfo) contextValidateAddress(ctx context.Context, formats strfmt.Registry) error { + + if m.Address != nil { + if err := m.Address.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("address") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("address") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ObjectBaseInfo) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ObjectBaseInfo) UnmarshalBinary(b []byte) error { + var res ObjectBaseInfo + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/models/object_list.go b/gen/models/object_list.go new file mode 100644 index 0000000..fbfc3df --- /dev/null +++ b/gen/models/object_list.go @@ -0,0 +1,136 @@ +// 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" +) + +// ObjectList object list +// +// swagger:model ObjectList +type ObjectList struct { + + // objects + // Required: true + Objects []*ObjectBaseInfo `json:"objects"` + + // size + // Required: true + Size *int64 `json:"size"` +} + +// Validate validates this object list +func (m *ObjectList) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateObjects(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSize(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ObjectList) validateObjects(formats strfmt.Registry) error { + + if err := validate.Required("objects", "body", m.Objects); err != nil { + return err + } + + for i := 0; i < len(m.Objects); i++ { + if swag.IsZero(m.Objects[i]) { // not required + continue + } + + if m.Objects[i] != nil { + if err := m.Objects[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("objects" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("objects" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +func (m *ObjectList) validateSize(formats strfmt.Registry) error { + + if err := validate.Required("size", "body", m.Size); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this object list based on the context it is used +func (m *ObjectList) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateObjects(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ObjectList) contextValidateObjects(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Objects); i++ { + + if m.Objects[i] != nil { + if err := m.Objects[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("objects" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("objects" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ObjectList) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ObjectList) UnmarshalBinary(b []byte) error { + var res ObjectList + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/models/search_filter.go b/gen/models/search_filter.go new file mode 100644 index 0000000..3230bf5 --- /dev/null +++ b/gen/models/search_filter.go @@ -0,0 +1,145 @@ +// 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" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// SearchFilter search filter +// +// swagger:model SearchFilter +type SearchFilter struct { + + // key + // Required: true + Key *string `json:"key"` + + // match + // Required: true + Match *SearchMatch `json:"match"` + + // value + // Required: true + Value *string `json:"value"` +} + +// Validate validates this search filter +func (m *SearchFilter) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateKey(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMatch(formats); err != nil { + res = append(res, err) + } + + if err := m.validateValue(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SearchFilter) validateKey(formats strfmt.Registry) error { + + if err := validate.Required("key", "body", m.Key); err != nil { + return err + } + + return nil +} + +func (m *SearchFilter) validateMatch(formats strfmt.Registry) error { + + if err := validate.Required("match", "body", m.Match); err != nil { + return err + } + + if err := validate.Required("match", "body", m.Match); err != nil { + return err + } + + if m.Match != nil { + if err := m.Match.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("match") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("match") + } + return err + } + } + + return nil +} + +func (m *SearchFilter) validateValue(formats strfmt.Registry) error { + + if err := validate.Required("value", "body", m.Value); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this search filter based on the context it is used +func (m *SearchFilter) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateMatch(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SearchFilter) contextValidateMatch(ctx context.Context, formats strfmt.Registry) error { + + if m.Match != nil { + if err := m.Match.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("match") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("match") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *SearchFilter) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SearchFilter) UnmarshalBinary(b []byte) error { + var res SearchFilter + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/models/search_filters.go b/gen/models/search_filters.go new file mode 100644 index 0000000..2d71fcd --- /dev/null +++ b/gen/models/search_filters.go @@ -0,0 +1,119 @@ +// 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" +) + +// SearchFilters search filters +// +// swagger:model SearchFilters +type SearchFilters struct { + + // filters + // Required: true + Filters []*SearchFilter `json:"filters"` +} + +// Validate validates this search filters +func (m *SearchFilters) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateFilters(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SearchFilters) validateFilters(formats strfmt.Registry) error { + + if err := validate.Required("filters", "body", m.Filters); err != nil { + return err + } + + for i := 0; i < len(m.Filters); i++ { + if swag.IsZero(m.Filters[i]) { // not required + continue + } + + if m.Filters[i] != nil { + if err := m.Filters[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("filters" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("filters" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this search filters based on the context it is used +func (m *SearchFilters) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateFilters(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SearchFilters) contextValidateFilters(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Filters); i++ { + + if m.Filters[i] != nil { + if err := m.Filters[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("filters" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("filters" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *SearchFilters) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SearchFilters) UnmarshalBinary(b []byte) error { + var res SearchFilters + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/models/search_match.go b/gen/models/search_match.go new file mode 100644 index 0000000..df81716 --- /dev/null +++ b/gen/models/search_match.go @@ -0,0 +1,84 @@ +// 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" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// SearchMatch search match +// +// swagger:model SearchMatch +type SearchMatch string + +func NewSearchMatch(value SearchMatch) *SearchMatch { + return &value +} + +// Pointer returns a pointer to a freshly-allocated SearchMatch. +func (m SearchMatch) Pointer() *SearchMatch { + return &m +} + +const ( + + // SearchMatchMatchStringEqual captures enum value "MatchStringEqual" + SearchMatchMatchStringEqual SearchMatch = "MatchStringEqual" + + // SearchMatchMatchStringNotEqual captures enum value "MatchStringNotEqual" + SearchMatchMatchStringNotEqual SearchMatch = "MatchStringNotEqual" + + // SearchMatchMatchNotPresent captures enum value "MatchNotPresent" + SearchMatchMatchNotPresent SearchMatch = "MatchNotPresent" + + // SearchMatchMatchCommonPrefix captures enum value "MatchCommonPrefix" + SearchMatchMatchCommonPrefix SearchMatch = "MatchCommonPrefix" +) + +// for schema +var searchMatchEnum []interface{} + +func init() { + var res []SearchMatch + if err := json.Unmarshal([]byte(`["MatchStringEqual","MatchStringNotEqual","MatchNotPresent","MatchCommonPrefix"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + searchMatchEnum = append(searchMatchEnum, v) + } +} + +func (m SearchMatch) validateSearchMatchEnum(path, location string, value SearchMatch) error { + if err := validate.EnumCase(path, location, value, searchMatchEnum, true); err != nil { + return err + } + return nil +} + +// Validate validates this search match +func (m SearchMatch) Validate(formats strfmt.Registry) error { + var res []error + + // value enum + if err := m.validateSearchMatchEnum("", "body", m); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this search match based on context it is used +func (m SearchMatch) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/gen/restapi/embedded_spec.go b/gen/restapi/embedded_spec.go index 38e5dbd..043a3ee 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -368,23 +368,7 @@ func init() { "200": { "description": "Address of uploaded objects", "schema": { - "type": "object", - "required": [ - "objectId", - "containerId" - ], - "properties": { - "containerId": { - "type": "string" - }, - "objectId": { - "type": "string" - } - }, - "example": { - "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", - "objectId": "8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd" - } + "$ref": "#/definitions/Address" } }, "400": { @@ -407,6 +391,67 @@ func init() { } ] }, + "/objects/{containerId}/search": { + "post": { + "summary": "Search objects by filters", + "operationId": "searchObjects", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "The number of containers to skip before starting to collect the result set.", + "name": "offset", + "in": "query" + }, + { + "maximum": 10000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "The numbers of containers to return.", + "name": "limit", + "in": "query" + }, + { + "description": "Filters to search objects", + "name": "searchFilters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchFilters" + } + } + ], + "responses": { + "200": { + "description": "List of objects", + "schema": { + "$ref": "#/definitions/ObjectList" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "parameters": [ + { + "$ref": "#/parameters/signatureParam" + }, + { + "$ref": "#/parameters/signatureKeyParam" + }, + { + "$ref": "#/parameters/signatureScheme" + }, + { + "$ref": "#/parameters/containerId" + } + ] + }, "/objects/{containerId}/{objectId}": { "get": { "summary": "Get object info by address and", @@ -468,6 +513,25 @@ func init() { "DENY" ] }, + "Address": { + "type": "object", + "required": [ + "containerId", + "objectId" + ], + "properties": { + "containerId": { + "type": "string" + }, + "objectId": { + "type": "string" + } + }, + "example": { + "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", + "objectId": "8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd" + } + }, "Attribute": { "type": "object", "required": [ @@ -645,6 +709,20 @@ func init() { "STRING_NOT_EQUAL" ] }, + "ObjectBaseInfo": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/Address" + }, + "name": { + "type": "string" + } + } + }, "ObjectInfo": { "type": "object", "required": [ @@ -686,6 +764,24 @@ func init() { "ownerId": "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM" } }, + "ObjectList": { + "type": "object", + "required": [ + "size", + "objects" + ], + "properties": { + "objects": { + "type": "array", + "items": { + "$ref": "#/definitions/ObjectBaseInfo" + } + }, + "size": { + "type": "integer" + } + } + }, "Operation": { "type": "string", "enum": [ @@ -763,6 +859,48 @@ func init() { } } }, + "SearchFilter": { + "type": "object", + "required": [ + "key", + "value", + "match" + ], + "properties": { + "key": { + "type": "string" + }, + "match": { + "$ref": "#/definitions/SearchMatch" + }, + "value": { + "type": "string" + } + } + }, + "SearchFilters": { + "type": "object", + "required": [ + "filters" + ], + "properties": { + "filters": { + "type": "array", + "items": { + "$ref": "#/definitions/SearchFilter" + } + } + } + }, + "SearchMatch": { + "type": "string", + "enum": [ + "MatchStringEqual", + "MatchStringNotEqual", + "MatchNotPresent", + "MatchCommonPrefix" + ] + }, "Target": { "type": "object", "required": [ @@ -1279,23 +1417,7 @@ func init() { "200": { "description": "Address of uploaded objects", "schema": { - "type": "object", - "required": [ - "objectId", - "containerId" - ], - "properties": { - "containerId": { - "type": "string" - }, - "objectId": { - "type": "string" - } - }, - "example": { - "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", - "objectId": "8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd" - } + "$ref": "#/definitions/Address" } }, "400": { @@ -1330,6 +1452,84 @@ func init() { } ] }, + "/objects/{containerId}/search": { + "post": { + "summary": "Search objects by filters", + "operationId": "searchObjects", + "parameters": [ + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "The number of containers to skip before starting to collect the result set.", + "name": "offset", + "in": "query" + }, + { + "maximum": 10000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "The numbers of containers to return.", + "name": "limit", + "in": "query" + }, + { + "description": "Filters to search objects", + "name": "searchFilters", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchFilters" + } + } + ], + "responses": { + "200": { + "description": "List of objects", + "schema": { + "$ref": "#/definitions/ObjectList" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "parameters": [ + { + "type": "string", + "description": "Base64 encoded signature for bearer token", + "name": "X-Bearer-Signature", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Hex encoded the public part of the key that signed the bearer token", + "name": "X-Bearer-Signature-Key", + "in": "header", + "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" + }, + { + "type": "string", + "description": "Base58 encoded container id", + "name": "containerId", + "in": "path", + "required": true + } + ] + }, "/objects/{containerId}/{objectId}": { "get": { "summary": "Get object info by address and", @@ -1411,6 +1611,25 @@ func init() { "DENY" ] }, + "Address": { + "type": "object", + "required": [ + "containerId", + "objectId" + ], + "properties": { + "containerId": { + "type": "string" + }, + "objectId": { + "type": "string" + } + }, + "example": { + "containerId": "5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv", + "objectId": "8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd" + } + }, "Attribute": { "type": "object", "required": [ @@ -1588,6 +1807,20 @@ func init() { "STRING_NOT_EQUAL" ] }, + "ObjectBaseInfo": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/Address" + }, + "name": { + "type": "string" + } + } + }, "ObjectInfo": { "type": "object", "required": [ @@ -1629,6 +1862,24 @@ func init() { "ownerId": "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM" } }, + "ObjectList": { + "type": "object", + "required": [ + "size", + "objects" + ], + "properties": { + "objects": { + "type": "array", + "items": { + "$ref": "#/definitions/ObjectBaseInfo" + } + }, + "size": { + "type": "integer" + } + } + }, "Operation": { "type": "string", "enum": [ @@ -1706,6 +1957,48 @@ func init() { } } }, + "SearchFilter": { + "type": "object", + "required": [ + "key", + "value", + "match" + ], + "properties": { + "key": { + "type": "string" + }, + "match": { + "$ref": "#/definitions/SearchMatch" + }, + "value": { + "type": "string" + } + } + }, + "SearchFilters": { + "type": "object", + "required": [ + "filters" + ], + "properties": { + "filters": { + "type": "array", + "items": { + "$ref": "#/definitions/SearchFilter" + } + } + } + }, + "SearchMatch": { + "type": "string", + "enum": [ + "MatchStringEqual", + "MatchStringNotEqual", + "MatchNotPresent", + "MatchCommonPrefix" + ] + }, "Target": { "type": "object", "required": [ diff --git a/gen/restapi/operations/neofs_rest_gw_api.go b/gen/restapi/operations/neofs_rest_gw_api.go index 5798828..3dd8021 100644 --- a/gen/restapi/operations/neofs_rest_gw_api.go +++ b/gen/restapi/operations/neofs_rest_gw_api.go @@ -74,6 +74,9 @@ func NewNeofsRestGwAPI(spec *loads.Document) *NeofsRestGwAPI { PutObjectHandler: PutObjectHandlerFunc(func(params PutObjectParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation PutObject has not yet been implemented") }), + SearchObjectsHandler: SearchObjectsHandlerFunc(func(params SearchObjectsParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation SearchObjects has not yet been implemented") + }), // Applies when the "Authorization" header is set BearerAuthAuth: func(token string) (*models.Principal, error) { @@ -144,6 +147,8 @@ type NeofsRestGwAPI struct { PutContainerEACLHandler PutContainerEACLHandler // PutObjectHandler sets the operation handler for the put object operation PutObjectHandler PutObjectHandler + // SearchObjectsHandler sets the operation handler for the search objects operation + SearchObjectsHandler SearchObjectsHandler // ServeError is called when an error is received, there is a default handler // but you can set your own with this @@ -255,6 +260,9 @@ func (o *NeofsRestGwAPI) Validate() error { if o.PutObjectHandler == nil { unregistered = append(unregistered, "PutObjectHandler") } + if o.SearchObjectsHandler == nil { + unregistered = append(unregistered, "SearchObjectsHandler") + } if len(unregistered) > 0 { return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", ")) @@ -394,6 +402,10 @@ func (o *NeofsRestGwAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/objects"] = NewPutObject(o.context, o.PutObjectHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/objects/{containerId}/search"] = NewSearchObjects(o.context, o.SearchObjectsHandler) } // Serve creates a http handler to serve the API over HTTP diff --git a/gen/restapi/operations/put_object.go b/gen/restapi/operations/put_object.go index b8e2845..0f72c84 100644 --- a/gen/restapi/operations/put_object.go +++ b/gen/restapi/operations/put_object.go @@ -151,77 +151,3 @@ func (o *PutObjectBody) UnmarshalBinary(b []byte) error { *o = res return nil } - -// PutObjectOKBody put object o k body -// Example: {"containerId":"5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv","objectId":"8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd"} -// -// swagger:model PutObjectOKBody -type PutObjectOKBody struct { - - // container Id - // Required: true - ContainerID *string `json:"containerId"` - - // object Id - // Required: true - ObjectID *string `json:"objectId"` -} - -// Validate validates this put object o k body -func (o *PutObjectOKBody) Validate(formats strfmt.Registry) error { - var res []error - - if err := o.validateContainerID(formats); err != nil { - res = append(res, err) - } - - if err := o.validateObjectID(formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (o *PutObjectOKBody) validateContainerID(formats strfmt.Registry) error { - - if err := validate.Required("putObjectOK"+"."+"containerId", "body", o.ContainerID); err != nil { - return err - } - - return nil -} - -func (o *PutObjectOKBody) validateObjectID(formats strfmt.Registry) error { - - if err := validate.Required("putObjectOK"+"."+"objectId", "body", o.ObjectID); err != nil { - return err - } - - return nil -} - -// ContextValidate validates this put object o k body based on context it is used -func (o *PutObjectOKBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (o *PutObjectOKBody) MarshalBinary() ([]byte, error) { - if o == nil { - return nil, nil - } - return swag.WriteJSON(o) -} - -// UnmarshalBinary interface implementation -func (o *PutObjectOKBody) UnmarshalBinary(b []byte) error { - var res PutObjectOKBody - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *o = res - return nil -} diff --git a/gen/restapi/operations/put_object_responses.go b/gen/restapi/operations/put_object_responses.go index 78ad30a..4bd2745 100644 --- a/gen/restapi/operations/put_object_responses.go +++ b/gen/restapi/operations/put_object_responses.go @@ -25,7 +25,7 @@ type PutObjectOK struct { /* In: Body */ - Payload *PutObjectOKBody `json:"body,omitempty"` + Payload *models.Address `json:"body,omitempty"` } // NewPutObjectOK creates PutObjectOK with default headers values @@ -35,13 +35,13 @@ func NewPutObjectOK() *PutObjectOK { } // WithPayload adds the payload to the put object o k response -func (o *PutObjectOK) WithPayload(payload *PutObjectOKBody) *PutObjectOK { +func (o *PutObjectOK) WithPayload(payload *models.Address) *PutObjectOK { o.Payload = payload return o } // SetPayload sets the payload to the put object o k response -func (o *PutObjectOK) SetPayload(payload *PutObjectOKBody) { +func (o *PutObjectOK) SetPayload(payload *models.Address) { o.Payload = payload } diff --git a/gen/restapi/operations/search_objects.go b/gen/restapi/operations/search_objects.go new file mode 100644 index 0000000..7169a45 --- /dev/null +++ b/gen/restapi/operations/search_objects.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" + + "github.com/nspcc-dev/neofs-rest-gw/gen/models" +) + +// SearchObjectsHandlerFunc turns a function with the right signature into a search objects handler +type SearchObjectsHandlerFunc func(SearchObjectsParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn SearchObjectsHandlerFunc) Handle(params SearchObjectsParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// SearchObjectsHandler interface for that can handle valid search objects params +type SearchObjectsHandler interface { + Handle(SearchObjectsParams, *models.Principal) middleware.Responder +} + +// NewSearchObjects creates a new http.Handler for the search objects operation +func NewSearchObjects(ctx *middleware.Context, handler SearchObjectsHandler) *SearchObjects { + return &SearchObjects{Context: ctx, Handler: handler} +} + +/* SearchObjects swagger:route POST /objects/{containerId}/search searchObjects + +Search objects by filters + +*/ +type SearchObjects struct { + Context *middleware.Context + Handler SearchObjectsHandler +} + +func (o *SearchObjects) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewSearchObjectsParams() + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + *r = *aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/gen/restapi/operations/search_objects_parameters.go b/gen/restapi/operations/search_objects_parameters.go new file mode 100644 index 0000000..5371497 --- /dev/null +++ b/gen/restapi/operations/search_objects_parameters.go @@ -0,0 +1,322 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "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" +) + +// NewSearchObjectsParams creates a new SearchObjectsParams object +// with the default values initialized. +func NewSearchObjectsParams() SearchObjectsParams { + + var ( + // initialize parameters with default values + + limitDefault = int64(100) + offsetDefault = int64(0) + + walletConnectDefault = bool(false) + ) + + return SearchObjectsParams{ + Limit: &limitDefault, + + Offset: &offsetDefault, + + WalletConnect: &walletConnectDefault, + } +} + +// SearchObjectsParams contains all the bound params for the search objects operation +// typically these are obtained from a http.Request +// +// swagger:parameters searchObjects +type SearchObjectsParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*Base64 encoded signature for bearer token + Required: true + In: header + */ + XBearerSignature string + /*Hex encoded the public part of the key that signed the bearer token + Required: true + In: header + */ + XBearerSignatureKey string + /*Base58 encoded container id + Required: true + In: path + */ + ContainerID string + /*The numbers of containers to return. + Maximum: 10000 + Minimum: 1 + In: query + Default: 100 + */ + Limit *int64 + /*The number of containers to skip before starting to collect the result set. + Minimum: 0 + In: query + Default: 0 + */ + Offset *int64 + /*Filters to search objects + Required: true + In: body + */ + SearchFilters *models.SearchFilters + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewSearchObjectsParams() beforehand. +func (o *SearchObjectsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + if err := o.bindXBearerSignature(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature")], true, route.Formats); err != nil { + res = append(res, err) + } + + if err := o.bindXBearerSignatureKey(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature-Key")], 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) + } + + qLimit, qhkLimit, _ := qs.GetOK("limit") + if err := o.bindLimit(qLimit, qhkLimit, route.Formats); err != nil { + res = append(res, err) + } + + qOffset, qhkOffset, _ := qs.GetOK("offset") + if err := o.bindOffset(qOffset, qhkOffset, route.Formats); err != nil { + res = append(res, err) + } + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.SearchFilters + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("searchFilters", "body", "")) + } else { + res = append(res, errors.NewParseError("searchFilters", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(context.Background()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.SearchFilters = &body + } + } + } else { + res = append(res, errors.Required("searchFilters", "body", "")) + } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindXBearerSignature binds and validates parameter XBearerSignature from header. +func (o *SearchObjectsParams) bindXBearerSignature(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("X-Bearer-Signature", "header", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + + if err := validate.RequiredString("X-Bearer-Signature", "header", raw); err != nil { + return err + } + o.XBearerSignature = raw + + return nil +} + +// bindXBearerSignatureKey binds and validates parameter XBearerSignatureKey from header. +func (o *SearchObjectsParams) bindXBearerSignatureKey(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("X-Bearer-Signature-Key", "header", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + + if err := validate.RequiredString("X-Bearer-Signature-Key", "header", raw); err != nil { + return err + } + o.XBearerSignatureKey = raw + + return nil +} + +// bindContainerID binds and validates parameter ContainerID from path. +func (o *SearchObjectsParams) bindContainerID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.ContainerID = raw + + return nil +} + +// bindLimit binds and validates parameter Limit from query. +func (o *SearchObjectsParams) bindLimit(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewSearchObjectsParams() + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("limit", "query", "int64", raw) + } + o.Limit = &value + + if err := o.validateLimit(formats); err != nil { + return err + } + + return nil +} + +// validateLimit carries on validations for parameter Limit +func (o *SearchObjectsParams) validateLimit(formats strfmt.Registry) error { + + if err := validate.MinimumInt("limit", "query", *o.Limit, 1, false); err != nil { + return err + } + + if err := validate.MaximumInt("limit", "query", *o.Limit, 10000, false); err != nil { + return err + } + + return nil +} + +// bindOffset binds and validates parameter Offset from query. +func (o *SearchObjectsParams) bindOffset(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewSearchObjectsParams() + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("offset", "query", "int64", raw) + } + o.Offset = &value + + if err := o.validateOffset(formats); err != nil { + return err + } + + return nil +} + +// validateOffset carries on validations for parameter Offset +func (o *SearchObjectsParams) validateOffset(formats strfmt.Registry) error { + + if err := validate.MinimumInt("offset", "query", *o.Offset, 0, false); err != nil { + return err + } + + return nil +} + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *SearchObjectsParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewSearchObjectsParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/gen/restapi/operations/search_objects_responses.go b/gen/restapi/operations/search_objects_responses.go new file mode 100644 index 0000000..afbe4a7 --- /dev/null +++ b/gen/restapi/operations/search_objects_responses.go @@ -0,0 +1,100 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/nspcc-dev/neofs-rest-gw/gen/models" +) + +// SearchObjectsOKCode is the HTTP code returned for type SearchObjectsOK +const SearchObjectsOKCode int = 200 + +/*SearchObjectsOK List of objects + +swagger:response searchObjectsOK +*/ +type SearchObjectsOK struct { + + /* + In: Body + */ + Payload *models.ObjectList `json:"body,omitempty"` +} + +// NewSearchObjectsOK creates SearchObjectsOK with default headers values +func NewSearchObjectsOK() *SearchObjectsOK { + + return &SearchObjectsOK{} +} + +// WithPayload adds the payload to the search objects o k response +func (o *SearchObjectsOK) WithPayload(payload *models.ObjectList) *SearchObjectsOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the search objects o k response +func (o *SearchObjectsOK) SetPayload(payload *models.ObjectList) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SearchObjectsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// SearchObjectsBadRequestCode is the HTTP code returned for type SearchObjectsBadRequest +const SearchObjectsBadRequestCode int = 400 + +/*SearchObjectsBadRequest Bad request + +swagger:response searchObjectsBadRequest +*/ +type SearchObjectsBadRequest struct { + + /* + In: Body + */ + Payload models.Error `json:"body,omitempty"` +} + +// NewSearchObjectsBadRequest creates SearchObjectsBadRequest with default headers values +func NewSearchObjectsBadRequest() *SearchObjectsBadRequest { + + return &SearchObjectsBadRequest{} +} + +// WithPayload adds the payload to the search objects bad request response +func (o *SearchObjectsBadRequest) WithPayload(payload models.Error) *SearchObjectsBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the search objects bad request response +func (o *SearchObjectsBadRequest) SetPayload(payload models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SearchObjectsBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/handlers/api.go b/handlers/api.go index a2235e3..e102d5f 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -66,6 +66,7 @@ func (a *API) Configure(api *operations.NeofsRestGwAPI) http.Handler { api.PutObjectHandler = operations.PutObjectHandlerFunc(a.PutObjects) api.GetObjectInfoHandler = operations.GetObjectInfoHandlerFunc(a.GetObjectInfo) api.DeleteObjectHandler = operations.DeleteObjectHandlerFunc(a.DeleteObject) + api.SearchObjectsHandler = operations.SearchObjectsHandlerFunc(a.SearchObjects) api.PutContainerHandler = operations.PutContainerHandlerFunc(a.PutContainers) api.GetContainerHandler = operations.GetContainerHandlerFunc(a.GetContainer) diff --git a/handlers/objects.go b/handlers/objects.go index 25c4570..d7778e4 100644 --- a/handlers/objects.go +++ b/handlers/objects.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "crypto/ecdsa" "encoding/base64" "fmt" @@ -67,7 +68,7 @@ func (a *API) PutObjects(params operations.PutObjectParams, principal *models.Pr return errorResponse.WithPayload(NewError(err)) } - var resp operations.PutObjectOKBody + var resp models.Address resp.ContainerID = params.Object.ContainerID resp.ObjectID = NewString(objID.String()) @@ -144,6 +145,109 @@ func (a *API) DeleteObject(params operations.DeleteObjectParams, principal *mode return operations.NewDeleteObjectNoContent() } +// SearchObjects handler that removes object from NeoFS. +func (a *API) SearchObjects(params operations.SearchObjectsParams, principal *models.Principal) middleware.Responder { + errorResponse := operations.NewSearchObjectsBadRequest() + ctx := params.HTTPRequest.Context() + + var cnrID cid.ID + if err := cnrID.Parse(params.ContainerID); err != nil { + a.log.Error("invalid container id", zap.Error(err)) + return errorResponse.WithPayload("invalid container id") + } + + btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey, *params.WalletConnect) + if err != nil { + a.log.Error("failed to get bearer token", zap.Error(err)) + return errorResponse.WithPayload(NewError(err)) + } + + filters, err := ToNativeFilters(params.SearchFilters) + if err != nil { + a.log.Error("failed to transform to native", zap.Error(err)) + return errorResponse.WithPayload(NewError(err)) + } + + var prm pool.PrmObjectSearch + prm.SetContainerID(cnrID) + prm.UseBearer(btoken) + prm.SetFilters(filters) + + resSearch, err := a.pool.SearchObjects(ctx, prm) + if err != nil { + a.log.Error("failed to search objects", zap.Error(err)) + return errorResponse.WithPayload(NewError(err)) + } + + offset := int(*params.Offset) + size := int(*params.Limit) + + var iterateErr error + var obj *models.ObjectBaseInfo + var objects []*models.ObjectBaseInfo + + i := 0 + err = resSearch.Iterate(func(id oid.ID) bool { + if i < offset { + i++ + return false + } + + if obj, iterateErr = headObjectBaseInfo(ctx, a.pool, &cnrID, &id, btoken); iterateErr != nil { + return true + } + + objects = append(objects, obj) + + return len(objects) == size + }) + if err == nil { + err = iterateErr + } + if err != nil { + a.log.Error("failed to search objects", zap.Error(err)) + return errorResponse.WithPayload(NewError(err)) + } + + list := &models.ObjectList{ + Size: NewInteger(int64(len(objects))), + Objects: objects, + } + + return operations.NewSearchObjectsOK().WithPayload(list) +} + +func headObjectBaseInfo(ctx context.Context, p *pool.Pool, cnrID *cid.ID, objID *oid.ID, btoken *token.BearerToken) (*models.ObjectBaseInfo, error) { + addr := address.NewAddress() + addr.SetContainerID(cnrID) + addr.SetObjectID(objID) + + var prm pool.PrmObjectHead + prm.SetAddress(*addr) + prm.UseBearer(btoken) + + objInfo, err := p.HeadObject(ctx, prm) + if err != nil { + return nil, err + } + + resp := &models.ObjectBaseInfo{ + Address: &models.Address{ + ContainerID: NewString(cnrID.String()), + ObjectID: NewString(objID.String()), + }, + } + + for _, attr := range objInfo.Attributes() { + if attr.Key() == object.AttributeFileName { + resp.Name = attr.Value() + break + } + } + + return resp, nil +} + func parseAddress(containerID, objectID string) (*address.Address, error) { var cnrID cid.ID if err := cnrID.Parse(containerID); err != nil { diff --git a/handlers/transformers.go b/handlers/transformers.go index 294f0cf..926d249 100644 --- a/handlers/transformers.go +++ b/handlers/transformers.go @@ -3,6 +3,7 @@ package handlers import ( "encoding/hex" "fmt" + "github.com/nspcc-dev/neofs-sdk-go/object" sessionv2 "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-rest-gw/gen/models" @@ -391,3 +392,40 @@ func ToNativeTable(records []*models.Record) (*eacl.Table, error) { return table, nil } + +// ToNativeMatchFilter converts models.SearchMatch to object.SearchMatchType. +func ToNativeMatchFilter(s *models.SearchMatch) (object.SearchMatchType, error) { + if s == nil { + return object.MatchUnknown, fmt.Errorf("unsupported empty verb type") + } + + switch *s { + case models.SearchMatchMatchStringEqual: + return object.MatchStringEqual, nil + case models.SearchMatchMatchStringNotEqual: + return object.MatchStringNotEqual, nil + case models.SearchMatchMatchNotPresent: + return object.MatchNotPresent, nil + case models.SearchMatchMatchCommonPrefix: + return object.MatchCommonPrefix, nil + default: + return object.MatchUnknown, fmt.Errorf("unsupported search match: '%s'", *s) + } +} + +// ToNativeFilters converts models.SearchFilters to object.SearchFilters. +func ToNativeFilters(fs *models.SearchFilters) (object.SearchFilters, error) { + filters := object.NewSearchFilters() + filters.AddRootFilter() + + for _, f := range fs.Filters { + matchFilter, err := ToNativeMatchFilter(f.Match) + if err != nil { + return nil, err + } + + filters.AddFilter(*f.Key, *f.Value, matchFilter) + } + + return filters, nil +} diff --git a/spec/rest.yaml b/spec/rest.yaml index a4e1e21..dec31fc 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -130,18 +130,46 @@ paths: 200: description: Address of uploaded objects schema: - type: object - properties: - objectId: - type: string - containerId: - type: string - required: - - objectId - - containerId - example: - objectId: 8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd - containerId: 5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv + $ref: '#/definitions/Address' + 400: + description: Bad request + schema: + $ref: '#/definitions/Error' + + /objects/{containerId}/search: + parameters: + - $ref: '#/parameters/signatureParam' + - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' + - $ref: '#/parameters/containerId' + post: + operationId: searchObjects + summary: Search objects by filters + parameters: + - in: query + name: offset + type: integer + default: 0 + minimum: 0 + description: The number of containers to skip before starting to collect the result set. + - in: query + name: limit + type: integer + default: 100 + minimum: 1 + maximum: 10000 + description: The numbers of containers to return. + - in: body + required: true + name: searchFilters + description: Filters to search objects + schema: + $ref: '#/definitions/SearchFilters' + responses: + 200: + description: List of objects + schema: + $ref: '#/definitions/ObjectList' 400: description: Bad request schema: @@ -513,6 +541,35 @@ definitions: required: - size - containers + SearchFilters: + type: object + properties: + filters: + type: array + items: + $ref: '#/definitions/SearchFilter' + required: + - filters + SearchFilter: + type: object + properties: + key: + type: string + value: + type: string + match: + $ref: '#/definitions/SearchMatch' + required: + - key + - value + - match + SearchMatch: + type: string + enum: + - MatchStringEqual + - MatchStringNotEqual + - MatchNotPresent + - MatchCommonPrefix ContainerBaseInfo: type: object properties: @@ -522,6 +579,27 @@ definitions: type: string required: - containerId + ObjectList: + type: object + properties: + size: + type: integer + objects: + type: array + items: + $ref: '#/definitions/ObjectBaseInfo' + required: + - size + - objects + ObjectBaseInfo: + type: object + properties: + address: + $ref: '#/definitions/Address' + name: + type: string + required: + - address ObjectInfo: type: object properties: @@ -549,6 +627,19 @@ definitions: value: "1648810072" - key: Name value: object + Address: + type: object + properties: + containerId: + type: string + objectId: + type: string + required: + - containerId + - objectId + example: + objectId: 8N3o7Dtr6T1xteCt6eRwhpmJ7JhME58Hyu1dvaswuTDd + containerId: 5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv Eacl: type: object properties: