diff --git a/api/handler/patch.go b/api/handler/patch.go new file mode 100644 index 00000000..277b288b --- /dev/null +++ b/api/handler/patch.go @@ -0,0 +1,108 @@ +package handler + +import ( + "net/http" + "strconv" + + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" + "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" +) + +func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + reqInfo = middleware.GetReqInfo(ctx) + ) + + startByteStr := reqInfo.URL.Query().Get("startByte") + startByte, err := strconv.ParseInt(startByteStr, 10, 64) + if err != nil || startByte < 0 { + h.logAndSendError(w, "invalid start byte", reqInfo, err) + return + } + + bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) + if err != nil { + h.logAndSendError(w, "could not get bucket info", reqInfo, err) + return + } + + settings, err := h.obj.GetBucketSettings(ctx, bktInfo) + if err != nil { + h.logAndSendError(w, "could not get bucket settings", reqInfo, err) + return + } + + if !settings.VersioningEnabled() { + h.logAndSendError(w, "could not patch object in unversioned bucket", reqInfo, errors.GetAPIError(errors.ErrBadRequest)) + return + } + + srcObjPrm := &layer.HeadObjectParams{ + Object: reqInfo.ObjectName, + VersionID: reqInfo.URL.Query().Get(api.QueryVersionID), + BktInfo: bktInfo, + } + + extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(ctx, srcObjPrm) + if err != nil { + h.logAndSendError(w, "could not find object", reqInfo, err) + return + } + srcObjInfo := extendedSrcObjInfo.ObjectInfo + + srcSize, err := layer.GetObjectSize(srcObjInfo) + if err != nil { + h.logAndSendError(w, "failed to get source object size", reqInfo, err) + return + } + + if uint64(startByte) > srcSize { + h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrBadRequest)) + return + } + + if len(srcObjInfo.ContentType) > 0 { + srcObjInfo.Headers[api.ContentType] = srcObjInfo.ContentType + } + metadata := makeCopyMap(srcObjInfo.Headers) + filterMetadataMap(metadata) + + var size uint64 + if r.ContentLength > 0 { + size = uint64(r.ContentLength) + } + + params := &layer.PatchObjectParams{ + Object: srcObjInfo, + BktInfo: bktInfo, + SrcSize: srcSize, + DstSize: srcSize + size, + Header: metadata, + NewBytes: r.Body, + StartByte: uint64(startByte), + } + + params.CopiesNumbers, err = h.pickCopiesNumbers(metadata, reqInfo.Namespace, bktInfo.LocationConstraint) + if err != nil { + h.logAndSendError(w, "invalid copies number", reqInfo, err) + return + } + + extendedObjInfo, err := h.obj.PatchObject(ctx, params) + if err != nil { + h.logAndSendError(w, "couldn't patch object", reqInfo, err) + return + } + + w.Header().Set(api.AmzVersionID, extendedObjInfo.ObjectInfo.VersionID()) + w.Header().Set(api.ETag, data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled()))) + + if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil { + h.logAndSendError(w, "write response", reqInfo, err) + return + } +} diff --git a/api/layer/layer.go b/api/layer/layer.go index eae130f4..9f2687fa 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -164,6 +164,19 @@ type ( DstEncryption encryption.Params CopiesNumbers []uint32 } + + PatchObjectParams struct { + Object *data.ObjectInfo + BktInfo *data.BucketInfo + SrcSize uint64 + DstSize uint64 + Header map[string]string + NewBytes io.Reader + StartByte uint64 + Encryption encryption.Params + CopiesNumbers []uint32 + } + // CreateBucketParams stores bucket create request parameters. CreateBucketParams struct { Name string @@ -269,6 +282,8 @@ type ( PutBucketNotificationConfiguration(ctx context.Context, p *PutBucketNotificationConfigurationParams) error GetBucketNotificationConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.NotificationConfiguration, error) + PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) + // Compound methods for optimizations // GetObjectTaggingAndLock unifies GetObjectTagging and GetLock methods in single tree service invocation. @@ -639,6 +654,70 @@ func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.Exte }) } +func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) { + if p.StartByte == 0 || p.StartByte == p.SrcSize { + var r io.Reader + + objPayload, err := n.GetObject(ctx, &GetObjectParams{ + ObjectInfo: p.Object, + Versioned: true, + BucketInfo: p.BktInfo, + Encryption: p.Encryption, + }) + if err != nil { + return nil, fmt.Errorf("get object to patch: %w", err) + } + + if p.StartByte == 0 { + r = io.MultiReader(p.NewBytes, objPayload) + } else { + r = io.MultiReader(objPayload, p.NewBytes) + } + + return n.PutObject(ctx, &PutObjectParams{ + BktInfo: p.BktInfo, + Object: p.Object.Name, + Size: p.DstSize, + Reader: r, + Header: p.Header, + Encryption: p.Encryption, + CopiesNumbers: p.CopiesNumbers, + }) + } + + objPayload1, err := n.GetObject(ctx, &GetObjectParams{ + ObjectInfo: p.Object, + Range: &RangeParams{Start: 0, End: p.StartByte - 1}, + Versioned: true, + BucketInfo: p.BktInfo, + Encryption: p.Encryption, + }) + if err != nil { + return nil, fmt.Errorf("get object range 1 to patch: %w", err) + } + + objPayload2, err := n.GetObject(ctx, &GetObjectParams{ + ObjectInfo: p.Object, + Range: &RangeParams{Start: p.StartByte, End: p.SrcSize - 1}, + Versioned: true, + BucketInfo: p.BktInfo, + Encryption: p.Encryption, + }) + if err != nil { + return nil, fmt.Errorf("get object range 2 to patch: %w", err) + } + + return n.PutObject(ctx, &PutObjectParams{ + BktInfo: p.BktInfo, + Object: p.Object.Name, + Size: p.DstSize, + Reader: io.MultiReader(objPayload1, p.NewBytes, objPayload2), + Header: p.Header, + Encryption: p.Encryption, + CopiesNumbers: p.CopiesNumbers, + }) +} + func getRandomOID() (oid.ID, error) { b := [32]byte{} if _, err := rand.Read(b[:]); err != nil { diff --git a/api/middleware/constants.go b/api/middleware/constants.go index a52b93a8..3f59f8ca 100644 --- a/api/middleware/constants.go +++ b/api/middleware/constants.go @@ -74,6 +74,7 @@ const ( AbortMultipartUploadOperation = "AbortMultipartUpload" DeleteObjectTaggingOperation = "DeleteObjectTagging" DeleteObjectOperation = "DeleteObject" + PatchObjectOperation = "PatchObject" ) const ( diff --git a/api/middleware/policy.go b/api/middleware/policy.go index 5a7142a2..79f7c70c 100644 --- a/api/middleware/policy.go +++ b/api/middleware/policy.go @@ -358,6 +358,8 @@ func determineObjectOperation(r *http.Request) string { switch r.Method { case http.MethodOptions: return OptionsObjectOperation + case http.MethodPatch: + return PatchObjectOperation case http.MethodHead: return HeadObjectOperation case http.MethodGet: diff --git a/api/router.go b/api/router.go index 0f86e2e5..f7d59e0e 100644 --- a/api/router.go +++ b/api/router.go @@ -87,6 +87,7 @@ type ( AbortMultipartUploadHandler(http.ResponseWriter, *http.Request) ListPartsHandler(w http.ResponseWriter, r *http.Request) ListMultipartUploadsHandler(http.ResponseWriter, *http.Request) + PatchHandler(http.ResponseWriter, *http.Request) ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error) ResolveCID(ctx context.Context, bucket string) (cid.ID, error) @@ -376,6 +377,8 @@ func objectRouter(h Handler, l *zap.Logger) chi.Router { objRouter.Head("/*", named(s3middleware.HeadObjectOperation, h.HeadObjectHandler)) + objRouter.Patch("/*", named(s3middleware.PatchObjectOperation, h.PatchHandler)) + // GET method handlers objRouter.Group(func(r chi.Router) { r.Method(http.MethodGet, "/*", NewHandlerFilter(). diff --git a/api/router_mock_test.go b/api/router_mock_test.go index 0b72813a..1691a3a0 100644 --- a/api/router_mock_test.go +++ b/api/router_mock_test.go @@ -540,6 +540,10 @@ func (h *handlerMock) ListMultipartUploadsHandler(w http.ResponseWriter, r *http h.writeResponse(w, res) } +func (h *handlerMock) PatchHandler(http.ResponseWriter, *http.Request) { + panic("implement me") +} + func (h *handlerMock) ResolveBucket(ctx context.Context, name string) (*data.BucketInfo, error) { reqInfo := middleware.GetReqInfo(ctx) bktInfo, ok := h.buckets[reqInfo.Namespace+name]