[#585] ListBuckets pagination support #591

Merged
alexvanin merged 2 commits from nzinkevich/frostfs-s3-gw:feature/list_buckets_pagination into master 2025-01-21 07:49:20 +00:00
9 changed files with 352 additions and 63 deletions

View file

@ -7,6 +7,7 @@ import (
"encoding/xml" "encoding/xml"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
@ -297,10 +298,17 @@ type createBucketInfo struct {
Key *keys.PrivateKey Key *keys.PrivateKey
} }
nzinkevich marked this conversation as resolved Outdated

Consider params like:

func createBucket(hc *handlerContext, bktName string, locationConstraint string) *createBucketInfo {

or even don't change this function but create new one

func createBucketWithContraint(hc *handlerContext, bktName string, locationConstraint string) *createBucketInfo {
Consider params like: ```golang func createBucket(hc *handlerContext, bktName string, locationConstraint string) *createBucketInfo { ``` or even don't change this function but create new one ```golang func createBucketWithContraint(hc *handlerContext, bktName string, locationConstraint string) *createBucketInfo { ```
type bucketPrm struct {
bktName string
query url.Values
box *accessbox.Box
createParams createBucketParams
}
func createBucket(hc *handlerContext, bktName string) *createBucketInfo { func createBucket(hc *handlerContext, bktName string) *createBucketInfo {
box, key := createAccessBox(hc.t) box, key := createAccessBox(hc.t)
w := createBucketBase(hc, bktName, box) w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box})
assertStatus(hc.t, w, http.StatusOK) assertStatus(hc.t, w, http.StatusOK)
bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName)
@ -314,13 +322,32 @@ func createBucket(hc *handlerContext, bktName string) *createBucketInfo {
} }
func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code apierr.ErrorCode) { func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code apierr.ErrorCode) {
w := createBucketBase(hc, bktName, box) w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box})
assertS3Error(hc.t, w, apierr.GetAPIError(code)) assertS3Error(hc.t, w, apierr.GetAPIError(code))
} }
func createBucketBase(hc *handlerContext, bktName string, box *accessbox.Box) *httptest.ResponseRecorder { func createBucketWithConstraint(hc *handlerContext, bktName, constraint string) *createBucketInfo {
w, r := prepareTestRequest(hc, bktName, "", nil) box, key := createAccessBox(hc.t)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}) var prm createBucketParams
if constraint != "" {
prm.LocationConstraint = constraint
}
w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box, createParams: prm})
assertStatus(hc.t, w, http.StatusOK)
bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName)
require.NoError(hc.t, err)
return &createBucketInfo{
BktInfo: bktInfo,
Box: box,
Key: key,
}
}
func createBucketBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder {
w, r := prepareTestFullRequest(hc, prm.bktName, "", nil, prm.createParams)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: prm.box})
r = r.WithContext(ctx) r = r.WithContext(ctx)
hc.Handler().CreateBucketHandler(w, r) hc.Handler().CreateBucketHandler(w, r)
return w return w

View file

@ -0,0 +1,70 @@
package handler
import (
"net/http"
"strconv"
"time"
"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"
)
const maxBucketList = 10000
// ListBucketsHandler handles bucket listing requests.
func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqInfo := middleware.GetReqInfo(ctx)
nzinkevich marked this conversation as resolved Outdated

nitpick: Please use

diff --git a/api/handler/bucket_list.go b/api/handler/bucket_list.go
index 1b067a18..35e2ce7e 100644
--- a/api/handler/bucket_list.go
+++ b/api/handler/bucket_list.go
@@ -12,12 +12,8 @@ import (
 
 // ListBucketsHandler handles bucket listing requests.
 func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
-       var (
-               own     user.ID
-               res     *ListBucketsResponse
-               ctx     = r.Context()
-               reqInfo = middleware.GetReqInfo(ctx)
-       )
+       ctx := r.Context()
+       reqInfo := middleware.GetReqInfo(ctx)
 
        params, err := parseListBucketParams(r)
        if err != nil {
@@ -31,11 +27,12 @@ func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
                return
        }
 
+       var own user.ID
        if len(result.Containers) > 0 {
                own = result.Containers[0].Owner
        }
 
-       res = &ListBucketsResponse{
+       res := &ListBucketsResponse{
                Owner: Owner{
                        ID:          own.String(),
                        DisplayName: own.String(),
nitpick: Please use ```diff diff --git a/api/handler/bucket_list.go b/api/handler/bucket_list.go index 1b067a18..35e2ce7e 100644 --- a/api/handler/bucket_list.go +++ b/api/handler/bucket_list.go @@ -12,12 +12,8 @@ import ( // ListBucketsHandler handles bucket listing requests. func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { - var ( - own user.ID - res *ListBucketsResponse - ctx = r.Context() - reqInfo = middleware.GetReqInfo(ctx) - ) + ctx := r.Context() + reqInfo := middleware.GetReqInfo(ctx) params, err := parseListBucketParams(r) if err != nil { @@ -31,11 +27,12 @@ func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { return } + var own user.ID if len(result.Containers) > 0 { own = result.Containers[0].Owner } - res = &ListBucketsResponse{ + res := &ListBucketsResponse{ Owner: Owner{ ID: own.String(), DisplayName: own.String(), ```
params, err := parseListBucketParams(r)
if err != nil {
h.logAndSendError(ctx, w, "failed to parse params", reqInfo, err)
return
}
resp, err := h.obj.ListBuckets(ctx, params)
if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
return
}
if err = middleware.EncodeToResponse(w, encodeListBuckets(reqInfo.User, resp, params)); err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
}
}
func encodeListBuckets(owner string, resp layer.ListBucketsResult, params layer.ListBucketsParams) *ListBucketsResponse {
res := &ListBucketsResponse{
nzinkevich marked this conversation as resolved Outdated

Let's extract this to method/function encodeListbuckets or something like that (similar to object listing handlers).

Let's extract this to method/function `encodeListbuckets` or something like that (similar to object listing handlers).
Owner: Owner{
ID: owner,
DisplayName: owner,
nzinkevich marked this conversation as resolved Outdated

Probably we can use reqInfo.User

Probably we can use `reqInfo.User`
},
ContinuationToken: resp.ContinuationToken,
Prefix: params.Prefix,
}
for _, item := range resp.Containers {
res.Buckets.Buckets = append(res.Buckets.Buckets, Bucket{
Name: item.Name,
CreationDate: item.Created.UTC().Format(time.RFC3339),
BucketRegion: item.LocationConstraint,
})
}
return res
}
func parseListBucketParams(r *http.Request) (prm layer.ListBucketsParams, err error) {
prm.MaxBuckets = maxBucketList
strMaxBuckets := r.URL.Query().Get(middleware.QueryMaxBuckets)
if strMaxBuckets != "" {
if prm.MaxBuckets, err = strconv.Atoi(strMaxBuckets); err != nil || prm.MaxBuckets < 0 {
nzinkevich marked this conversation as resolved Outdated

It's better to use consts.
Probably even from middleware (middleware.QueryMaxBuckets)

It's better to use consts. Probably even from middleware (`middleware.QueryMaxBuckets`)
return layer.ListBucketsParams{}, errors.GetAPIError(errors.ErrInvalidMaxKeys)
}
nzinkevich marked this conversation as resolved Outdated

We must check parsed value for negativeness and add test for that. Otherwise we will get panic, see:

diff --git a/api/handler/bucket_list_test.go b/api/handler/bucket_list_test.go
index b6c700b1..b4fc24e9 100644
--- a/api/handler/bucket_list_test.go
+++ b/api/handler/bucket_list_test.go
@@ -94,7 +94,7 @@ func TestHandler_ListBucketsHandler(t *testing.T) {
 
        t.Run("pagination", func(t *testing.T) {
                t.Run("happy path", func(t *testing.T) {
-                       params := bucketPrm{query: url.Values{"max-buckets": []string{"1"}}}
+                       params := bucketPrm{query: url.Values{"max-buckets": []string{"-1"}}}
                        resp := listBuckets(hc, params)
                        require.Len(t, resp.Buckets.Buckets, 1)
                        require.NotEmpty(t, resp.ContinuationToken)

We must check parsed value for negativeness and add test for that. Otherwise we will get panic, see: ```diff diff --git a/api/handler/bucket_list_test.go b/api/handler/bucket_list_test.go index b6c700b1..b4fc24e9 100644 --- a/api/handler/bucket_list_test.go +++ b/api/handler/bucket_list_test.go @@ -94,7 +94,7 @@ func TestHandler_ListBucketsHandler(t *testing.T) { t.Run("pagination", func(t *testing.T) { t.Run("happy path", func(t *testing.T) { - params := bucketPrm{query: url.Values{"max-buckets": []string{"1"}}} + params := bucketPrm{query: url.Values{"max-buckets": []string{"-1"}}} resp := listBuckets(hc, params) require.Len(t, resp.Buckets.Buckets, 1) require.NotEmpty(t, resp.ContinuationToken) ```
}
prm.Prefix = r.URL.Query().Get(middleware.QueryPrefix)
prm.BucketRegion = r.URL.Query().Get(middleware.QueryBucketRegion)
prm.ContinuationToken = r.URL.Query().Get(middleware.QueryContinuationToken)
return
}

View file

@ -0,0 +1,174 @@
package handler
import (
"encoding/xml"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"testing"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"github.com/stretchr/testify/require"
)
func TestHandler_ListBucketsHandler(t *testing.T) {
const defaultConstraint = "default"
region := "us-west-1"
hc := prepareHandlerContext(t)
hc.config.putLocationConstraint(region)
props := []Bucket{
{Name: "first"},
{Name: "regional", BucketRegion: "us-west-1"},
{Name: "third"},
}
sort.Slice(props, func(i, j int) bool {
return props[i].Name < props[j].Name
})
for _, bkt := range props {
createBucketWithConstraint(hc, bkt.Name, bkt.BucketRegion)
}
for _, tt := range []struct {
title string
token string
prefix string
bucketRegion string
maxBuckets string
expectErr bool
expected []Bucket
expectedToken string
}{
{
title: "no params",
expected: []Bucket{
{Name: "first", BucketRegion: defaultConstraint},
{Name: "regional", BucketRegion: "us-west-1"},
{Name: "third", BucketRegion: defaultConstraint},
},
},
{
title: "negative max-buckets",
maxBuckets: "-1",
expected: []Bucket{},
expectErr: true,
},
{
title: "zero max-buckets",
maxBuckets: "0",
expected: []Bucket{},
},
{
title: "prefix",
prefix: "thi",
expected: []Bucket{{Name: "third", BucketRegion: defaultConstraint}},
},
{
title: "wrong prefix",
prefix: "sdh",
expected: []Bucket{},
},
{
title: "bucket region",
bucketRegion: region,
expected: []Bucket{{Name: "regional", BucketRegion: "us-west-1"}},
},
{
title: "default bucket region",
bucketRegion: defaultConstraint,
expected: []Bucket{
{Name: "first", BucketRegion: defaultConstraint},
{Name: "third", BucketRegion: defaultConstraint},
},
},
{
title: "wrong bucket region",
bucketRegion: "sj dfdlsj",
expected: []Bucket{},
},
} {
t.Run(tt.title, func(t *testing.T) {
if tt.expectErr {
listBucketsErr(hc, tt.prefix, tt.token, tt.bucketRegion, tt.maxBuckets, apierr.GetAPIError(apierr.ErrInvalidMaxKeys))
return
}
resp := listBuckets(hc, tt.prefix, tt.token, tt.bucketRegion, tt.maxBuckets)
require.Len(t, resp.Buckets.Buckets, len(tt.expected))
require.Equal(t, tt.prefix, resp.Prefix)
require.Equal(t, hc.owner.String(), resp.Owner.ID)
if len(resp.Buckets.Buckets) > 0 {
t.Log(resp.Buckets.Buckets[0].Name)
}
for i, bkt := range resp.Buckets.Buckets {
require.Equal(t, tt.expected[i].Name, bkt.Name)
require.Equal(t, tt.expected[i].BucketRegion, bkt.BucketRegion)
}
})
}
t.Run("pagination", func(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
resp := listBuckets(hc, "", "", "", "1")
require.Len(t, resp.Buckets.Buckets, 1)
require.Equal(t, props[0].Name, resp.Buckets.Buckets[0].Name)
require.NotEmpty(t, resp.ContinuationToken)
resp = listBuckets(hc, "", resp.ContinuationToken, "", "1")
require.Len(t, resp.Buckets.Buckets, 1)
require.Equal(t, props[1].Name, resp.Buckets.Buckets[0].Name)
require.NotEmpty(t, resp.ContinuationToken)
nzinkevich marked this conversation as resolved Outdated

Consider using function signature like in listObjectsV1. Or at least create new param (don't reuse bucketPrm since a half of that fields aren't used in this function)

Consider using function signature like in `listObjectsV1`. Or at least create new param (don't reuse `bucketPrm` since a half of that fields aren't used in this function)
resp = listBuckets(hc, "", resp.ContinuationToken, "", "1")
require.Len(t, resp.Buckets.Buckets, 1)
require.Equal(t, props[2].Name, resp.Buckets.Buckets[0].Name)
require.Empty(t, resp.ContinuationToken)
})
t.Run("wrong continuation-token", func(t *testing.T) {
resp := listBuckets(hc, "", "CebuVwfRpdMqi9dvgV2SUNbrkfteGtudchKKhNabXUu9", "", "1")
require.Len(t, resp.Buckets.Buckets, 0)
require.Empty(t, resp.ContinuationToken)
})
})
}
func listBuckets(hc *handlerContext, prefix, token, bucketRegion, maxBuckets string) ListBucketsResponse {
query := url.Values{
middleware.QueryPrefix: []string{prefix},
middleware.QueryContinuationToken: []string{token},
middleware.QueryBucketRegion: []string{bucketRegion},
middleware.QueryMaxBuckets: []string{maxBuckets},
}
w := listBucketsBase(hc, bucketPrm{query: query})
assertStatus(hc.t, w, http.StatusOK)
var resp ListBucketsResponse
err := xml.NewDecoder(w.Body).Decode(&resp)
require.NoError(hc.t, err)
return resp
}
func listBucketsErr(hc *handlerContext, prefix, token, bucketRegion, maxBuckets string, err apierr.Error) {
query := url.Values{
middleware.QueryPrefix: []string{prefix},
middleware.QueryContinuationToken: []string{token},
middleware.QueryBucketRegion: []string{bucketRegion},
middleware.QueryMaxBuckets: []string{maxBuckets},
}
w := listBucketsBase(hc, bucketPrm{query: query})
assertS3Error(hc.t, w, err)
}
func listBucketsBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder {
box, _ := createAccessBox(hc.t)
w, r := prepareTestFullRequest(hc, "", "", prm.query, nil)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().ListBucketsHandler(w, r)
return w
}

View file

@ -74,6 +74,7 @@ func (hc *handlerContextBase) Context() context.Context {
type configMock struct { type configMock struct {
defaultPolicy netmap.PlacementPolicy defaultPolicy netmap.PlacementPolicy
placementPolicies map[string]netmap.PlacementPolicy
copiesNumbers map[string][]uint32 copiesNumbers map[string][]uint32
defaultCopiesNumbers []uint32 defaultCopiesNumbers []uint32
bypassContentEncodingInChunks bool bypassContentEncodingInChunks bool
@ -86,8 +87,9 @@ func (c *configMock) DefaultPlacementPolicy(_ string) netmap.PlacementPolicy {
return c.defaultPolicy return c.defaultPolicy
} }
func (c *configMock) PlacementPolicy(_, _ string) (netmap.PlacementPolicy, bool) { func (c *configMock) PlacementPolicy(_, constraint string) (netmap.PlacementPolicy, bool) {
return netmap.PlacementPolicy{}, false policy, ok := c.placementPolicies[constraint]
return policy, ok
} }
func (c *configMock) CopiesNumbers(_, locationConstraint string) ([]uint32, bool) { func (c *configMock) CopiesNumbers(_, locationConstraint string) ([]uint32, bool) {
@ -151,6 +153,10 @@ func (c *configMock) TLSTerminationHeader() string {
return c.tlsTerminationHeader return c.tlsTerminationHeader
} }
func (c *configMock) putLocationConstraint(constraint string) {
c.placementPolicies[constraint] = c.defaultPolicy
}
func prepareHandlerContext(t *testing.T) *handlerContext { func prepareHandlerContext(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample())) hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample()))
require.NoError(t, err) require.NoError(t, err)
@ -217,7 +223,8 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
} }
cfg := &configMock{ cfg := &configMock{
defaultPolicy: pp, defaultPolicy: pp,
placementPolicies: make(map[string]netmap.PlacementPolicy),
} }
h := &handler{ h := &handler{
log: log, log: log,

View file

@ -1,49 +0,0 @@
package handler
import (
"net/http"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
)
const maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
// ListBucketsHandler handles bucket listing requests.
func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
var (
own user.ID
res *ListBucketsResponse
ctx = r.Context()
reqInfo = middleware.GetReqInfo(ctx)
)
list, err := h.obj.ListBuckets(ctx)
if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
return
}
if len(list) > 0 {
own = list[0].Owner
}
res = &ListBucketsResponse{
Owner: Owner{
ID: own.String(),
DisplayName: own.String(),
},
}
for _, item := range list {
res.Buckets.Buckets = append(res.Buckets.Buckets, Bucket{
Name: item.Name,
CreationDate: item.Created.UTC().Format(time.RFC3339),
})
}
if err = middleware.EncodeToResponse(w, res); err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
}
}

View file

@ -15,6 +15,8 @@ import (
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
) )
const maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
// ListObjectsV1Handler handles objects listing requests for API version 1. // ListObjectsV1Handler handles objects listing requests for API version 1.
func (h *handler) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) { func (h *handler) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()

View file

@ -15,6 +15,9 @@ type ListBucketsResponse struct {
Buckets struct { Buckets struct {
Buckets []Bucket `xml:"Bucket"` Buckets []Bucket `xml:"Bucket"`
} // Buckets are nested } // Buckets are nested
ContinuationToken string `xml:"ContinuationToken,omitempty"`
Prefix string `xml:"Prefix,omitempty"`
} }
// ListObjectsV1Response -- format for ListObjectsV1 response. // ListObjectsV1Response -- format for ListObjectsV1 response.
@ -51,8 +54,9 @@ type ListObjectsV2Response struct {
// Bucket container for bucket metadata. // Bucket container for bucket metadata.
type Bucket struct { type Bucket struct {
Name string Name string `xml:"Name"`
CreationDate string // time string of format "2006-01-02T15:04:05.000Z" CreationDate string `xml:"CreationDate"` // time string of format "2006-01-02T15:04:05.000Z"
BucketRegion string `xml:"BucketRegion,omitempty"`
} }
// PolicyStatus contains status of bucket policy. // PolicyStatus contains status of bucket policy.

View file

@ -3,7 +3,9 @@ package layer
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "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/data"
@ -76,7 +78,7 @@ func (n *Layer) containerInfo(ctx context.Context, prm frostfs.PrmContainer) (*d
return info, nil return info, nil
} }
func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) { func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams) ([]*data.BucketInfo, error) {
stoken := n.SessionTokenForRead(ctx) stoken := n.SessionTokenForRead(ctx)
prm := frostfs.PrmUserContainers{ prm := frostfs.PrmUserContainers{
@ -102,10 +104,34 @@ func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) {
continue continue
} }
if shouldSkipBucket(info, listParams) {
continue
}
list = append(list, info) list = append(list, info)
} }
nzinkevich marked this conversation as resolved Outdated

It seems before skip bucket we should list all and sort by bucket name

It seems before skip bucket we should list all and sort by bucket name
return list, nil sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
dkirillov marked this conversation as resolved Outdated

Despite that in AWS ContinuationToken is not a real key I suppose we can use real key. Technically the bevavior be the same. So I suggest consider this:

diff --git a/api/layer/container.go b/api/layer/container.go
index 83d312f1..9006f700 100644
--- a/api/layer/container.go
+++ b/api/layer/container.go
@@ -15,7 +15,6 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
-	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
 	"go.uber.org/zap"
 )
 
@@ -104,6 +103,11 @@ func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams)
 			n.reqLogger(ctx).Error(logs.CouldNotFetchContainerInfo, zap.Error(err))
 			continue
 		}
+
+		if shouldSkipBucket(info, listParams) {
+			continue
+		}
+
 		list = append(list, info)
 	}
 
@@ -111,34 +115,18 @@ func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams)
 		return list[i].Name < list[j].Name
 	})
 
-	var tokenCID cid.ID
-	tokenMet := false
-	if listParams.ContinuationToken == "" {
-		// skip token check if empty
-		tokenMet = true
-	} else if err = tokenCID.DecodeString(listParams.ContinuationToken); err != nil {
-		return nil, err
-	}
-
-	k := 0
-	count := len(list)
-	for i := 0; i < count; i++ {
-		if list[k].CID.Equals(tokenCID) {
-			tokenMet = true
-		}
-		if shouldSkipBucket(tokenMet, list[k], listParams) {
-			list = append(list[:k], list[k+1:]...)
+	for i, info := range list {
+		if info.Name != listParams.ContinuationToken && listParams.ContinuationToken != "" {
 			continue
 		}
-		k++
+		return list[i:], nil
 	}
 
-	return list, nil
+	return nil, nil
 }
 
-func shouldSkipBucket(tokenMet bool, info *data.BucketInfo, prm ListBucketsParams) bool {
-	if !tokenMet ||
-		!strings.HasPrefix(info.Name, prm.Prefix) ||
+func shouldSkipBucket(info *data.BucketInfo, prm ListBucketsParams) bool {
+	if !strings.HasPrefix(info.Name, prm.Prefix) ||
 		(prm.BucketRegion != "" && info.LocationConstraint != prm.BucketRegion) {
 		return true
 	}
diff --git a/api/layer/layer.go b/api/layer/layer.go
index 2ff74da1..643461d4 100644
--- a/api/layer/layer.go
+++ b/api/layer/layer.go
@@ -396,7 +396,7 @@ func (n *Layer) ListBuckets(ctx context.Context, params ListBucketsParams) (List
 		return ListBucketsResult{}, err
 	}
 	if params.MaxBuckets != 0 && len(result.Containers) > params.MaxBuckets {
-		result.ContinuationToken = result.Containers[params.MaxBuckets].CID.EncodeToString()
+		result.ContinuationToken = result.Containers[params.MaxBuckets].Name
 		result.Containers = result.Containers[:params.MaxBuckets]
 	}
 

Despite that in AWS `ContinuationToken` is not a real key I suppose we can use real key. Technically the bevavior be the same. So I suggest consider this: ```diff diff --git a/api/layer/container.go b/api/layer/container.go index 83d312f1..9006f700 100644 --- a/api/layer/container.go +++ b/api/layer/container.go @@ -15,7 +15,6 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" - cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "go.uber.org/zap" ) @@ -104,6 +103,11 @@ func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams) n.reqLogger(ctx).Error(logs.CouldNotFetchContainerInfo, zap.Error(err)) continue } + + if shouldSkipBucket(info, listParams) { + continue + } + list = append(list, info) } @@ -111,34 +115,18 @@ func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams) return list[i].Name < list[j].Name }) - var tokenCID cid.ID - tokenMet := false - if listParams.ContinuationToken == "" { - // skip token check if empty - tokenMet = true - } else if err = tokenCID.DecodeString(listParams.ContinuationToken); err != nil { - return nil, err - } - - k := 0 - count := len(list) - for i := 0; i < count; i++ { - if list[k].CID.Equals(tokenCID) { - tokenMet = true - } - if shouldSkipBucket(tokenMet, list[k], listParams) { - list = append(list[:k], list[k+1:]...) + for i, info := range list { + if info.Name != listParams.ContinuationToken && listParams.ContinuationToken != "" { continue } - k++ + return list[i:], nil } - return list, nil + return nil, nil } -func shouldSkipBucket(tokenMet bool, info *data.BucketInfo, prm ListBucketsParams) bool { - if !tokenMet || - !strings.HasPrefix(info.Name, prm.Prefix) || +func shouldSkipBucket(info *data.BucketInfo, prm ListBucketsParams) bool { + if !strings.HasPrefix(info.Name, prm.Prefix) || (prm.BucketRegion != "" && info.LocationConstraint != prm.BucketRegion) { return true } diff --git a/api/layer/layer.go b/api/layer/layer.go index 2ff74da1..643461d4 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -396,7 +396,7 @@ func (n *Layer) ListBuckets(ctx context.Context, params ListBucketsParams) (List return ListBucketsResult{}, err } if params.MaxBuckets != 0 && len(result.Containers) > params.MaxBuckets { - result.ContinuationToken = result.Containers[params.MaxBuckets].CID.EncodeToString() + result.ContinuationToken = result.Containers[params.MaxBuckets].Name result.Containers = result.Containers[:params.MaxBuckets] } ```
for i, info := range list {
if listParams.ContinuationToken != "" && info.Name != listParams.ContinuationToken {
continue
}
return list[i:], nil
}
return nil, nil
}
func shouldSkipBucket(info *data.BucketInfo, prm ListBucketsParams) bool {
if !strings.HasPrefix(info.Name, prm.Prefix) ||
(prm.BucketRegion != "" && info.LocationConstraint != prm.BucketRegion) {
return true
}
return false
} }
func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) { func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {

View file

@ -195,6 +195,18 @@ type (
Encode string Encode string
} }
ListBucketsParams struct {
MaxBuckets int
Prefix string
ContinuationToken string
BucketRegion string
}
ListBucketsResult struct {
Containers []*data.BucketInfo
ContinuationToken string
}
// VersionedObject stores info about objects to delete. // VersionedObject stores info about objects to delete.
VersionedObject struct { VersionedObject struct {
Name string Name string
@ -371,8 +383,24 @@ func (n *Layer) ResolveCID(ctx context.Context, name string) (cid.ID, error) {
// ListBuckets returns all user containers. The name of the bucket is a container // ListBuckets returns all user containers. The name of the bucket is a container
// id. Timestamp is omitted since it is not saved in frostfs container. // id. Timestamp is omitted since it is not saved in frostfs container.
func (n *Layer) ListBuckets(ctx context.Context) ([]*data.BucketInfo, error) { func (n *Layer) ListBuckets(ctx context.Context, params ListBucketsParams) (ListBucketsResult, error) {
return n.containerList(ctx) var result ListBucketsResult
var err error
if params.MaxBuckets == 0 {
nzinkevich marked this conversation as resolved Outdated

Before listing we should check max-buckets. It it's 0 then just return empty result

Before listing we should check max-buckets. It it's 0 then just return empty result
return result, nil
}
result.Containers, err = n.containerList(ctx, params)
if err != nil {
return ListBucketsResult{}, err
}
if len(result.Containers) > params.MaxBuckets {
nzinkevich marked this conversation as resolved Outdated

Can be just

if len(result.Containers) > params.MaxBuckets {
Can be just ``` if len(result.Containers) > params.MaxBuckets { ```
result.ContinuationToken = result.Containers[params.MaxBuckets].Name
result.Containers = result.Containers[:params.MaxBuckets]
}
return result, nil
} }
// GetObject from storage. // GetObject from storage.