forked from TrueCloudLab/frostfs-s3-gw
195 lines
5.4 KiB
Go
195 lines
5.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
const maxPatchSize = 5 * 1024 * 1024 * 1024 // 5GB
|
|
|
|
func (h *handler) PatchObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
if _, ok := r.Header[api.ContentRange]; !ok {
|
|
h.logAndSendError(w, "missing Content-Range", reqInfo, errors.GetAPIError(errors.ErrMissingContentRange))
|
|
return
|
|
}
|
|
|
|
if _, ok := r.Header[api.ContentLength]; !ok {
|
|
h.logAndSendError(w, "missing Content-Length", reqInfo, errors.GetAPIError(errors.ErrMissingContentLength))
|
|
return
|
|
}
|
|
|
|
conditional, err := parsePatchConditionalHeaders(r.Header)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not parse conditional headers", 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
|
|
}
|
|
|
|
srcObjPrm := &layer.HeadObjectParams{
|
|
Object: reqInfo.ObjectName,
|
|
BktInfo: bktInfo,
|
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
|
}
|
|
|
|
extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(ctx, srcObjPrm)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not find object", reqInfo, err)
|
|
return
|
|
}
|
|
srcObjInfo := extendedSrcObjInfo.ObjectInfo
|
|
|
|
if err = checkPreconditions(srcObjInfo, conditional, h.cfg.MD5Enabled()); err != nil {
|
|
h.logAndSendError(w, "precondition failed", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
srcSize, err := layer.GetObjectSize(srcObjInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "failed to get source object size", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
byteRange, err := parsePatchByteRange(r.Header.Get(api.ContentRange), srcSize)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not parse byte range", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(err))
|
|
return
|
|
}
|
|
|
|
if maxPatchSize < byteRange.End-byteRange.Start+1 {
|
|
h.logAndSendError(w, "byte range length is longer than allowed", 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.ErrInvalidRangeLength))
|
|
return
|
|
}
|
|
|
|
if byteRange.Start > srcSize {
|
|
h.logAndSendError(w, "start byte is greater than object size", reqInfo, errors.GetAPIError(errors.ErrRangeOutOfBounds))
|
|
return
|
|
}
|
|
|
|
params := &layer.PatchObjectParams{
|
|
Object: extendedSrcObjInfo,
|
|
BktInfo: bktInfo,
|
|
NewBytes: r.Body,
|
|
Range: byteRange,
|
|
VersioningEnabled: settings.VersioningEnabled(),
|
|
}
|
|
|
|
params.CopiesNumbers, err = h.pickCopiesNumbers(nil, 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 {
|
|
if isErrObjectLocked(err) {
|
|
h.logAndSendError(w, "object is locked", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
|
|
} else {
|
|
h.logAndSendError(w, "could not patch object", reqInfo, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if settings.VersioningEnabled() {
|
|
w.Header().Set(api.AmzVersionID, extendedObjInfo.ObjectInfo.VersionID())
|
|
}
|
|
|
|
w.Header().Set(api.ETag, data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled())))
|
|
|
|
resp := PatchObjectResult{
|
|
Object: PatchObject{
|
|
LastModified: extendedObjInfo.ObjectInfo.Created.UTC().Format(time.RFC3339),
|
|
ETag: data.Quote(extendedObjInfo.ObjectInfo.ETag(h.cfg.MD5Enabled())),
|
|
},
|
|
}
|
|
|
|
if err = middleware.EncodeToResponse(w, resp); err != nil {
|
|
h.logAndSendError(w, "could not encode PatchObjectResult to response", reqInfo, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func parsePatchConditionalHeaders(headers http.Header) (*conditionalArgs, error) {
|
|
var err error
|
|
args := &conditionalArgs{
|
|
IfMatch: data.UnQuote(headers.Get(api.IfMatch)),
|
|
}
|
|
|
|
if args.IfUnmodifiedSince, err = parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return args, nil
|
|
}
|
|
|
|
func parsePatchByteRange(rangeStr string, objSize uint64) (*layer.RangeParams, error) {
|
|
const prefix = "bytes "
|
|
|
|
if rangeStr == "" {
|
|
return nil, fmt.Errorf("empty range")
|
|
}
|
|
|
|
if !strings.HasPrefix(rangeStr, prefix) {
|
|
return nil, fmt.Errorf("unknown unit in range header")
|
|
}
|
|
|
|
rangeStr, _, found := strings.Cut(strings.TrimPrefix(rangeStr, prefix), "/") // value after / is ignored
|
|
if !found {
|
|
return nil, fmt.Errorf("invalid range: %s", rangeStr)
|
|
}
|
|
|
|
startStr, endStr, found := strings.Cut(rangeStr, "-")
|
|
if !found {
|
|
return nil, fmt.Errorf("invalid range: %s", rangeStr)
|
|
}
|
|
|
|
start, err := strconv.ParseUint(startStr, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid start byte: %s", startStr)
|
|
}
|
|
|
|
end := objSize - 1
|
|
if len(endStr) > 0 {
|
|
end, err = strconv.ParseUint(endStr, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid end byte: %s", endStr)
|
|
}
|
|
}
|
|
|
|
if start > end {
|
|
return nil, fmt.Errorf("start byte is greater than end byte")
|
|
}
|
|
|
|
return &layer.RangeParams{
|
|
Start: start,
|
|
End: end,
|
|
}, nil
|
|
}
|