[#476] Save full parts info to multipart object

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
Denis Kirillov 2022-06-02 18:26:26 +03:00 committed by Alex Vanin
parent 8731dcfd3c
commit 9dfc7e043f
6 changed files with 112 additions and 23 deletions

View file

@ -1,6 +1,7 @@
package data package data
import ( import (
"strconv"
"time" "time"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
@ -67,6 +68,11 @@ type PartInfo struct {
Created time.Time Created time.Time
} }
// ToHeaderString form short part representation to use in S3-Completed-Parts header.
func (p *PartInfo) ToHeaderString() string {
return strconv.Itoa(p.Number) + "-" + strconv.FormatInt(p.Size, 10) + "-" + p.ETag
}
// LockInfo is lock information to create appropriate tree node. // LockInfo is lock information to create appropriate tree node.
type LockInfo struct { type LockInfo struct {
ID uint64 ID uint64

View file

@ -15,28 +15,34 @@ import (
type ( type (
GetObjectAttributesResponse struct { GetObjectAttributesResponse struct {
ETag string `xml:"ETag,omitempty"` ETag string `xml:"ETag,omitempty"`
Checksum *Checksum `xml:"Checksum,omitempty"`
ObjectSize int64 `xml:"ObjectSize,omitempty"` ObjectSize int64 `xml:"ObjectSize,omitempty"`
StorageClass string `xml:"StorageClass,omitempty"` StorageClass string `xml:"StorageClass,omitempty"`
ObjectParts *ObjectParts `xml:"ObjectParts,omitempty"` ObjectParts *ObjectParts `xml:"ObjectParts,omitempty"`
} }
Checksum struct {
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
}
ObjectParts struct { ObjectParts struct {
IsTruncated bool `xml:"IsTruncated,omitempty"` IsTruncated bool `xml:"IsTruncated,omitempty"`
MaxParts int `xml:"MaxParts,omitempty"` MaxParts int `xml:"MaxParts,omitempty"`
NextPartNumberMarker int `xml:"NextPartNumberMarker,omitempty"` NextPartNumberMarker int `xml:"NextPartNumberMarker,omitempty"`
PartNumberMarker int `xml:"PartNumberMarker,omitempty"` PartNumberMarker int `xml:"PartNumberMarker,omitempty"`
Parts []Part `xml:"Part,omitempty"` Parts []Part `xml:"Part,omitempty"`
// Only this field is used.
PartsCount int `xml:"PartsCount,omitempty"` PartsCount int `xml:"PartsCount,omitempty"`
} }
Part struct { Part struct {
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
PartNumber int `xml:"PartNumber,omitempty"` PartNumber int `xml:"PartNumber,omitempty"`
Size int `xml:"Size,omitempty"` Size int `xml:"Size,omitempty"`
} }
GetObjectAttributesArgs struct { GetObjectAttributesArgs struct {
MaxParts int
PartNumberMarker int
Attributes []string Attributes []string
VersionID string VersionID string
Conditional *conditionalArgs Conditional *conditionalArgs
@ -138,7 +144,23 @@ func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, err
res.Attributes = append(res.Attributes, a) res.Attributes = append(res.Attributes, a)
} }
return res, nil 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) (*GetObjectAttributesResponse, error) { func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttributesArgs) (*GetObjectAttributesResponse, error) {
@ -152,8 +174,10 @@ func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttribu
resp.StorageClass = "STANDARD" resp.StorageClass = "STANDARD"
case objectSize: case objectSize:
resp.ObjectSize = info.Size resp.ObjectSize = info.Size
case checksum:
resp.Checksum = &Checksum{ChecksumSHA256: info.HashSum}
case objectParts: case objectParts:
parts, err := formUploadAttributes(info) parts, err := formUploadAttributes(info, p.MaxParts, p.PartNumberMarker)
if err != nil { if err != nil {
return nil, fmt.Errorf("form upload attributes: %w", err) return nil, fmt.Errorf("form upload attributes: %w", err)
} }
@ -166,19 +190,62 @@ func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttribu
return resp, nil return resp, nil
} }
func formUploadAttributes(info *data.ObjectInfo) (*ObjectParts, error) { func formUploadAttributes(info *data.ObjectInfo, maxParts, marker int) (*ObjectParts, error) {
var err error completedParts, ok := info.Headers[layer.UploadCompletedParts]
res := ObjectParts{}
partsCountStr, ok := info.Headers[layer.UploadCompletedPartsCount]
if !ok { if !ok {
return nil, nil return nil, nil
} }
res.PartsCount, err = strconv.Atoi(partsCountStr) partInfos := strings.Split(completedParts, ",")
parts := make([]Part, len(partInfos))
for i, p := range partInfos {
// partInfo[0] -- part number, partInfo[1] -- part size, partInfo[2] -- checksum
partInfo := strings.Split(p, "-")
if len(partInfo) != 3 {
return nil, fmt.Errorf("invalid completed parts header")
}
num, err := strconv.Atoi(partInfo[0])
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid parts count header '%s': %w", partsCountStr, err) return nil, err
}
size, err := strconv.Atoi(partInfo[1])
if err != nil {
return nil, err
}
parts[i] = Part{
PartNumber: num,
Size: size,
ChecksumSHA256: partInfo[2],
}
} }
return &res, nil 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
} }

View file

@ -67,5 +67,8 @@ func TestGetObjectPartsAttributes(t *testing.T) {
result = &GetObjectAttributesResponse{} result = &GetObjectAttributesResponse{}
parseTestResponse(t, w, result) parseTestResponse(t, w, result)
require.NotNil(t, result.ObjectParts) require.NotNil(t, result.ObjectParts)
require.Len(t, result.ObjectParts.Parts, 1)
require.Equal(t, etag, result.ObjectParts.Parts[0].ChecksumSHA256)
require.Equal(t, 8, result.ObjectParts.Parts[0].Size)
require.Equal(t, 1, result.ObjectParts.PartsCount) require.Equal(t, 1, result.ObjectParts.PartsCount)
} }

View file

@ -53,6 +53,8 @@ const (
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date" AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
AmzBypassGovernanceRetention = "X-Amz-Bypass-Governance-Retention" AmzBypassGovernanceRetention = "X-Amz-Bypass-Governance-Retention"
AmzObjectAttributes = "X-Amz-Object-Attributes" AmzObjectAttributes = "X-Amz-Object-Attributes"
AmzMaxParts = "X-Amz-Max-Parts"
AmzPartNumberMarker = "X-Amz-Part-Number-Marker"
ContainerID = "X-Container-Id" ContainerID = "X-Container-Id"

View file

@ -22,7 +22,7 @@ import (
const ( const (
UploadIDAttributeName = "S3-Upload-Id" UploadIDAttributeName = "S3-Upload-Id"
UploadPartNumberAttributeName = "S3-Upload-Part-Number" UploadPartNumberAttributeName = "S3-Upload-Part-Number"
UploadCompletedPartsCount = "S3-Completed-Parts-Count" UploadCompletedParts = "S3-Completed-Parts"
metaPrefix = "meta-" metaPrefix = "meta-"
aclPrefix = "acl-" aclPrefix = "acl-"
@ -337,6 +337,7 @@ func (n *layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
parts := make([]*data.PartInfo, 0, len(p.Parts)) parts := make([]*data.PartInfo, 0, len(p.Parts))
var completedPartsHeader strings.Builder
for i, part := range p.Parts { for i, part := range p.Parts {
partInfo := partsInfo[part.PartNumber] partInfo := partsInfo[part.PartNumber]
if part.ETag != partInfo.ETag { if part.ETag != partInfo.ETag {
@ -347,10 +348,19 @@ func (n *layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
return nil, nil, errors.GetAPIError(errors.ErrEntityTooSmall) return nil, nil, errors.GetAPIError(errors.ErrEntityTooSmall)
} }
parts = append(parts, partInfo) parts = append(parts, partInfo)
partInfoStr := partInfo.ToHeaderString()
if i != len(p.Parts)-1 {
partInfoStr += ","
}
if _, err = completedPartsHeader.WriteString(partInfoStr); err != nil {
return nil, nil, err
}
} }
initMetadata := make(map[string]string, len(multipartInfo.Meta)+1) initMetadata := make(map[string]string, len(multipartInfo.Meta)+1)
initMetadata[UploadCompletedPartsCount] = strconv.Itoa(len(p.Parts)) initMetadata[UploadCompletedParts] = completedPartsHeader.String()
uploadData := &UploadData{ uploadData := &UploadData{
TagSet: make(map[string]string), TagSet: make(map[string]string),
ACLHeaders: make(map[string]string), ACLHeaders: make(map[string]string),

View file

@ -21,8 +21,9 @@ type TreeServiceMock struct {
} }
func (t *TreeServiceMock) GetObjectTaggingAndLock(ctx context.Context, cnrID *cid.ID, objVersion *data.NodeVersion) (map[string]string, *data.LockInfo, error) { func (t *TreeServiceMock) GetObjectTaggingAndLock(ctx context.Context, cnrID *cid.ID, objVersion *data.NodeVersion) (map[string]string, *data.LockInfo, error) {
// TODO implement me // TODO implement object tagging
panic("implement me") lock, err := t.GetLock(ctx, cnrID, objVersion.ID)
return nil, lock, err
} }
func (t *TreeServiceMock) GetObjectTagging(ctx context.Context, cnrID *cid.ID, objVersion *data.NodeVersion) (map[string]string, error) { func (t *TreeServiceMock) GetObjectTagging(ctx context.Context, cnrID *cid.ID, objVersion *data.NodeVersion) (map[string]string, error) {