frostfs-s3-gw/api/handler/attributes.go
Denis Kirillov fafe4af529 [#165] Fix real object size in listing
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-02-02 16:15:03 +03:00

269 lines
7.4 KiB
Go

package handler
import (
"encoding/base64"
"encoding/hex"
"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"
)
type (
GetObjectAttributesResponse struct {
ETag string `xml:"ETag,omitempty"`
Checksum *Checksum `xml:"Checksum,omitempty"`
ObjectSize uint64 `xml:"ObjectSize,omitempty"`
StorageClass string `xml:"StorageClass,omitempty"`
ObjectParts *ObjectParts `xml:"ObjectParts,omitempty"`
}
Checksum struct {
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
}
ObjectParts struct {
IsTruncated bool `xml:"IsTruncated,omitempty"`
MaxParts int `xml:"MaxParts,omitempty"`
NextPartNumberMarker int `xml:"NextPartNumberMarker,omitempty"`
PartNumberMarker int `xml:"PartNumberMarker,omitempty"`
Parts []Part `xml:"Part,omitempty"`
PartsCount int `xml:"PartsCount,omitempty"`
}
Part struct {
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
PartNumber int `xml:"PartNumber,omitempty"`
Size int `xml:"Size,omitempty"`
}
GetObjectAttributesArgs struct {
MaxParts int
PartNumberMarker int
Attributes []string
VersionID string
Conditional *conditionalArgs
}
)
const (
eTag = "ETag"
checksum = "Checksum"
objectParts = "ObjectParts"
storageClass = "StorageClass"
objectSize = "ObjectSize"
)
var validAttributes = map[string]struct{}{
eTag: {},
checksum: {},
objectParts: {},
storageClass: {},
objectSize: {},
}
func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Request) {
reqInfo := middleware.GetReqInfo(r.Context())
params, err := parseGetObjectAttributeArgs(r)
if err != nil {
h.logAndSendError(w, "invalid request", reqInfo, err)
return
}
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
return
}
p := &layer.HeadObjectParams{
BktInfo: bktInfo,
Object: reqInfo.ObjectName,
VersionID: params.VersionID,
}
extendedInfo, err := h.obj.GetExtendedObjectInfo(r.Context(), p)
if err != nil {
h.logAndSendError(w, "could not fetch object info", reqInfo, err)
return
}
info := extendedInfo.ObjectInfo
encryptionParams, err := formEncryptionParams(r)
if err != nil {
h.logAndSendError(w, "invalid sse headers", reqInfo, err)
return
}
if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil {
h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
return
}
if err = checkPreconditions(info, params.Conditional, h.cfg.MD5Enabled()); err != nil {
h.logAndSendError(w, "precondition failed", reqInfo, err)
return
}
bktSettings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
if err != nil {
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
return
}
response, err := encodeToObjectAttributesResponse(info, params, h.cfg.MD5Enabled())
if err != nil {
h.logAndSendError(w, "couldn't encode object info to response", reqInfo, err)
return
}
writeAttributesHeaders(w.Header(), extendedInfo, bktSettings.Unversioned())
if err = middleware.EncodeToResponse(w, response); err != nil {
h.logAndSendError(w, "something went wrong", reqInfo, err)
}
}
func writeAttributesHeaders(h http.Header, info *data.ExtendedObjectInfo, isBucketUnversioned bool) {
h.Set(api.LastModified, info.ObjectInfo.Created.UTC().Format(http.TimeFormat))
if !isBucketUnversioned {
h.Set(api.AmzVersionID, info.Version())
}
if info.NodeVersion.IsDeleteMarker {
h.Set(api.AmzDeleteMarker, strconv.FormatBool(true))
}
// x-amz-request-charged
}
func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, error) {
res := &GetObjectAttributesArgs{
VersionID: r.URL.Query().Get(api.QueryVersionID),
}
attributesVal := r.Header.Get(api.AmzObjectAttributes)
if attributesVal == "" {
return nil, errors.GetAPIError(errors.ErrInvalidAttributeName)
}
attributes := strings.Split(attributesVal, ",")
for _, a := range attributes {
if _, ok := validAttributes[a]; !ok {
return nil, errors.GetAPIError(errors.ErrInvalidAttributeName)
}
res.Attributes = append(res.Attributes, a)
}
var err error
maxPartsVal := r.Header.Get(api.AmzMaxParts)
if maxPartsVal == "" {
res.MaxParts = layer.MaxSizePartsList
} else if res.MaxParts, err = strconv.Atoi(maxPartsVal); err != nil || res.MaxParts < 0 {
return nil, errors.GetAPIError(errors.ErrInvalidMaxKeys)
}
markerVal := r.Header.Get(api.AmzPartNumberMarker)
if markerVal != "" {
if res.PartNumberMarker, err = strconv.Atoi(markerVal); err != nil || res.PartNumberMarker < 0 {
return nil, errors.GetAPIError(errors.ErrInvalidPartNumberMarker)
}
}
res.Conditional, err = parseConditionalHeaders(r.Header)
return res, err
}
func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttributesArgs, md5Enabled bool) (*GetObjectAttributesResponse, error) {
resp := &GetObjectAttributesResponse{}
for _, attr := range p.Attributes {
switch attr {
case eTag:
resp.ETag = data.Quote(info.ETag(md5Enabled))
case storageClass:
resp.StorageClass = api.DefaultStorageClass
case objectSize:
resp.ObjectSize = info.Size
case checksum:
checksumBytes, err := hex.DecodeString(info.HashSum)
if err != nil {
return nil, fmt.Errorf("form upload attributes: %w", err)
}
resp.Checksum = &Checksum{ChecksumSHA256: base64.StdEncoding.EncodeToString(checksumBytes)}
case objectParts:
parts, err := formUploadAttributes(info, p.MaxParts, p.PartNumberMarker)
if err != nil {
return nil, fmt.Errorf("form upload attributes: %w", err)
}
if parts != nil {
resp.ObjectParts = parts
}
}
}
return resp, nil
}
func formUploadAttributes(info *data.ObjectInfo, maxParts, marker int) (*ObjectParts, error) {
completedParts, ok := info.Headers[layer.UploadCompletedParts]
if !ok {
return nil, nil
}
partInfos := strings.Split(completedParts, ",")
parts := make([]Part, len(partInfos))
for i, p := range partInfos {
part, err := layer.ParseCompletedPartHeader(p)
if err != nil {
return nil, fmt.Errorf("invalid completed part: %w", err)
}
// ETag value contains SHA256 checksum.
checksumBytes, err := hex.DecodeString(part.ETag)
if err != nil {
return nil, fmt.Errorf("invalid sha256 checksum in completed part: %w", err)
}
parts[i] = Part{
PartNumber: part.PartNumber,
Size: int(part.Size),
ChecksumSHA256: base64.StdEncoding.EncodeToString(checksumBytes),
}
}
res := &ObjectParts{
PartsCount: len(parts),
}
if marker != 0 {
res.PartNumberMarker = marker
var found bool
for i, n := range parts {
if n.PartNumber == marker {
parts = parts[i:]
found = true
break
}
}
if !found {
return nil, errors.GetAPIError(errors.ErrInvalidPartNumberMarker)
}
}
res.MaxParts = maxParts
if len(parts) > maxParts {
res.IsTruncated = true
res.NextPartNumberMarker = parts[maxParts].PartNumber
parts = parts[:maxParts]
}
res.Parts = parts
return res, nil
}