diff --git a/gen/models/storage_group.go b/gen/models/storage_group.go new file mode 100644 index 0000000..b20cae4 --- /dev/null +++ b/gen/models/storage_group.go @@ -0,0 +1,130 @@ +// 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" +) + +// StorageGroup Storage group keeps verification information for Data Audit sessions. +// +// swagger:model StorageGroup +type StorageGroup struct { + + // Container id to which storage group is belong. Set by server. + // Read Only: true + ContainerID string `json:"containerId,omitempty"` + + // Lifetime in epochs for storage group. + // Required: true + Lifetime *int64 `json:"lifetime"` + + // Object identifiers to be placed into storage group. Must be unique. + // Required: true + Members []string `json:"members"` + + // Name of storage group. It will be the value of the `FileName` attribute in storage group object. + Name string `json:"name,omitempty"` + + // Object id of storage group. Set by server. + // Read Only: true + ObjectID string `json:"objectId,omitempty"` +} + +// Validate validates this storage group +func (m *StorageGroup) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateLifetime(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMembers(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *StorageGroup) validateLifetime(formats strfmt.Registry) error { + + if err := validate.Required("lifetime", "body", m.Lifetime); err != nil { + return err + } + + return nil +} + +func (m *StorageGroup) validateMembers(formats strfmt.Registry) error { + + if err := validate.Required("members", "body", m.Members); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this storage group based on the context it is used +func (m *StorageGroup) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateContainerID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateObjectID(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *StorageGroup) contextValidateContainerID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "containerId", "body", string(m.ContainerID)); err != nil { + return err + } + + return nil +} + +func (m *StorageGroup) contextValidateObjectID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "objectId", "body", string(m.ObjectID)); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *StorageGroup) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *StorageGroup) UnmarshalBinary(b []byte) error { + var res StorageGroup + 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 95e7bd5..c90344c 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -523,6 +523,51 @@ func init() { } ] }, + "/containers/{containerId}/storagegroups": { + "put": { + "summary": "Create a new storage group in container.", + "operationId": "putStorageGroup", + "parameters": [ + { + "$ref": "#/parameters/signatureParam" + }, + { + "$ref": "#/parameters/signatureKeyParam" + }, + { + "$ref": "#/parameters/signatureScheme" + }, + { + "description": "Storage group co create.", + "name": "storageGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/StorageGroup" + } + } + ], + "responses": { + "200": { + "description": "Address of uploaded storage group.", + "schema": { + "$ref": "#/definitions/Address" + } + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "parameters": [ + { + "$ref": "#/parameters/containerId" + } + ] + }, "/objects": { "put": { "consumes": [ @@ -1522,6 +1567,41 @@ func init() { "MatchCommonPrefix" ] }, + "StorageGroup": { + "description": "Storage group keeps verification information for Data Audit sessions.", + "type": "object", + "required": [ + "lifetime", + "members" + ], + "properties": { + "containerId": { + "description": "Container id to which storage group is belong. Set by server.", + "type": "string", + "readOnly": true + }, + "lifetime": { + "description": "Lifetime in epochs for storage group.", + "type": "integer" + }, + "members": { + "description": "Object identifiers to be placed into storage group. Must be unique.", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "Name of storage group. It will be the value of the ` + "`" + `FileName` + "`" + ` attribute in storage group object.", + "type": "string" + }, + "objectId": { + "description": "Object id of storage group. Set by server.", + "type": "string", + "readOnly": true + } + } + }, "SuccessResponse": { "description": "Success response.", "type": "object", @@ -2220,6 +2300,67 @@ func init() { } ] }, + "/containers/{containerId}/storagegroups": { + "put": { + "summary": "Create a new storage group in container.", + "operationId": "putStorageGroup", + "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 wallet connect signature scheme or native NeoFS signature.", + "name": "walletConnect", + "in": "query" + }, + { + "description": "Storage group co create.", + "name": "storageGroup", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/StorageGroup" + } + } + ], + "responses": { + "200": { + "description": "Address of uploaded storage group.", + "schema": { + "$ref": "#/definitions/Address" + } + }, + "400": { + "description": "Bad request.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "parameters": [ + { + "type": "string", + "description": "Base58 encoded container id.", + "name": "containerId", + "in": "path", + "required": true + } + ] + }, "/objects": { "put": { "consumes": [ @@ -3290,6 +3431,41 @@ func init() { "MatchCommonPrefix" ] }, + "StorageGroup": { + "description": "Storage group keeps verification information for Data Audit sessions.", + "type": "object", + "required": [ + "lifetime", + "members" + ], + "properties": { + "containerId": { + "description": "Container id to which storage group is belong. Set by server.", + "type": "string", + "readOnly": true + }, + "lifetime": { + "description": "Lifetime in epochs for storage group.", + "type": "integer" + }, + "members": { + "description": "Object identifiers to be placed into storage group. Must be unique.", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "description": "Name of storage group. It will be the value of the ` + "`" + `FileName` + "`" + ` attribute in storage group object.", + "type": "string" + }, + "objectId": { + "description": "Object id of storage group. Set by server.", + "type": "string", + "readOnly": true + } + } + }, "SuccessResponse": { "description": "Success response.", "type": "object", diff --git a/gen/restapi/operations/frostfs_rest_gw_api.go b/gen/restapi/operations/frostfs_rest_gw_api.go index fe9ddbc..d7e8a9f 100644 --- a/gen/restapi/operations/frostfs_rest_gw_api.go +++ b/gen/restapi/operations/frostfs_rest_gw_api.go @@ -104,6 +104,9 @@ func NewFrostfsRestGwAPI(spec *loads.Document) *FrostfsRestGwAPI { PutObjectHandler: PutObjectHandlerFunc(func(params PutObjectParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation PutObject has not yet been implemented") }), + PutStorageGroupHandler: PutStorageGroupHandlerFunc(func(params PutStorageGroupParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation PutStorageGroup 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") }), @@ -197,6 +200,8 @@ type FrostfsRestGwAPI struct { PutContainerEACLHandler PutContainerEACLHandler // PutObjectHandler sets the operation handler for the put object operation PutObjectHandler PutObjectHandler + // PutStorageGroupHandler sets the operation handler for the put storage group operation + PutStorageGroupHandler PutStorageGroupHandler // SearchObjectsHandler sets the operation handler for the search objects operation SearchObjectsHandler SearchObjectsHandler @@ -340,6 +345,9 @@ func (o *FrostfsRestGwAPI) Validate() error { if o.PutObjectHandler == nil { unregistered = append(unregistered, "PutObjectHandler") } + if o.PutStorageGroupHandler == nil { + unregistered = append(unregistered, "PutStorageGroupHandler") + } if o.SearchObjectsHandler == nil { unregistered = append(unregistered, "SearchObjectsHandler") } @@ -522,6 +530,10 @@ func (o *FrostfsRestGwAPI) initHandlerCache() { o.handlers["PUT"] = make(map[string]http.Handler) } o.handlers["PUT"]["/objects"] = NewPutObject(o.context, o.PutObjectHandler) + if o.handlers["PUT"] == nil { + o.handlers["PUT"] = make(map[string]http.Handler) + } + o.handlers["PUT"]["/containers/{containerId}/storagegroups"] = NewPutStorageGroup(o.context, o.PutStorageGroupHandler) if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } diff --git a/gen/restapi/operations/put_storage_group.go b/gen/restapi/operations/put_storage_group.go new file mode 100644 index 0000000..0d8b178 --- /dev/null +++ b/gen/restapi/operations/put_storage_group.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" +) + +// PutStorageGroupHandlerFunc turns a function with the right signature into a put storage group handler +type PutStorageGroupHandlerFunc func(PutStorageGroupParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn PutStorageGroupHandlerFunc) Handle(params PutStorageGroupParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// PutStorageGroupHandler interface for that can handle valid put storage group params +type PutStorageGroupHandler interface { + Handle(PutStorageGroupParams, *models.Principal) middleware.Responder +} + +// NewPutStorageGroup creates a new http.Handler for the put storage group operation +func NewPutStorageGroup(ctx *middleware.Context, handler PutStorageGroupHandler) *PutStorageGroup { + return &PutStorageGroup{Context: ctx, Handler: handler} +} + +/* PutStorageGroup swagger:route PUT /containers/{containerId}/storagegroups putStorageGroup + +Create a new storage group in container. + +*/ +type PutStorageGroup struct { + Context *middleware.Context + Handler PutStorageGroupHandler +} + +func (o *PutStorageGroup) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewPutStorageGroupParams() + 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/put_storage_group_parameters.go b/gen/restapi/operations/put_storage_group_parameters.go new file mode 100644 index 0000000..299dfc7 --- /dev/null +++ b/gen/restapi/operations/put_storage_group_parameters.go @@ -0,0 +1,212 @@ +// 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" +) + +// NewPutStorageGroupParams creates a new PutStorageGroupParams object +// with the default values initialized. +func NewPutStorageGroupParams() PutStorageGroupParams { + + var ( + // initialize parameters with default values + + walletConnectDefault = bool(false) + ) + + return PutStorageGroupParams{ + WalletConnect: &walletConnectDefault, + } +} + +// PutStorageGroupParams contains all the bound params for the put storage group operation +// typically these are obtained from a http.Request +// +// swagger:parameters putStorageGroup +type PutStorageGroupParams 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 + /*Storage group co create. + Required: true + In: body + */ + StorageGroup *models.StorageGroup + /*Use wallet connect signature scheme or native NeoFS signature. + 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 NewPutStorageGroupParams() beforehand. +func (o *PutStorageGroupParams) 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) + } + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.StorageGroup + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("storageGroup", "body", "")) + } else { + res = append(res, errors.NewParseError("storageGroup", "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.StorageGroup = &body + } + } + } else { + res = append(res, errors.Required("storageGroup", "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 *PutStorageGroupParams) 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 *PutStorageGroupParams) 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 *PutStorageGroupParams) 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 +} + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *PutStorageGroupParams) 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 NewPutStorageGroupParams() + 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/put_storage_group_responses.go b/gen/restapi/operations/put_storage_group_responses.go new file mode 100644 index 0000000..ea49c8d --- /dev/null +++ b/gen/restapi/operations/put_storage_group_responses.go @@ -0,0 +1,102 @@ +// 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" +) + +// PutStorageGroupOKCode is the HTTP code returned for type PutStorageGroupOK +const PutStorageGroupOKCode int = 200 + +/*PutStorageGroupOK Address of uploaded storage group. + +swagger:response putStorageGroupOK +*/ +type PutStorageGroupOK struct { + + /* + In: Body + */ + Payload *models.Address `json:"body,omitempty"` +} + +// NewPutStorageGroupOK creates PutStorageGroupOK with default headers values +func NewPutStorageGroupOK() *PutStorageGroupOK { + + return &PutStorageGroupOK{} +} + +// WithPayload adds the payload to the put storage group o k response +func (o *PutStorageGroupOK) WithPayload(payload *models.Address) *PutStorageGroupOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put storage group o k response +func (o *PutStorageGroupOK) SetPayload(payload *models.Address) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutStorageGroupOK) 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 + } + } +} + +// PutStorageGroupBadRequestCode is the HTTP code returned for type PutStorageGroupBadRequest +const PutStorageGroupBadRequestCode int = 400 + +/*PutStorageGroupBadRequest Bad request. + +swagger:response putStorageGroupBadRequest +*/ +type PutStorageGroupBadRequest struct { + + /* + In: Body + */ + Payload *models.ErrorResponse `json:"body,omitempty"` +} + +// NewPutStorageGroupBadRequest creates PutStorageGroupBadRequest with default headers values +func NewPutStorageGroupBadRequest() *PutStorageGroupBadRequest { + + return &PutStorageGroupBadRequest{} +} + +// WithPayload adds the payload to the put storage group bad request response +func (o *PutStorageGroupBadRequest) WithPayload(payload *models.ErrorResponse) *PutStorageGroupBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the put storage group bad request response +func (o *PutStorageGroupBadRequest) SetPayload(payload *models.ErrorResponse) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *PutStorageGroupBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + 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 67af39f..ce9055c 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -126,6 +126,8 @@ func (a *API) Configure(api *operations.FrostfsRestGwAPI) http.Handler { api.OptionsContainersEACLHandler = operations.OptionsContainersEACLHandlerFunc(a.OptionsContainersEACL) api.PutContainerEACLHandler = operations.PutContainerEACLHandlerFunc(a.PutContainerEACL) api.GetContainerEACLHandler = operations.GetContainerEACLHandlerFunc(a.GetContainerEACL) + api.ListContainersHandler = operations.ListContainersHandlerFunc(a.ListContainer) + api.PutStorageGroupHandler = operations.PutStorageGroupHandlerFunc(a.PutStorageGroup) api.BearerAuthAuth = func(s string) (*models.Principal, error) { if !strings.HasPrefix(s, BearerPrefix) { diff --git a/handlers/containers.go b/handlers/containers.go index 645cb31..89c8b14 100644 --- a/handlers/containers.go +++ b/handlers/containers.go @@ -413,6 +413,10 @@ func createContainer(ctx context.Context, p *pool.Pool, stoken session.Container container.WriteDomain(&cnr, domain) } + if err = pool.SyncContainerWithNetwork(ctx, &cnr, p); err != nil { + return cid.ID{}, fmt.Errorf("sync container with network: %w", err) + } + var prm pool.PrmContainerPut prm.SetContainer(cnr) prm.WithinSession(stoken) diff --git a/handlers/storagegroup.go b/handlers/storagegroup.go new file mode 100644 index 0000000..7ff5e96 --- /dev/null +++ b/handlers/storagegroup.go @@ -0,0 +1,190 @@ +package handlers + +import ( + "context" + "fmt" + + "github.com/go-openapi/runtime/middleware" + "github.com/nspcc-dev/neofs-rest-gw/gen/models" + "github.com/nspcc-dev/neofs-rest-gw/gen/restapi/operations" + "github.com/nspcc-dev/neofs-rest-gw/internal/util" + "github.com/nspcc-dev/neofs-sdk-go/bearer" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + "github.com/nspcc-dev/neofs-sdk-go/container" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/pool" + "github.com/nspcc-dev/neofs-sdk-go/storagegroup" + "github.com/nspcc-dev/tzhash/tz" +) + +// PutStorageGroup handler that create a new storage group. +func (a *API) PutStorageGroup(params operations.PutStorageGroupParams, principal *models.Principal) middleware.Responder { + ctx := params.HTTPRequest.Context() + + cnrID, err := parseContainerID(params.ContainerID) + if err != nil { + resp := a.logAndGetErrorResponse("invalid container id", err) + return operations.NewPutStorageGroupBadRequest().WithPayload(resp) + } + + btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey, *params.WalletConnect) + if err != nil { + resp := a.logAndGetErrorResponse("invalid bearer token", err) + return operations.NewPutStorageGroupBadRequest().WithPayload(resp) + } + + sg, err := a.formStorageGroup(ctx, cnrID, btoken, params.StorageGroup) + if err != nil { + resp := a.logAndGetErrorResponse("form storage group", err) + return operations.NewPutStorageGroupBadRequest().WithPayload(resp) + } + + objID, err := a.putStorageGroupObject(ctx, cnrID, btoken, params.StorageGroup.Name, *sg) + if err != nil { + resp := a.logAndGetErrorResponse("put storage group", err) + return operations.NewPutStorageGroupBadRequest().WithPayload(resp) + } + + var resp models.Address + resp.ContainerID = util.NewString(params.ContainerID) + resp.ObjectID = util.NewString(objID.String()) + + return operations.NewPutStorageGroupOK().WithPayload(&resp) +} + +func (a *API) formStorageGroup(ctx context.Context, cnrID cid.ID, btoken bearer.Token, storageGroup *models.StorageGroup) (*storagegroup.StorageGroup, error) { + members, err := a.parseStorageGroupMembers(storageGroup) + if err != nil { + return nil, fmt.Errorf("parse storage group members: %w", err) + } + + needCalcHash, err := isHomomorphicHashingDisabled(ctx, a.pool, cnrID) + if err != nil { + return nil, fmt.Errorf("check if homomorphic hash disabled: %w", err) + } + + sgSize, cs, err := a.getStorageGroupSizeAndHash(ctx, cnrID, btoken, members, needCalcHash) + if err != nil { + return nil, fmt.Errorf("get storage group size: %w", err) + } + + networkInfo, err := a.pool.NetworkInfo(ctx) + if err != nil { + return nil, fmt.Errorf("get network info: %w", err) + } + + var sg storagegroup.StorageGroup + sg.SetMembers(members) + sg.SetValidationDataSize(sgSize) + sg.SetExpirationEpoch(networkInfo.CurrentEpoch() + uint64(*storageGroup.Lifetime)) + + if needCalcHash { + sg.SetValidationDataHash(*cs) + } + + return &sg, nil +} + +func (a *API) putStorageGroupObject(ctx context.Context, cnrID cid.ID, btoken bearer.Token, fileName string, sg storagegroup.StorageGroup) (*oid.ID, error) { + owner := bearer.ResolveIssuer(btoken) + + var attrFileName object.Attribute + attrFileName.SetKey(object.AttributeFileName) + attrFileName.SetValue(fileName) + + obj := object.New() + obj.SetContainerID(cnrID) + obj.SetOwnerID(&owner) + obj.SetAttributes(attrFileName) + + storagegroup.WriteToObject(sg, obj) + + var prmPut pool.PrmObjectPut + prmPut.SetHeader(*obj) + prmPut.UseBearer(btoken) + + objID, err := a.pool.PutObject(ctx, prmPut) + if err != nil { + return nil, fmt.Errorf("put object: %w", err) + } + + return objID, nil +} + +func (a *API) getStorageGroupSizeAndHash(ctx context.Context, cnrID cid.ID, btoken bearer.Token, members []oid.ID, needCalcHash bool) (uint64, *checksum.Checksum, error) { + var ( + sgSize uint64 + objHashes [][]byte + addr oid.Address + prm pool.PrmObjectHead + ) + + addr.SetContainer(cnrID) + prm.UseBearer(btoken) + + for _, objID := range members { + addr.SetObject(objID) + prm.SetAddress(addr) + + objInfo, err := a.pool.HeadObject(ctx, prm) + if err != nil { + return 0, nil, fmt.Errorf("chead object from storage group members, id '%s': %w", objID.EncodeToString(), err) + } + + sgSize += objInfo.PayloadSize() + + if needCalcHash { + cs, _ := objInfo.PayloadHomomorphicHash() + objHashes = append(objHashes, cs.Value()) + } + } + + if needCalcHash { + sumHash, err := tz.Concat(objHashes) + if err != nil { + return 0, nil, fmt.Errorf("concat tz hashes: %w", err) + } + + var cs checksum.Checksum + tzHash := [64]byte{} + copy(tzHash[:], sumHash) + cs.SetTillichZemor(tzHash) + + return sgSize, &cs, nil + } + + return sgSize, nil, nil +} + +func (a *API) parseStorageGroupMembers(storageGroup *models.StorageGroup) ([]oid.ID, error) { + var err error + + members := make([]oid.ID, len(storageGroup.Members)) + uniqueFilter := make(map[oid.ID]struct{}, len(members)) + + for i, objIDStr := range storageGroup.Members { + if err = members[i].DecodeString(objIDStr); err != nil { + return nil, fmt.Errorf("invalid object id '%s': %w", objIDStr, err) + } + if _, ok := uniqueFilter[members[i]]; ok { + return nil, fmt.Errorf("invalid storage group members: duplicate id '%s': %w", objIDStr, err) + } + uniqueFilter[members[i]] = struct{}{} + } + + return members, nil +} + +func isHomomorphicHashingDisabled(ctx context.Context, p *pool.Pool, cnrID cid.ID) (bool, error) { + var prm pool.PrmContainerGet + prm.SetContainerID(cnrID) + + cnr, err := p.GetContainer(ctx, prm) + if err != nil { + return false, fmt.Errorf("get container: %w", err) + } + + return container.IsHomomorphicHashingDisabled(*cnr), nil +} diff --git a/spec/rest.yaml b/spec/rest.yaml index 5ddcba8..8e6c603 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -556,6 +556,31 @@ paths: description: Bad request. schema: $ref: '#/definitions/ErrorResponse' + /containers/{containerId}/storagegroups: + parameters: + - $ref: '#/parameters/containerId' + put: + operationId: putStorageGroup + summary: Create a new storage group in container. + parameters: + - $ref: '#/parameters/signatureParam' + - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' + - in: body + name: storageGroup + required: true + description: Storage group co create. + schema: + $ref: '#/definitions/StorageGroup' + responses: + 200: + description: Address of uploaded storage group. + schema: + $ref: '#/definitions/Address' + 400: + description: Bad request. + schema: + $ref: '#/definitions/ErrorResponse' definitions: BinaryBearer: @@ -1030,6 +1055,32 @@ definitions: value: myfile targets: - role: OTHERS + StorageGroup: + description: Storage group keeps verification information for Data Audit sessions. + type: object + properties: + name: + description: Name of storage group. It will be the value of the `FileName` attribute in storage group object. + type: string + containerId: + description: Container id to which storage group is belong. Set by server. + type: string + readOnly: true + objectId: + description: Object id of storage group. Set by server. + type: string + readOnly: true + lifetime: + description: Lifetime in epochs for storage group. + type: integer + members: + description: Object identifiers to be placed into storage group. Must be unique. + type: array + items: + type: string + required: + - lifetime + - members Attribute: description: Attribute is a pair of strings that can be attached to a container or an object. type: object