forked from TrueCloudLab/frostfs-s3-gw
[#585] Add ListBuckets pagination
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
This commit is contained in:
parent
5842f5bad5
commit
65fc776dea
6 changed files with 136 additions and 55 deletions
70
api/handler/bucket_list.go
Normal file
70
api/handler/bucket_list.go
Normal 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)
|
||||||
|
|
||||||
|
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{
|
||||||
|
Owner: Owner{
|
||||||
|
ID: owner,
|
||||||
|
DisplayName: owner,
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
return layer.ListBucketsParams{}, errors.GetAPIError(errors.ErrInvalidMaxKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
return list[i].Name < list[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Containers, err = n.containerList(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return ListBucketsResult{}, err
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
|
Loading…
Reference in a new issue