forked from TrueCloudLab/frostfs-s3-gw
[#476] Save full parts info to multipart object
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
parent
8731dcfd3c
commit
9dfc7e043f
6 changed files with 112 additions and 23 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue