From a11b6e20ee1e8416a67ba96045dc52449d9f19bd Mon Sep 17 00:00:00 2001 From: Tatiana Nesterenko Date: Mon, 13 May 2024 00:22:57 +0100 Subject: [PATCH] handlers: Add `/new-upload/{cId}`, edit `/get/{cId}/{oId}` Add a new POST request `/new-upload/{cId}` that accepts the `X-Attribute-JSON` header. Also, modify GET and HEAD requests `/get/{cId}/{oId}` to return all custom `X-Attribute-*` attributes in the `X-Attribute-JSON` header. Note: Need to fix the `useJSON` parameter in the `/get/{cId}/{oId}` request. Perhaps pass an extra parameter like `download`? Signed-off-by: Tatiana Nesterenko --- handlers/apiserver/rest-server.gen.go | 314 +++++++++++++++----------- handlers/newObjects.go | 173 ++++++++++++++ handlers/objects.go | 50 +++- handlers/util.go | 30 +++ spec/rest.yaml | 44 ++++ 5 files changed, 475 insertions(+), 136 deletions(-) create mode 100644 handlers/newObjects.go diff --git a/handlers/apiserver/rest-server.gen.go b/handlers/apiserver/rest-server.gen.go index d57ba53..0a8cb7c 100644 --- a/handlers/apiserver/rest-server.gen.go +++ b/handlers/apiserver/rest-server.gen.go @@ -536,6 +536,18 @@ type HeadByAttributeParams struct { Download *string `form:"download,omitempty" json:"download,omitempty"` } +// NewUploadContainerObjectMultipartBody defines parameters for NewUploadContainerObject. +type NewUploadContainerObjectMultipartBody struct { + // Payload The file to upload. If no file is present in this field, any other field name will be accepted, except for an empty one. + Payload *openapi_types.File `json:"payload,omitempty"` +} + +// NewUploadContainerObjectParams defines parameters for NewUploadContainerObject. +type NewUploadContainerObjectParams struct { + // XAttributeJSON All attributes + XAttributeJSON *string `json:"X-Attribute-JSON,omitempty"` +} + // PutObjectParams defines parameters for PutObject. type PutObjectParams struct { // WalletConnect Use wallet connect signature scheme or native NeoFS signature. @@ -630,6 +642,9 @@ type UploadContainerObjectParams struct { // AuthJSONRequestBody defines body for Auth for application/json ContentType. type AuthJSONRequestBody = AuthJSONBody +// NewUploadContainerObjectMultipartRequestBody defines body for NewUploadContainerObject for multipart/form-data ContentType. +type NewUploadContainerObjectMultipartRequestBody NewUploadContainerObjectMultipartBody + // PutObjectJSONRequestBody defines body for PutObject for application/json ContentType. type PutObjectJSONRequestBody = ObjectUpload @@ -707,6 +722,9 @@ type ServerInterface interface { // (OPTIONS /network-info) OptionsNetworkInfo(ctx echo.Context) error + // Upload object to NeoFS + // (POST /new-upload/{containerId}) + NewUploadContainerObject(ctx echo.Context, containerId ContainerId, params NewUploadContainerObjectParams) error // (OPTIONS /objects) OptionsObjectsPut(ctx echo.Context) error @@ -1432,6 +1450,46 @@ func (w *ServerInterfaceWrapper) OptionsNetworkInfo(ctx echo.Context) error { return err } +// NewUploadContainerObject converts echo context to params. +func (w *ServerInterfaceWrapper) NewUploadContainerObject(ctx echo.Context) error { + var err error + // ------------- Path parameter "containerId" ------------- + var containerId ContainerId + + err = runtime.BindStyledParameterWithOptions("simple", "containerId", ctx.Param("containerId"), &containerId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter containerId: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + ctx.Set(CookieAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params NewUploadContainerObjectParams + + headers := ctx.Request().Header + // ------------- Optional header parameter "X-Attribute-JSON" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("X-Attribute-JSON")]; found { + var XAttributeJSON string + n := len(valueList) + if n != 1 { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for X-Attribute-JSON, got %d", n)) + } + + err = runtime.BindStyledParameterWithOptions("simple", "X-Attribute-JSON", valueList[0], &XAttributeJSON, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter X-Attribute-JSON: %s", err)) + } + + params.XAttributeJSON = &XAttributeJSON + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.NewUploadContainerObject(ctx, containerId, params) + return err +} + // OptionsObjectsPut converts echo context to params. func (w *ServerInterfaceWrapper) OptionsObjectsPut(ctx echo.Context) error { var err error @@ -1915,6 +1973,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.OPTIONS(baseURL+"/get_by_attribute/:containerId/:attrKey/:attrVal", wrapper.OptionsByAttribute) router.GET(baseURL+"/network-info", wrapper.GetNetworkInfo) router.OPTIONS(baseURL+"/network-info", wrapper.OptionsNetworkInfo) + router.POST(baseURL+"/new-upload/:containerId", wrapper.NewUploadContainerObject) router.OPTIONS(baseURL+"/objects", wrapper.OptionsObjectsPut) router.PUT(baseURL+"/objects", wrapper.PutObject) router.OPTIONS(baseURL+"/objects/:containerId/search", wrapper.OptionsObjectsSearch) @@ -1930,133 +1989,134 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/handlers/newObjects.go b/handlers/newObjects.go new file mode 100644 index 0000000..96be287 --- /dev/null +++ b/handlers/newObjects.go @@ -0,0 +1,173 @@ +package handlers + +import ( + "fmt" + "io" + "mime/multipart" + "net/http" + "strconv" + "time" + + "github.com/labstack/echo/v4" + "github.com/nspcc-dev/neofs-rest-gw/handlers/apiserver" + "github.com/nspcc-dev/neofs-rest-gw/internal/util" + "github.com/nspcc-dev/neofs-sdk-go/bearer" + "github.com/nspcc-dev/neofs-sdk-go/client" + 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" + "go.uber.org/zap" +) + +// NewUploadContainerObject handler that upload file as object with attributes to NeoFS. +func (a *RestAPI) NewUploadContainerObject(ctx echo.Context, containerID apiserver.ContainerId, params apiserver.NewUploadContainerObjectParams) error { + var ( + header *multipart.FileHeader + file multipart.File + err error + idObj oid.ID + addr oid.Address + btoken *bearer.Token + ) + + var idCnr cid.ID + if err = idCnr.DecodeString(containerID); err != nil { + resp := a.logAndGetErrorResponse("invalid container id", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + principal, err := getPrincipal(ctx) + if err != nil { + return ctx.JSON(http.StatusBadRequest, util.NewErrorResponse(err)) + } + + if principal != "" { + btoken, err = getBearerTokenFromString(principal) + if err != nil { + resp := a.logAndGetErrorResponse("get bearer token", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + } + + if err = ctx.Request().ParseMultipartForm(defaultMaxMemory); err != nil { + resp := a.logAndGetErrorResponse("parse multi form", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + if ctx.Request().MultipartForm == nil { + resp := a.logAndGetErrorResponse("multi form is nil", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + var fileKey string + for fileKey = range ctx.Request().MultipartForm.File { + file, header, err = ctx.Request().FormFile(fileKey) + if err != nil { + resp := a.logAndGetErrorResponse(fmt.Sprintf("get file %q from HTTP request", fileKey), err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + break + } + + if fileKey == "" { + resp := a.logAndGetErrorResponse("no multipart/form file", http.ErrMissingFile) + return ctx.JSON(http.StatusBadRequest, resp) + } + + defer func() { + if file == nil { + return + } + err := file.Close() + a.log.Debug( + "close temporary multipart/form file", + zap.Stringer("address", addr), + zap.String("filename", header.Filename), + zap.Error(err), + ) + }() + + filtered, err := parseAndFilterAttributes(a.log, params.XAttributeJSON) + if err != nil { + resp := a.logAndGetErrorResponse("could not process header", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + if needParseExpiration(filtered) { + epochDuration, err := getEpochDurations(ctx.Request().Context(), a.pool) + if err != nil { + resp := a.logAndGetErrorResponse("could not get epoch durations from network info", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + if err = prepareExpirationHeader(filtered, epochDuration, time.Now()); err != nil { + resp := a.logAndGetErrorResponse("could not parse expiration header", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + } + + attributes := make([]object.Attribute, 0, len(filtered)) + // prepares attributes from filtered headers + for key, val := range filtered { + attribute := object.NewAttribute(key, val) + attributes = append(attributes, *attribute) + } + // sets FileName attribute if it wasn't set from header + if _, ok := filtered[object.AttributeFileName]; !ok { + filename := object.NewAttribute(object.AttributeFileName, header.Filename) + attributes = append(attributes, *filename) + } + // sets Content-Type attribute if it wasn't set from header + if _, ok := filtered[object.AttributeContentType]; !ok { + if contentTypes, ok := header.Header["Content-Type"]; ok && len(contentTypes) > 0 { + contentType := contentTypes[0] + cType := object.NewAttribute(object.AttributeContentType, contentType) + attributes = append(attributes, *cType) + } + } + // sets Timestamp attribute if it wasn't set from header and enabled by settings + if _, ok := filtered[object.AttributeTimestamp]; !ok && a.defaultTimestamp { + timestamp := object.NewAttribute(object.AttributeTimestamp, strconv.FormatInt(time.Now().Unix(), 10)) + attributes = append(attributes, *timestamp) + } + + var obj object.Object + obj.SetContainerID(idCnr) + a.setOwner(&obj, btoken) + obj.SetAttributes(attributes...) + + var prmPutInit client.PrmObjectPutInit + if btoken != nil { + prmPutInit.WithBearerToken(*btoken) + } + + writer, err := a.pool.ObjectPutInit(ctx.Request().Context(), obj, a.signer, prmPutInit) + if err != nil { + resp := a.logAndGetErrorResponse("put object init", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + chunk := make([]byte, a.maxObjectSize) + _, err = io.CopyBuffer(writer, file, chunk) + if err != nil { + resp := a.logAndGetErrorResponse("write", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + if err = writer.Close(); err != nil { + resp := a.logAndGetErrorResponse("writer close", err) + return ctx.JSON(http.StatusBadRequest, resp) + } + + idObj = writer.GetResult().StoredObjectID() + addr.SetObject(idObj) + addr.SetContainer(idCnr) + + var resp apiserver.AddressForUpload + resp.ContainerId = containerID + resp.ObjectId = idObj.String() + + ctx.Response().Header().Set(accessControlAllowOriginHeader, "*") + return ctx.JSON(http.StatusOK, resp) +} diff --git a/handlers/objects.go b/handlers/objects.go index 7c42b3d..31665e1 100644 --- a/handlers/objects.go +++ b/handlers/objects.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "encoding/base64" "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -434,7 +435,7 @@ func (a *RestAPI) getByAddress(ctx echo.Context, addr oid.Address, downloadParam var ( payloadSize = header.PayloadSize() - contentType = a.setAttributes(ctx, payloadSize, addr.Container().String(), addr.Object().String(), header, downloadParam) + contentType = a.setAttributes(ctx, payloadSize, addr.Container().String(), addr.Object().String(), header, downloadParam, true) payload io.ReadCloser = payloadReader ) @@ -515,7 +516,7 @@ func (a *RestAPI) headByAddress(ctx echo.Context, addr oid.Address, downloadPara } payloadSize := header.PayloadSize() - contentType := a.setAttributes(ctx, payloadSize, addr.Container().String(), addr.Object().String(), *header, downloadParam) + contentType := a.setAttributes(ctx, payloadSize, addr.Container().String(), addr.Object().String(), *header, downloadParam, true) if len(contentType) == 0 { if payloadSize > 0 { contentType, _, err = readContentType(payloadSize, func(sz uint64) (io.Reader, error) { @@ -548,11 +549,18 @@ func isNotFoundError(err error) bool { errors.Is(err, apistatus.ErrObjectAlreadyRemoved) } -func (a *RestAPI) setAttributes(ctx echo.Context, payloadSize uint64, cid string, oid string, header object.Object, download *string) string { +func (a *RestAPI) setAttributes(ctx echo.Context, payloadSize uint64, cid string, oid string, header object.Object, download *string, useJSON bool) string { ctx.Response().Header().Set("Content-Length", strconv.FormatUint(payloadSize, 10)) - ctx.Response().Header().Set("X-Container-Id", cid) - ctx.Response().Header().Set("X-Object-Id", oid) - ctx.Response().Header().Set("X-Owner-Id", header.OwnerID().EncodeToString()) + attrJSON := make(map[string]string) + if useJSON { + attrJSON["X-Container-Id"] = cid + attrJSON["X-Object-Id"] = oid + attrJSON["X-Owner-Id"] = header.OwnerID().EncodeToString() + } else { + ctx.Response().Header().Set("X-Container-Id", cid) + ctx.Response().Header().Set("X-Object-Id", oid) + ctx.Response().Header().Set("X-Owner-Id", header.OwnerID().EncodeToString()) + } var ( contentType string @@ -577,7 +585,11 @@ func (a *RestAPI) setAttributes(ctx echo.Context, payloadSize uint64, cid string } } ctx.Response().Header().Set("Content-Disposition", dis+"; filename="+path.Base(val)) - ctx.Response().Header().Set("X-Attribute-FileName", val) + if useJSON { + attrJSON["X-Attribute-FileName"] = val + } else { + ctx.Response().Header().Set("X-Attribute-FileName", val) + } case object.AttributeTimestamp: attrTimestamp, err := strconv.ParseInt(val, 10, 64) if err != nil { @@ -587,7 +599,11 @@ func (a *RestAPI) setAttributes(ctx echo.Context, payloadSize uint64, cid string zap.Error(err)) continue } - ctx.Response().Header().Set("X-Attribute-Timestamp", val) + if useJSON { + attrJSON["X-Attribute-Timestamp"] = val + } else { + ctx.Response().Header().Set("X-Attribute-Timestamp", val) + } ctx.Response().Header().Set("Last-Modified", time.Unix(attrTimestamp, 0).UTC().Format(http.TimeFormat)) case object.AttributeContentType: contentType = val @@ -595,11 +611,27 @@ func (a *RestAPI) setAttributes(ctx echo.Context, payloadSize uint64, cid string if strings.HasPrefix(key, SystemAttributePrefix) { key = systemBackwardTranslator(key) } - ctx.Response().Header().Set(userAttributeHeaderPrefix+key, attr.Value()) + if useJSON { + attrJSON[userAttributeHeaderPrefix+key] = attr.Value() + } else { + ctx.Response().Header().Set(userAttributeHeaderPrefix+key, attr.Value()) + } } } } + if useJSON { + // Marshal the map to a JSON string + s, err := json.Marshal(attrJSON) + if err != nil { + a.log.Info("marshal attributes error", + zap.String("container ID", cid), + zap.String("object ID", oid), + zap.Error(err)) + } + ctx.Response().Header().Set(userAttributeHeaderPrefix+"JSON", string(s)) + } + return contentType } diff --git a/handlers/util.go b/handlers/util.go index a1a756d..0708afa 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "encoding/json" "errors" "fmt" "math" @@ -350,3 +351,32 @@ func processKey(key string) string { } return clearKey } + +func parseAndFilterAttributes(logger *zap.Logger, jsonAttr *string) (map[string]string, error) { + parsed := make(map[string]string) + if jsonAttr == nil { + logger.Debug("JSON attribute pointer is nil") + return parsed, nil + } + + if err := json.Unmarshal([]byte(*jsonAttr), &parsed); err != nil { + return nil, err + } + + result := filterAttributes(logger, parsed) + return result, nil +} + +func filterAttributes(logger *zap.Logger, attributes map[string]string) map[string]string { + result := make(map[string]string) + for key, value := range attributes { + if isValidKeyValue(key, value) { + clearKey := processKey(key) + if clearKey != "" { + result[clearKey] = value + logger.Debug("Added attribute to result object", zap.String("key", clearKey), zap.String("value", value)) + } + } + } + return result +} diff --git a/spec/rest.yaml b/spec/rest.yaml index e48b9d3..00e18c7 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -819,6 +819,50 @@ paths: type: string content: { } security: [ ] + /new-upload/{containerId}: + post: + summary: Upload object to NeoFS + operationId: newUploadContainerObject + parameters: + - $ref: '#/components/parameters/containerId' + - name: X-Attribute-JSON + in: header + description: "All attributes" + schema: + type: string + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + payload: + type: string + description: "The file to upload. If no file is present in this\ + \ field, any other field name will be accepted, except for an\ + \ empty one." + format: binary + responses: + "200": + description: Address of uploaded objects. + headers: + Access-Control-Allow-Origin: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/AddressForUpload' + "400": + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + security: + - { } + - BearerAuth: [ ] + - CookieAuth: [ ] /get_by_attribute/{containerId}/{attrKey}/{attrVal}: get: summary: "Find and get an object (payload and attributes) by a specific attribute.\