diff --git a/api/handler/patch.go b/api/handler/patch.go index 277b288b..bc44af40 100644 --- a/api/handler/patch.go +++ b/api/handler/patch.go @@ -1,14 +1,17 @@ package handler import ( + "fmt" "net/http" "strconv" + "strings" "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" + "go.uber.org/zap" ) 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) ) - 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) + byteRange, err := parseByteRange(r.Header.Get(api.ContentRange)) + if err != nil { + h.logAndSendError(w, "could not parse byte range", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(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 } @@ -60,7 +67,7 @@ func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) { return } - if uint64(startByte) > srcSize { + if byteRange.Start > srcSize { h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrBadRequest)) return } @@ -77,13 +84,13 @@ func (h *handler) PatchHandler(w http.ResponseWriter, r *http.Request) { } params := &layer.PatchObjectParams{ - Object: srcObjInfo, - BktInfo: bktInfo, - SrcSize: srcSize, - DstSize: srcSize + size, - Header: metadata, - NewBytes: r.Body, - StartByte: uint64(startByte), + Object: srcObjInfo, + BktInfo: bktInfo, + SrcSize: srcSize, + Header: metadata, + NewBytes: r.Body, + NewBytesSize: size, + Range: byteRange, } 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 } } + +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 +} diff --git a/api/layer/layer.go b/api/layer/layer.go index 9f2687fa..f6bb4e5c 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -169,10 +169,10 @@ type ( Object *data.ObjectInfo BktInfo *data.BucketInfo SrcSize uint64 - DstSize uint64 Header map[string]string NewBytes io.Reader - StartByte uint64 + NewBytesSize uint64 + Range *RangeParams Encryption encryption.Params 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) { - if p.StartByte == 0 || p.StartByte == p.SrcSize { - var r io.Reader - + if p.Range.Start == p.SrcSize { objPayload, err := n.GetObject(ctx, &GetObjectParams{ ObjectInfo: p.Object, 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) } - 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.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{ BktInfo: p.BktInfo, Object: p.Object.Name, - Size: p.DstSize, - Reader: r, + Size: p.SrcSize - 1 - p.Range.End + p.NewBytesSize, + Reader: io.MultiReader(p.NewBytes, objPayload), Header: p.Header, Encryption: p.Encryption, CopiesNumbers: p.CopiesNumbers, @@ -687,7 +714,7 @@ func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.Ex objPayload1, err := n.GetObject(ctx, &GetObjectParams{ ObjectInfo: p.Object, - Range: &RangeParams{Start: 0, End: p.StartByte - 1}, + Range: &RangeParams{Start: 0, End: p.Range.Start - 1}, Versioned: true, BucketInfo: p.BktInfo, 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) } + 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{ 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, BucketInfo: p.BktInfo, Encryption: p.Encryption, @@ -710,7 +749,7 @@ func (n *layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.Ex return n.PutObject(ctx, &PutObjectParams{ BktInfo: p.BktInfo, Object: p.Object.Name, - Size: p.DstSize, + Size: p.SrcSize, Reader: io.MultiReader(objPayload1, p.NewBytes, objPayload2), Header: p.Header, Encryption: p.Encryption,