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" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "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(ctx, w, "missing Content-Range", reqInfo, errors.GetAPIError(errors.ErrMissingContentRange)) return } if _, ok := r.Header[api.ContentLength]; !ok { h.logAndSendError(ctx, w, "missing Content-Length", reqInfo, errors.GetAPIError(errors.ErrMissingContentLength)) return } conditional := parsePatchConditionalHeaders(r.Header, h.reqLogger(ctx)) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err) return } settings, err := h.obj.GetBucketSettings(ctx, bktInfo) if err != nil { h.logAndSendError(ctx, 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(ctx, w, "could not find object", reqInfo, err) return } srcObjInfo := extendedSrcObjInfo.ObjectInfo if err = checkPreconditions(srcObjInfo, conditional, h.cfg.MD5Enabled()); err != nil { h.logAndSendError(ctx, w, "precondition failed", reqInfo, err) return } srcSize, err := layer.GetObjectSize(srcObjInfo) if err != nil { h.logAndSendError(ctx, w, "failed to get source object size", reqInfo, err) return } byteRange, err := parsePatchByteRange(r.Header.Get(api.ContentRange), srcSize) if err != nil { h.logAndSendError(ctx, w, "could not parse byte range", reqInfo, errors.GetAPIError(errors.ErrInvalidRange), zap.Error(err)) return } if maxPatchSize < byteRange.End-byteRange.Start+1 { h.logAndSendError(ctx, 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(ctx, w, "content-length must be equal to byte range length", reqInfo, errors.GetAPIError(errors.ErrInvalidRangeLength)) return } if byteRange.Start > srcSize { h.logAndSendError(ctx, 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(ctx, w, "invalid copies number", reqInfo, err) return } extendedObjInfo, err := h.obj.PatchObject(ctx, params) if err != nil { if isErrObjectLocked(err) { h.logAndSendError(ctx, w, "object is locked", reqInfo, errors.GetAPIError(errors.ErrAccessDenied)) } else { h.logAndSendError(ctx, 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(ctx, w, "could not encode PatchObjectResult to response", reqInfo, err) return } } func parsePatchConditionalHeaders(headers http.Header, log *zap.Logger) *conditionalArgs { args := &conditionalArgs{ IfMatch: data.UnQuote(headers.Get(api.IfMatch)), } if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil { args.IfUnmodifiedSince = httpTime } else { log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err)) } return args } 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 }