Use Content-Range in PATCH

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
This commit is contained in:
Marina Biryukova 2024-06-11 17:48:21 +03:00
parent b2682e49ea
commit 6daa8b4698
2 changed files with 113 additions and 26 deletions

View file

@ -1,14 +1,17 @@
package handler package handler
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "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/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "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/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"go.uber.org/zap"
) )
func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) {
@ -17,10 +20,14 @@ func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) {
reqInfo = middleware.GetReqInfo(ctx) reqInfo = middleware.GetReqInfo(ctx)
) )
startByteStr := reqInfo.URL.Query().Get("startByte") byteRange, err := parseByteRange(r.Header.Get(api.ContentRange))
startByte, err := strconv.ParseInt(startByteStr, 10, 64) if err != nil {
if err != nil || startByte < 0 { h.logAndSendError(w, "could not parse byte range", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(err))
h.logAndSendError(w, "invalid start byte", reqInfo, err) return
}
if uint64(r.ContentLength) != (byteRange.End - byteRange.Start + 1) {
h.logAndSendError(w, "content-length must be equal to byte range length", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
return return
} }
@ -60,7 +67,7 @@ func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if uint64(startByte) > srcSize { if byteRange.Start > srcSize {
h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrBadRequest)) h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
return return
} }
@ -77,13 +84,13 @@ func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) {
} }
params := &layer.PatchObjectParams{ params := &layer.PatchObjectParams{
Object: srcObjInfo, Object: srcObjInfo,
BktInfo: bktInfo, BktInfo: bktInfo,
SrcSize: srcSize, SrcSize: srcSize,
DstSize: srcSize + size, Header: metadata,
Header: metadata, NewBytes: r.Body,
NewBytes: r.Body, NewBytesSize: size,
StartByte: uint64(startByte), Range: byteRange,
} }
params.CopiesNumbers, err = h.pickCopiesNumbers(metadata, reqInfo.Namespace, bktInfo.LocationConstraint) params.CopiesNumbers, err = h.pickCopiesNumbers(metadata, reqInfo.Namespace, bktInfo.LocationConstraint)
@ -106,3 +113,44 @@ func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
func parseByteRange(rangeStr string) (*layer.RangeParams, error) {
const (
prefix = "bytes "
suffix = "/*"
)
if rangeStr == "" {
return nil, fmt.Errorf("empty range")
}
if !strings.HasPrefix(rangeStr, prefix) {
return nil, fmt.Errorf("unknown unit in range header")
}
if !strings.HasSuffix(rangeStr, suffix) {
return nil, fmt.Errorf("invalid size in range header")
}
parts := strings.Split(strings.TrimSuffix(strings.TrimPrefix(rangeStr, prefix), suffix), "-")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid range: %s", rangeStr)
}
start, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid start byte: %s", parts[0])
}
end, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid end byte: %s", parts[1])
}
if start > end {
return nil, fmt.Errorf("start byte is greater than end byte")
}
return &layer.RangeParams{
Start: start,
End: end,
}, nil
}

View file

@ -169,10 +169,10 @@ type (
Object *data.ObjectInfo Object *data.ObjectInfo
BktInfo *data.BucketInfo BktInfo *data.BucketInfo
SrcSize uint64 SrcSize uint64
DstSize uint64
Header map[string]string Header map[string]string
NewBytes io.Reader NewBytes io.Reader
StartByte uint64 NewBytesSize uint64
Range *RangeParams
Encryption encryption.Params Encryption encryption.Params
CopiesNumbers []uint32 CopiesNumbers []uint32
} }
@ -655,9 +655,7 @@ func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.Exte
} }
func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) { func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) {
if p.StartByte == 0 || p.StartByte == p.SrcSize { if p.Range.Start == p.SrcSize {
var r io.Reader
objPayload, err := n.GetObject(ctx, &GetObjectParams{ objPayload, err := n.GetObject(ctx, &GetObjectParams{
ObjectInfo: p.Object, ObjectInfo: p.Object,
Versioned: true, Versioned: true,
@ -668,17 +666,46 @@ func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.Ex
return nil, fmt.Errorf("get object to patch: %w", err) return nil, fmt.Errorf("get object to patch: %w", err)
} }
if p.StartByte == 0 { return n.PutObject(ctx, &PutObjectParams{
r = io.MultiReader(p.NewBytes, objPayload) BktInfo: p.BktInfo,
} else { Object: p.Object.Name,
r = io.MultiReader(objPayload, p.NewBytes) Size: p.SrcSize + p.NewBytesSize,
Reader: io.MultiReader(objPayload, p.NewBytes),
Header: p.Header,
Encryption: p.Encryption,
CopiesNumbers: p.CopiesNumbers,
})
}
if p.Range.Start == 0 {
if p.Range.End >= p.SrcSize-1 {
return n.PutObject(ctx, &PutObjectParams{
BktInfo: p.BktInfo,
Object: p.Object.Name,
Size: p.NewBytesSize,
Reader: p.NewBytes,
Header: p.Header,
Encryption: p.Encryption,
CopiesNumbers: p.CopiesNumbers,
})
}
objPayload, err := n.GetObject(ctx, &GetObjectParams{
ObjectInfo: p.Object,
Range: &RangeParams{Start: p.Range.End + 1, End: p.SrcSize - 1},
Versioned: true,
BucketInfo: p.BktInfo,
Encryption: p.Encryption,
})
if err != nil {
return nil, fmt.Errorf("get object range to patch: %w", err)
} }
return n.PutObject(ctx, &PutObjectParams{ return n.PutObject(ctx, &PutObjectParams{
BktInfo: p.BktInfo, BktInfo: p.BktInfo,
Object: p.Object.Name, Object: p.Object.Name,
Size: p.DstSize, Size: p.SrcSize - 1 - p.Range.End + p.NewBytesSize,
Reader: r, Reader: io.MultiReader(p.NewBytes, objPayload),
Header: p.Header, Header: p.Header,
Encryption: p.Encryption, Encryption: p.Encryption,
CopiesNumbers: p.CopiesNumbers, CopiesNumbers: p.CopiesNumbers,
@ -687,7 +714,7 @@ func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.Ex
objPayload1, err := n.GetObject(ctx, &GetObjectParams{ objPayload1, err := n.GetObject(ctx, &GetObjectParams{
ObjectInfo: p.Object, ObjectInfo: p.Object,
Range: &RangeParams{Start: 0, End: p.StartByte - 1}, Range: &RangeParams{Start: 0, End: p.Range.Start - 1},
Versioned: true, Versioned: true,
BucketInfo: p.BktInfo, BucketInfo: p.BktInfo,
Encryption: p.Encryption, Encryption: p.Encryption,
@ -696,9 +723,21 @@ func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.Ex
return nil, fmt.Errorf("get object range 1 to patch: %w", err) return nil, fmt.Errorf("get object range 1 to patch: %w", err)
} }
if p.Range.End >= p.SrcSize-1 {
return n.PutObject(ctx, &PutObjectParams{
BktInfo: p.BktInfo,
Object: p.Object.Name,
Size: p.Range.Start + p.NewBytesSize,
Reader: io.MultiReader(objPayload1, p.NewBytes),
Header: p.Header,
Encryption: p.Encryption,
CopiesNumbers: p.CopiesNumbers,
})
}
objPayload2, err := n.GetObject(ctx, &GetObjectParams{ objPayload2, err := n.GetObject(ctx, &GetObjectParams{
ObjectInfo: p.Object, ObjectInfo: p.Object,
Range: &RangeParams{Start: p.StartByte, End: p.SrcSize - 1}, Range: &RangeParams{Start: p.Range.End + 1, End: p.SrcSize - 1},
Versioned: true, Versioned: true,
BucketInfo: p.BktInfo, BucketInfo: p.BktInfo,
Encryption: p.Encryption, Encryption: p.Encryption,
@ -710,7 +749,7 @@ func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.Ex
return n.PutObject(ctx, &PutObjectParams{ return n.PutObject(ctx, &PutObjectParams{
BktInfo: p.BktInfo, BktInfo: p.BktInfo,
Object: p.Object.Name, Object: p.Object.Name,
Size: p.DstSize, Size: p.SrcSize,
Reader: io.MultiReader(objPayload1, p.NewBytes, objPayload2), Reader: io.MultiReader(objPayload1, p.NewBytes, objPayload2),
Header: p.Header, Header: p.Header,
Encryption: p.Encryption, Encryption: p.Encryption,