forked from TrueCloudLab/frostfs-s3-gw
Add PATCH method for object
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
This commit is contained in:
parent
bb81afc14a
commit
b2682e49ea
6 changed files with 197 additions and 0 deletions
108
api/handler/patch.go
Normal file
108
api/handler/patch.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -164,6 +164,19 @@ type (
|
||||||
DstEncryption encryption.Params
|
DstEncryption encryption.Params
|
||||||
CopiesNumbers []uint32
|
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 stores bucket create request parameters.
|
||||||
CreateBucketParams struct {
|
CreateBucketParams struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -269,6 +282,8 @@ type (
|
||||||
PutBucketNotificationConfiguration(ctx context.Context, p *PutBucketNotificationConfigurationParams) error
|
PutBucketNotificationConfiguration(ctx context.Context, p *PutBucketNotificationConfigurationParams) error
|
||||||
GetBucketNotificationConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.NotificationConfiguration, error)
|
GetBucketNotificationConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.NotificationConfiguration, error)
|
||||||
|
|
||||||
|
PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error)
|
||||||
|
|
||||||
// Compound methods for optimizations
|
// Compound methods for optimizations
|
||||||
|
|
||||||
// GetObjectTaggingAndLock unifies GetObjectTagging and GetLock methods in single tree service invocation.
|
// 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) {
|
func getRandomOID() (oid.ID, error) {
|
||||||
b := [32]byte{}
|
b := [32]byte{}
|
||||||
if _, err := rand.Read(b[:]); err != nil {
|
if _, err := rand.Read(b[:]); err != nil {
|
||||||
|
|
|
@ -74,6 +74,7 @@ const (
|
||||||
AbortMultipartUploadOperation = "AbortMultipartUpload"
|
AbortMultipartUploadOperation = "AbortMultipartUpload"
|
||||||
DeleteObjectTaggingOperation = "DeleteObjectTagging"
|
DeleteObjectTaggingOperation = "DeleteObjectTagging"
|
||||||
DeleteObjectOperation = "DeleteObject"
|
DeleteObjectOperation = "DeleteObject"
|
||||||
|
PatchObjectOperation = "PatchObject"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -358,6 +358,8 @@ func determineObjectOperation(r *http.Request) string {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodOptions:
|
case http.MethodOptions:
|
||||||
return OptionsObjectOperation
|
return OptionsObjectOperation
|
||||||
|
case http.MethodPatch:
|
||||||
|
return PatchObjectOperation
|
||||||
case http.MethodHead:
|
case http.MethodHead:
|
||||||
return HeadObjectOperation
|
return HeadObjectOperation
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
|
|
@ -87,6 +87,7 @@ type (
|
||||||
AbortMultipartUploadHandler(http.ResponseWriter, *http.Request)
|
AbortMultipartUploadHandler(http.ResponseWriter, *http.Request)
|
||||||
ListPartsHandler(w http.ResponseWriter, r *http.Request)
|
ListPartsHandler(w http.ResponseWriter, r *http.Request)
|
||||||
ListMultipartUploadsHandler(http.ResponseWriter, *http.Request)
|
ListMultipartUploadsHandler(http.ResponseWriter, *http.Request)
|
||||||
|
PatchHandler(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error)
|
ResolveBucket(ctx context.Context, bucket string) (*data.BucketInfo, error)
|
||||||
ResolveCID(ctx context.Context, bucket string) (cid.ID, 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.Head("/*", named(s3middleware.HeadObjectOperation, h.HeadObjectHandler))
|
||||||
|
|
||||||
|
objRouter.Patch("/*", named(s3middleware.PatchObjectOperation, h.PatchHandler))
|
||||||
|
|
||||||
// GET method handlers
|
// GET method handlers
|
||||||
objRouter.Group(func(r chi.Router) {
|
objRouter.Group(func(r chi.Router) {
|
||||||
r.Method(http.MethodGet, "/*", NewHandlerFilter().
|
r.Method(http.MethodGet, "/*", NewHandlerFilter().
|
||||||
|
|
|
@ -540,6 +540,10 @@ func (h *handlerMock) ListMultipartUploadsHandler(w http.ResponseWriter, r *http
|
||||||
h.writeResponse(w, res)
|
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) {
|
func (h *handlerMock) ResolveBucket(ctx context.Context, name string) (*data.BucketInfo, error) {
|
||||||
reqInfo := middleware.GetReqInfo(ctx)
|
reqInfo := middleware.GetReqInfo(ctx)
|
||||||
bktInfo, ok := h.buckets[reqInfo.Namespace+name]
|
bktInfo, ok := h.buckets[reqInfo.Namespace+name]
|
||||||
|
|
Loading…
Reference in a new issue