feat: adds delimiter support in ListMultipartUploads

Fixes #1792
Fixes #1747
Fixes #1797
Fixes #1799

This PR primarily introduces delimiter support and several bug fixes for the `ListMultipartUploads` action in the POSIX and Azure backends. Delimiter handling is now implemented — when a delimiter is present in multipart-upload object key names, the backend collects and returns the appropriate common prefixes.
This functionality is achieved by introducing a common multipart-upload lister in the backend package. All backends (Azure, POSIX) now use this lister. The lister accepts a list that is already sorted and filtered by `KeyMarker` and `Prefix`.

Previously, the `KeyMarker` was required to exactly match an existing multipart-upload object key. This restriction is removed. The listing now relies on a lexicographical comparison between the provided `KeyMarker` and existing multipart-upload object keys.

Validation for `UploadIdMarker` is also added to correctly return an `InvalidArgument` error for invalid upload IDs. If `KeyMarker` is missing, the `UploadIdMarker` is ignored entirely. If `KeyMarker` is provided, a valid upload ID is one that matches an upload belonging to *the first object key after the KeyMarker*. For example, if the `KeyMarker` is `foo`, but the provided `UploadIdMarker` corresponds to an upload under `quxx`, it is invalid. It must match one of the uploads for the next object key equal to `foo`.

Finally, this PR fixes multipart-upload sorting. Multipart uploads must be sorted primarily lexicographically by their object key, and secondarily—when multiple uploads share the same object key—by their initiation time in ascending order.
This commit is contained in:
niksis02
2026-02-06 14:16:16 +04:00
parent db12fb94a3
commit 2e6794007c
8 changed files with 691 additions and 220 deletions
+45 -58
View File
@@ -1355,25 +1355,41 @@ func (az *Azure) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3res
// Lists all the multipart uploads initiated with .sgwtmp/multipart prefix
func (az *Azure) ListMultipartUploads(ctx context.Context, input *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
client, err := az.getContainerClient(*input.Bucket)
var bucket string
if input.Bucket != nil {
bucket = *input.Bucket
}
client, err := az.getContainerClient(bucket)
if err != nil {
return s3response.ListMultipartUploadsResult{}, err
}
uploads := []s3response.Upload{}
var delimiter string
if input.Delimiter != nil {
delimiter = *input.Delimiter
}
var prefix string
if input.Prefix != nil {
prefix = *input.Prefix
}
var keyMarker string
if input.KeyMarker != nil {
keyMarker = *input.KeyMarker
}
var uploadIDMarker string
if input.UploadIdMarker != nil {
uploadIDMarker = *input.UploadIdMarker
}
uploadIdMarkerFound := false
prefix := string(metaTmpMultipartPrefix)
maxUploads := int(*input.MaxUploads)
mpPrefix := string(metaTmpMultipartPrefix)
pager := client.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
Include: container.ListBlobsInclude{Metadata: true},
Prefix: &prefix,
Prefix: &mpPrefix,
})
uploads := []s3response.Upload{}
for pager.More() {
resp, err := pager.NextPage(ctx)
if err != nil {
@@ -1384,10 +1400,10 @@ func (az *Azure) ListMultipartUploads(ctx context.Context, input *s3.ListMultipa
if !ok {
continue
}
if *key <= *input.KeyMarker {
if keyMarker != "" && *key <= keyMarker {
continue
}
if input.Prefix != nil && !strings.HasPrefix(*key, *input.Prefix) {
if prefix != "" && !strings.HasPrefix(*key, prefix) {
continue
}
@@ -1403,62 +1419,33 @@ func (az *Azure) ListMultipartUploads(ctx context.Context, input *s3.ListMultipa
})
}
}
maxUploads := 1000
if input.MaxUploads != nil {
maxUploads = int(*input.MaxUploads)
}
if *input.KeyMarker != "" && uploadIDMarker != "" && !uploadIdMarkerFound {
return s3response.ListMultipartUploadsResult{
Bucket: *input.Bucket,
Delimiter: *input.Delimiter,
KeyMarker: *input.KeyMarker,
MaxUploads: maxUploads,
Prefix: *input.Prefix,
UploadIDMarker: *input.UploadIdMarker,
Uploads: []s3response.Upload{},
}, nil
}
// Sort once: Key asc, Initiated asc
sort.SliceStable(uploads, func(i, j int) bool {
return uploads[i].Key < uploads[j].Key
if uploads[i].Key != uploads[j].Key {
return uploads[i].Key < uploads[j].Key
}
return uploads[i].Initiated.Before(uploads[j].Initiated)
})
if *input.KeyMarker != "" && *input.UploadIdMarker != "" {
// the uploads are already filtered by keymarker
// filter the uploads by uploadIdMarker
for i, upl := range uploads {
if upl.UploadID == uploadIDMarker {
uploads = uploads[i+1:]
break
}
}
result, err := backend.ListMultipartUploads(uploads, prefix, delimiter, keyMarker, uploadIDMarker, maxUploads)
if err != nil {
return s3response.ListMultipartUploadsResult{}, err
}
if len(uploads) <= maxUploads {
return s3response.ListMultipartUploadsResult{
Bucket: *input.Bucket,
Delimiter: *input.Delimiter,
KeyMarker: *input.KeyMarker,
MaxUploads: maxUploads,
Prefix: *input.Prefix,
UploadIDMarker: *input.UploadIdMarker,
Uploads: uploads,
}, nil
} else {
resUploads := uploads[:maxUploads]
return s3response.ListMultipartUploadsResult{
Bucket: *input.Bucket,
Delimiter: *input.Delimiter,
KeyMarker: *input.KeyMarker,
NextKeyMarker: resUploads[len(resUploads)-1].Key,
MaxUploads: maxUploads,
Prefix: *input.Prefix,
UploadIDMarker: *input.UploadIdMarker,
NextUploadIDMarker: resUploads[len(resUploads)-1].UploadID,
IsTruncated: true,
Uploads: resUploads,
}, nil
}
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: maxUploads,
Prefix: prefix,
NextKeyMarker: result.NextKeyMarker,
NextUploadIDMarker: result.NextUploadIDMarker,
UploadIDMarker: uploadIDMarker,
IsTruncated: result.IsTruncated,
Uploads: result.Uploads,
CommonPrefixes: result.CommonPrefixes,
}, nil
}
// Deletes the block blob with committed/uncommitted blocks
+205
View File
@@ -0,0 +1,205 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package backend
import (
"strings"
"github.com/google/uuid"
"github.com/versity/versitygw/s3err"
"github.com/versity/versitygw/s3response"
)
// ListMultipartUploads initializes a multipart upload lister and calls Run()
func ListMultipartUploads(uploads []s3response.Upload, prefix, delimiter, keyMarker, uploadIdMarker string, maxUploads int) (*ListMultipartUploadsPage, error) {
lister := &MultipartUploadLister{
Uploads: uploads,
Prefix: prefix,
Delimiter: delimiter,
KeyMarker: keyMarker,
UploadIDMarker: uploadIdMarker,
MaxUploads: maxUploads,
}
return lister.Run()
}
// MultipartUploadLister emits a ListMultipartUploads-compatible page from an
// already-sorted, already prefix- and key-marker-filtered upload list.
//
// Assumptions about input Uploads:
// - Sorted by (Key asc, Initiated asc)
// - Filtered by Prefix
// - Filtered to start strictly after key-marker when key-marker was provided.
type MultipartUploadLister struct {
Uploads []s3response.Upload
Prefix string
Delimiter string
MaxUploads int
KeyMarker string
UploadIDMarker string
}
// ListMultipartUploadsPage is the lister output
type ListMultipartUploadsPage struct {
Uploads []s3response.Upload
CommonPrefixes []s3response.CommonPrefix
IsTruncated bool
NextKeyMarker string
NextUploadIDMarker string
}
// Run validates marker constraints, then performs a single-pass list that:
// - collapses uploads into CommonPrefixes when delimiter is set
// - enforces MaxUploads over (Uploads + CommonPrefixes)
// - computes truncation and next markers
func (l *MultipartUploadLister) Run() (*ListMultipartUploadsPage, error) {
out := &ListMultipartUploadsPage{}
var startIndex int
// if upload-id-marker is provided without a corresponding key-marker, ignore it.
uploadIDMarker := l.UploadIDMarker
if l.KeyMarker == "" {
uploadIDMarker = ""
}
if uploadIDMarker != "" {
// any invalid uuid is considered as an invalid uploadIdMarker
_, err := uuid.Parse(uploadIDMarker)
if err != nil {
return nil, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)
}
startIndex = l.findUploadIdMarkerIndex(uploadIDMarker)
if startIndex == -1 {
return nil, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)
}
if startIndex >= len(l.Uploads) {
return out, nil
}
}
// Common prefix uniqueness tracking.
seenCP := make(map[string]struct{})
emitted := 0
var lastKey string
// emitUpload appends a new upload to out.Uplodas
emitUpload := func(up s3response.Upload) bool {
out.Uploads = append(out.Uploads, up)
emitted++
lastKey = up.Key
return emitted == l.MaxUploads
}
// emitCp appends a new common prefix to out.CommonPrefixes
emitCP := func(cpref string) bool {
out.CommonPrefixes = append(out.CommonPrefixes, s3response.CommonPrefix{Prefix: cpref})
emitted++
lastKey = cpref
return emitted == l.MaxUploads
}
for i, up := range l.Uploads[startIndex:] {
if l.Delimiter != "" {
// delimiter check
suffix := strings.TrimPrefix(up.Key, l.Prefix)
before, _, found := strings.Cut(suffix, l.Delimiter)
if found {
cpref := l.Prefix + before + l.Delimiter
if _, ok := seenCP[cpref]; !ok {
seenCP[cpref] = struct{}{}
if emitCP(cpref) {
out.IsTruncated = l.hasMoreAfter(i+1, seenCP)
if out.IsTruncated {
out.NextKeyMarker = lastKey
out.NextUploadIDMarker = up.UploadID
}
return out, nil
}
}
continue
}
}
if emitUpload(up) {
out.IsTruncated = l.hasMoreAfter(i+1, seenCP)
if out.IsTruncated {
out.NextKeyMarker = lastKey
out.NextUploadIDMarker = up.UploadID
}
return out, nil
}
}
return out, nil
}
// findUploadIdMarkerIndex finds the index of given uploadId marker in uploads
// uploadIDMarker must match an upload-id among uploads with the first key after KeyMarker.
// Since caller filtered to Key > KeyMarker and the list is sorted by key/time,
// the first key after KeyMarker is Uploads[0].Key (if any).
// -1 is returned if no uploadId is found
func (l *MultipartUploadLister) findUploadIdMarkerIndex(uploadIDMarker string) int {
if len(l.Uploads) == 0 {
// key-marker provided but nothing after it => upload-id-marker can never be valid.
return -1
}
firstKey := l.Uploads[0].Key
// it must match an upload-id under firstKey only.
// If firstKey has multiple uploads, any of those IDs is valid.
for i, up := range l.Uploads {
if up.Key != firstKey {
// sorted by key, so we're past firstKey group
break
}
if up.UploadID == uploadIDMarker {
// the listing should start from the next index
// to skip the uploadId marker
return i + 1
}
}
return -1
}
// hasMoreAfter checks if there exists at least one more effective item after idx,
// considering delimiter collapse and already-emitted common prefixes.
func (l *MultipartUploadLister) hasMoreAfter(idx int, seenCP map[string]struct{}) bool {
if idx >= len(l.Uploads) {
return false
}
if l.Delimiter == "" {
// any remaining upload would be emitted
return true
}
for i := idx; i < len(l.Uploads); i++ {
up := l.Uploads[i]
suffix := strings.TrimPrefix(up.Key, l.Prefix)
before, _, found := strings.Cut(suffix, l.Delimiter)
if !found {
// would emit an upload
return true
}
cpref := l.Prefix + before + l.Delimiter
if _, ok := seenCP[cpref]; ok {
continue
}
// would emit a new common prefix
return true
}
return false
}
+36 -72
View File
@@ -2180,11 +2180,12 @@ func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUpl
var lmu s3response.ListMultipartUploadsResult
bucket := *mpu.Bucket
var delimiter string
if !p.isBucketValid(bucket) {
return lmu, s3err.GetAPIError(s3err.ErrInvalidBucketName)
}
var delimiter string
if mpu.Delimiter != nil {
delimiter = *mpu.Delimiter
}
@@ -2192,6 +2193,15 @@ func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUpl
if mpu.Prefix != nil {
prefix = *mpu.Prefix
}
var keyMarker string
if mpu.KeyMarker != nil {
keyMarker = *mpu.KeyMarker
}
var uploadIDMarker string
if mpu.UploadIdMarker != nil {
uploadIDMarker = *mpu.UploadIdMarker
}
maxUploads := int(*mpu.MaxUploads)
_, err := os.Stat(bucket)
if errors.Is(err, fs.ErrNotExist) {
@@ -2205,17 +2215,6 @@ func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUpl
objs, _ := os.ReadDir(filepath.Join(bucket, MetaTmpMultipartDir))
var uploads []s3response.Upload
var resultUpds []s3response.Upload
var keyMarker string
if mpu.KeyMarker != nil {
keyMarker = *mpu.KeyMarker
}
var uploadIDMarker string
if mpu.UploadIdMarker != nil {
uploadIDMarker = *mpu.UploadIdMarker
}
keyMarkerInd, uploadIdMarkerFound := -1, false
for _, obj := range objs {
if !obj.IsDir() {
@@ -2227,7 +2226,12 @@ func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUpl
continue
}
objectName := string(b)
if mpu.Prefix != nil && !strings.HasPrefix(objectName, *mpu.Prefix) {
// filter by prefix
if prefix != "" && !strings.HasPrefix(objectName, prefix) {
continue
}
// filter by keyMarker
if keyMarker != "" && objectName <= keyMarker {
continue
}
@@ -2241,22 +2245,12 @@ func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUpl
continue
}
// userMetaData := make(map[string]string)
// upiddir := filepath.Join(bucket, metaTmpMultipartDir, obj.Name(), upid.Name())
// loadUserMetaData(upiddir, userMetaData)
fi, err := upid.Info()
if err != nil {
return lmu, fmt.Errorf("stat %q: %w", upid.Name(), err)
}
uploadID := upid.Name()
if !uploadIdMarkerFound && uploadIDMarker == uploadID {
uploadIdMarkerFound = true
}
if keyMarkerInd == -1 && objectName == keyMarker {
keyMarkerInd = len(uploads)
}
checksum, err := p.retrieveChecksums(nil, bucket, filepath.Join(MetaTmpMultipartDir, obj.Name(), uploadID))
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
@@ -2274,61 +2268,31 @@ func (p *Posix) ListMultipartUploads(_ context.Context, mpu *s3.ListMultipartUpl
}
}
maxUploads := int(*mpu.MaxUploads)
if (uploadIDMarker != "" && !uploadIdMarkerFound) || (keyMarker != "" && keyMarkerInd == -1) {
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: maxUploads,
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: []s3response.Upload{},
}, nil
}
// Sort once: Key asc, Initiated asc
sort.SliceStable(uploads, func(i, j int) bool {
return uploads[i].Key < uploads[j].Key
if uploads[i].Key != uploads[j].Key {
return uploads[i].Key < uploads[j].Key
}
return uploads[i].Initiated.Before(uploads[j].Initiated)
})
start := 0
if keyMarker != "" {
for i, up := range uploads {
if up.Key == keyMarker && (uploadIDMarker == "" ||
up.UploadID == uploadIDMarker) {
// Start after the marker
start = i + 1
break
}
}
}
for i := start; i < len(uploads); i++ {
if len(resultUpds) == maxUploads {
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: maxUploads,
NextKeyMarker: resultUpds[len(resultUpds)-1].Key,
NextUploadIDMarker: resultUpds[len(resultUpds)-1].UploadID,
IsTruncated: true,
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: resultUpds,
}, nil
}
resultUpds = append(resultUpds, uploads[i])
result, err := backend.ListMultipartUploads(uploads, prefix, delimiter, keyMarker, uploadIDMarker, maxUploads)
if err != nil {
return lmu, err
}
return s3response.ListMultipartUploadsResult{
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: maxUploads,
Prefix: prefix,
UploadIDMarker: uploadIDMarker,
Uploads: resultUpds,
Bucket: bucket,
Delimiter: delimiter,
KeyMarker: keyMarker,
MaxUploads: maxUploads,
Prefix: prefix,
NextKeyMarker: result.NextKeyMarker,
NextUploadIDMarker: result.NextUploadIDMarker,
UploadIDMarker: uploadIDMarker,
IsTruncated: result.IsTruncated,
Uploads: result.Uploads,
CommonPrefixes: result.CommonPrefixes,
}, nil
}
+9
View File
@@ -374,6 +374,9 @@ func extractIntTests() (commands []*cli.Command) {
if hostStyle {
opts = append(opts, integration.WithHostStyle())
}
if azureTests {
opts = append(opts, integration.WithAzureMode())
}
s := integration.NewS3Conf(opts...)
err := testFunc(s)
@@ -386,6 +389,12 @@ func extractIntTests() (commands []*cli.Command) {
Destination: &versioningEnabled,
Aliases: []string{"vs"},
},
&cli.BoolFlag{
Name: "azure-test-mode",
Usage: "Skips tests that are not supported by Azure",
Destination: &azureTests,
Aliases: []string{"azure"},
},
},
})
}
+6
View File
@@ -177,6 +177,7 @@ const (
ErrTrailerHeaderNotSupported
ErrBadRequest
ErrMissingUploadId
ErrInvalidUploadIdMarker
ErrNoSuchCORSConfiguration
ErrCORSForbidden
ErrMissingCORSOrigin
@@ -796,6 +797,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "This operation does not accept partNumber without uploadId",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidUploadIdMarker: {
Code: "InvalidArgument",
Description: "Invalid uploadId marker",
HTTPStatusCode: http.StatusBadRequest,
},
ErrNoSuchCORSConfiguration: {
Code: "NoSuchCORSConfiguration",
Description: "The CORS configuration does not exist",
+372 -78
View File
@@ -17,9 +17,13 @@ package integration
import (
"context"
"fmt"
"sort"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/google/uuid"
"github.com/versity/versitygw/s3err"
)
@@ -174,34 +178,6 @@ func ListMultipartUploads_exceeding_max_uploads(s *S3Conf) error {
})
}
func ListMultipartUploads_incorrect_next_key_marker(s *S3Conf) error {
testName := "ListMultipartUploads_incorrect_next_key_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for i := 1; i < 6; i++ {
_, err := createMp(s3client, bucket, fmt.Sprintf("obj%v", i))
if err != nil {
return err
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("wrong_object_key"),
})
cancel()
if err != nil {
return err
}
if len(out.Uploads) != 0 {
return fmt.Errorf("expected empty list of multipart uploads, instead got %v",
out.Uploads)
}
return nil
})
}
func ListMultipartUploads_ignore_upload_id_marker(s *S3Conf) error {
testName := "ListMultipartUploads_ignore_upload_id_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
@@ -226,11 +202,378 @@ func ListMultipartUploads_ignore_upload_id_marker(s *S3Conf) error {
if err != nil {
return err
}
if ok := compareMultipartUploads(out.Uploads, uploads); !ok {
if !compareMultipartUploads(out.Uploads, uploads) {
return fmt.Errorf("expected multipart uploads to be %v, instead got %v",
uploads, out.Uploads)
}
// should ignore invalid uploaId marker
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
UploadIdMarker: getPtr("invalid_uploadId_marker"),
})
cancel()
if err != nil {
return err
}
if !compareMultipartUploads(out.Uploads, uploads) {
return fmt.Errorf("expected multipart uploads to be %v, instead got %v",
uploads, out.Uploads)
}
return nil
})
}
func ListMultipartUploads_invalid_uploadId_marker(s *S3Conf) error {
testName := "ListMultipartUploads_invalid_uploadId_marker"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
uploads := make([]types.MultipartUpload, 0, 5)
for i := range 5 {
out, err := createMp(s3client, bucket, fmt.Sprintf("obj-%v", i))
if err != nil {
return err
}
uploads = append(uploads, types.MultipartUpload{
UploadId: out.UploadId,
Key: out.Key,
StorageClass: types.StorageClassStandard,
})
}
// invalid UUID
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("obj-2"),
UploadIdMarker: getPtr("invalid_uploadId_marker"),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)); err != nil {
return err
}
// valid UUID, but not from the list
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("obj-2"),
UploadIdMarker: getPtr(uuid.New().String()),
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)); err != nil {
return err
}
// uploadId marker and key marker mismatch
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("obj-2"),
UploadIdMarker: uploads[4].UploadId,
})
cancel()
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker))
})
}
func ListMultipartUploads_keyMarker_not_from_list(s *S3Conf) error {
testName := "ListMultipartUploads_keyMarker_not_from_list"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
uploads := make([]types.MultipartUpload, 0, 9)
for _, mp := range []struct {
key string
count int
}{
{"bar", 3},
{"baz", 4},
{"foo", 2},
} {
for range mp.count {
out, err := createMp(s3client, bucket, mp.key)
if err != nil {
return err
}
uploads = append(uploads, types.MultipartUpload{
Key: out.Key,
UploadId: out.UploadId,
StorageClass: types.StorageClassStandard,
})
if s.azureTests {
// add an artificial delay for azure tests
// as azure uploads all these mps with the same
// identical creation time
time.Sleep(time.Second)
}
}
}
// without uploadId marker
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("bat"),
})
cancel()
if err != nil {
return err
}
if !compareMultipartUploads(uploads[3:], out.Uploads) {
return fmt.Errorf("expected the mp list to be %v, instead got %v", uploads[:3], out.Uploads)
}
// should start the listing after the specified uploadId marker
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("bat"),
UploadIdMarker: uploads[4].UploadId,
})
cancel()
if err != nil {
return err
}
if !compareMultipartUploads(uploads[5:], out.Uploads) {
return fmt.Errorf("expected the mp list to be %v, instead got %v", uploads[5:], out.Uploads)
}
return nil
})
}
func ListMultipartUploads_delimiter_truncated(s *S3Conf) error {
testName := "ListMultipartUploads_delimiter_truncated"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
uploads := make([]types.MultipartUpload, 0, 6)
for _, key := range []string{
"abc/something",
"foo/bar/baz",
"foo/quxx",
"xyz/hello",
"zzz/bca",
"some/very/nested/mp/object",
} {
out, err := createMp(s3client, bucket, key)
if err != nil {
return err
}
uploads = append(uploads, types.MultipartUpload{
Key: out.Key,
UploadId: out.UploadId,
StorageClass: types.StorageClassStandard,
})
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
Delimiter: getPtr("/"),
MaxUploads: getPtr(int32(2)),
})
cancel()
if err != nil {
return err
}
if len(out.Uploads) != 0 {
return fmt.Errorf("expected empty uplodas list, instead got %v", out.Uploads)
}
expectedCps := []string{"abc/", "foo/"}
if !comparePrefixes(expectedCps, out.CommonPrefixes) {
return fmt.Errorf("expected the common prefixes to be %v, instead got %v", expectedCps, out.CommonPrefixes)
}
if getString(out.NextKeyMarker) != "foo/" {
return fmt.Errorf("expected the next key marker to be 'foo/', instead got %s", getString(out.NextKeyMarker))
}
if getString(out.NextUploadIdMarker) != getString(uploads[1].UploadId) {
return fmt.Errorf("expected the next upload id marker to be %s, instead got %s", getString(uploads[1].UploadId), getString(out.NextUploadIdMarker))
}
if !*out.IsTruncated {
return fmt.Errorf("expected a truncated response")
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
out2, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
Delimiter: getPtr("/"),
UploadIdMarker: out.NextUploadIdMarker,
KeyMarker: out.NextKeyMarker,
})
cancel()
if err != nil {
return err
}
if len(out2.Uploads) != 0 {
return fmt.Errorf("expected empty uplodas list, instead got %v", out2.Uploads)
}
expectedCps = []string{"foo/", "some/", "xyz/", "zzz/"}
if !comparePrefixes(expectedCps, out2.CommonPrefixes) {
return fmt.Errorf("expected the common prefixes to be %v, instead got %v", expectedCps, out2.CommonPrefixes)
}
if getString(out2.KeyMarker) != "foo/" {
return fmt.Errorf("expected key marker to be 'foo/', instead got %s", getString(out2.KeyMarker))
}
if getString(out2.UploadIdMarker) != getString(uploads[1].UploadId) {
return fmt.Errorf("expected the upload id marker to be %s, instead got %s", getString(uploads[1].UploadId), getString(out2.UploadIdMarker))
}
if getString(out2.NextKeyMarker) != "" {
return fmt.Errorf("expected empty next key marker, instead got %s", getString(out2.NextKeyMarker))
}
if getString(out2.NextUploadIdMarker) != "" {
return fmt.Errorf("expected empty next upload id marker, instead got %s", getString(out2.NextUploadIdMarker))
}
if *out2.IsTruncated {
return fmt.Errorf("expected a non-truncated response")
}
return nil
})
}
func ListMultipartUploads_prefix(s *S3Conf) error {
testName := "ListMultipartUploads_prefix"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
prefix := "foo"
uploads := make([]types.MultipartUpload, 0, 8)
for _, key := range []string{
"abc/something",
"foo/bar/baz",
"foo/quxx",
"hello/world",
"xyz/hello",
"zzz/bca",
"some/very/nested/mp/object",
"foo/xyz",
} {
out, err := createMp(s3client, bucket, key)
if err != nil {
return err
}
if strings.HasPrefix(key, prefix) {
uploads = append(uploads, types.MultipartUpload{
Key: out.Key,
UploadId: out.UploadId,
StorageClass: types.StorageClassStandard,
})
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
Prefix: &prefix,
})
cancel()
if err != nil {
return err
}
if getString(out.Prefix) != prefix {
return fmt.Errorf("expected the prefix to be %s, instead got %s", prefix, getString(out.Prefix))
}
if !compareMultipartUploads(out.Uploads, uploads) {
return fmt.Errorf("expected the uploads list to be %v, instead got %v", uploads, out.Uploads)
}
return nil
})
}
func ListMultipartUploads_both_delimiter_and_prefix(s *S3Conf) error {
testName := "ListMultipartUploads_both_delimiter_and_prefix"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
for _, key := range []string{
"foo/abc/bbb/aaa/c",
"abc/something",
"foo/bar/baz",
"foo/quxx",
"hello/world",
"foo/random/object",
"foo/random/another/object",
"xyz/hello",
"zzz/bca",
"some/very/nested/mp/object",
"foo/xyz",
} {
_, err := createMp(s3client, bucket, key)
if err != nil {
return err
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
Delimiter: getPtr("/"),
Prefix: getPtr("foo/"),
})
cancel()
if err != nil {
return err
}
expectedCps := []string{"foo/abc/", "foo/bar/", "foo/random/"}
if !comparePrefixes(expectedCps, out.CommonPrefixes) {
return fmt.Errorf("expected the common prefixes to be %v, instead got %v", expectedCps, out.CommonPrefixes)
}
return nil
})
}
func ListMultipartUploads_delimiter_no_matches(s *S3Conf) error {
testName := "ListMultipartUploads_delimiter_no_matches"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
uploads := make([]types.MultipartUpload, 0, 8)
for _, key := range []string{
"abc/something",
"foo/bar/baz",
"foo/quxx",
"hello/world",
"xyz/hello",
"zzz/bca",
"some/very/nested/mp/object",
"foo/xyz",
} {
out, err := createMp(s3client, bucket, key)
if err != nil {
return err
}
uploads = append(uploads, types.MultipartUpload{
Key: out.Key,
UploadId: out.UploadId,
StorageClass: types.StorageClassStandard,
})
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
Delimiter: getPtr("delim"),
})
cancel()
if err != nil {
return err
}
sort.SliceStable(uploads, func(i, j int) bool {
return *uploads[i].Key < *uploads[j].Key
})
if !compareMultipartUploads(uploads, out.Uploads) {
return fmt.Errorf("expected the uploads to be %v, instead got %v", uploads, out.Uploads)
}
if len(out.CommonPrefixes) != 0 {
return fmt.Errorf("expected empty common prefixes, instead got %v", out.CommonPrefixes)
}
return nil
})
}
@@ -302,52 +645,3 @@ func ListMultipartUploads_with_checksums(s *S3Conf) error {
return nil
})
}
func ListMultipartUploads_success(s *S3Conf) error {
testName := "ListMultipartUploads_success"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
obj1, obj2 := "my-obj-1", "my-obj-2"
out1, err := createMp(s3client, bucket, obj1)
if err != nil {
return err
}
out2, err := createMp(s3client, bucket, obj2)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
})
cancel()
if err != nil {
return err
}
expected := []types.MultipartUpload{
{
Key: &obj1,
UploadId: out1.UploadId,
StorageClass: types.StorageClassStandard,
},
{
Key: &obj2,
UploadId: out2.UploadId,
StorageClass: types.StorageClassStandard,
},
}
if len(out.Uploads) != 2 {
return fmt.Errorf("expected 2 upload, instead got %v",
len(out.Uploads))
}
if ok := compareMultipartUploads(out.Uploads, expected); !ok {
return fmt.Errorf("expected uploads %v, instead got %v",
expected, out.Uploads)
}
return nil
})
}
+11 -4
View File
@@ -459,13 +459,17 @@ func TestListMultipartUploads(ts *TestState) {
ts.Run(ListMultipartUploads_invalid_max_uploads)
ts.Run(ListMultipartUploads_max_uploads)
ts.Run(ListMultipartUploads_exceeding_max_uploads)
ts.Run(ListMultipartUploads_incorrect_next_key_marker)
ts.Run(ListMultipartUploads_ignore_upload_id_marker)
ts.Run(ListMultipartUploads_invalid_uploadId_marker)
ts.Run(ListMultipartUploads_keyMarker_not_from_list)
ts.Run(ListMultipartUploads_delimiter_truncated)
ts.Run(ListMultipartUploads_prefix)
ts.Run(ListMultipartUploads_both_delimiter_and_prefix)
ts.Run(ListMultipartUploads_delimiter_no_matches)
//TODO: remove the condition after implementing checksums in azure
if !ts.conf.azureTests {
ts.Run(ListMultipartUploads_with_checksums)
}
ts.Run(ListMultipartUploads_success)
}
func TestAbortMultipartUpload(ts *TestState) {
@@ -1473,10 +1477,13 @@ func GetIntTests() IntTests {
"ListMultipartUploads_invalid_max_uploads": ListMultipartUploads_invalid_max_uploads,
"ListMultipartUploads_max_uploads": ListMultipartUploads_max_uploads,
"ListMultipartUploads_exceeding_max_uploads": ListMultipartUploads_exceeding_max_uploads,
"ListMultipartUploads_incorrect_next_key_marker": ListMultipartUploads_incorrect_next_key_marker,
"ListMultipartUploads_ignore_upload_id_marker": ListMultipartUploads_ignore_upload_id_marker,
"ListMultipartUploads_invalid_uploadId_marker": ListMultipartUploads_invalid_uploadId_marker,
"ListMultipartUploads_keyMarker_not_from_list": ListMultipartUploads_keyMarker_not_from_list,
"ListMultipartUploads_delimiter_truncated": ListMultipartUploads_delimiter_truncated,
"ListMultipartUploads_prefix": ListMultipartUploads_prefix,
"ListMultipartUploads_both_delimiter_and_prefix": ListMultipartUploads_both_delimiter_and_prefix,
"ListMultipartUploads_with_checksums": ListMultipartUploads_with_checksums,
"ListMultipartUploads_success": ListMultipartUploads_success,
"AbortMultipartUpload_non_existing_bucket": AbortMultipartUpload_non_existing_bucket,
"AbortMultipartUpload_incorrect_uploadId": AbortMultipartUpload_incorrect_uploadId,
"AbortMultipartUpload_incorrect_object_key": AbortMultipartUpload_incorrect_object_key,
+7 -8
View File
@@ -838,14 +838,13 @@ func comparePrefixes(list1 []string, list2 []types.CommonPrefix) bool {
return false
}
elementMap := make(map[string]bool)
for _, elem := range list1 {
elementMap[elem] = true
}
for _, elem := range list2 {
if _, found := elementMap[*elem.Prefix]; !found {
for i, prefix := range list1 {
if list2[i].Prefix == nil {
fmt.Printf("unexpected nil prefix on index %v", i)
return false
}
if *list2[i].Prefix != prefix {
fmt.Printf("prefix mismatch on index %v: expected %s, got %v", i, prefix, *list2[i].Prefix)
return false
}
}