Skip to content

Commit

Permalink
ft: BKTCLT-31 go client: ListBasic
Browse files Browse the repository at this point in the history
Implement the ListBasic API method, which calls bucketd route
`/default/bucket/{bucketname}?listingType=Basic`.

Additional improvements:

- ListObjectVersions: use references for options

- ListObjectVersions: replace allocation with new() with a local var

- ListObjectVersions and CreateBucket: use url.URL object to create
  the full URL
  • Loading branch information
jonathan-gramain committed Oct 23, 2024
1 parent cf19c4c commit 4c2df87
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 26 deletions.
4 changes: 3 additions & 1 deletion go/createbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ func (client *BucketClient) CreateBucket(ctx context.Context,
if parsedOpts.sessionId > 0 {
query.Set("raftsession", strconv.Itoa(parsedOpts.sessionId))
}
resource += "?" + query.Encode()
u, _ := url.Parse(resource)
u.RawQuery = query.Encode()
resource = u.String()
requestOptions := []RequestOption{
RequestBodyOption(bucketAttributes),
RequestBodyContentTypeOption("application/json"),
Expand Down
153 changes: 153 additions & 0 deletions go/listbasic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package bucketclient

import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
)

type ListBasicOption func(*listBasicOptionSet) error

// ListBasicGTOption only lists keys greater than the given argument
func ListBasicGTOption(gt string) ListBasicOption {
return func(opts *listBasicOptionSet) error {
opts.gt = &gt
return nil
}
}

// ListBasicGTEOption only lists keys greater or equal to the given argument
func ListBasicGTEOption(gte string) ListBasicOption {
return func(opts *listBasicOptionSet) error {
opts.gte = &gte
return nil
}
}

// ListBasicLTOption only lists keys less than the given argument
func ListBasicLTOption(lt string) ListBasicOption {
return func(opts *listBasicOptionSet) error {
opts.lt = &lt
return nil
}
}

// ListBasicLTEOption only lists keys less or equal to the given argument
func ListBasicLTEOption(lte string) ListBasicOption {
return func(opts *listBasicOptionSet) error {
opts.lte = &lte
return nil
}
}

// ListBasicMaxKeysOption limits the number of returned keys (default and maximum is 10000).
func ListBasicMaxKeysOption(maxKeys int) ListBasicOption {
return func(opts *listBasicOptionSet) error {
if maxKeys < 0 || maxKeys > 10000 {
return fmt.Errorf("maxKeys=%d is out of the valid range [0, 10000]", maxKeys)
}
opts.maxKeys = &maxKeys
return nil
}
}

// ListBasicNoKeysOption declares that keys are not needed in the
// result entries and may be returned empty.
//
// Note: keys may still be returned until ARSN-438 is fixed.
func ListBasicNoKeysOption() ListBasicOption {
return func(opts *listBasicOptionSet) error {
opts.noKeys = true
return nil
}
}

// ListBasicNoValuesOption declares that values are not needed in the
// result entries and may be returned empty.
//
// Note: values may still be returned until ARSN-438 is fixed.
func ListBasicNoValuesOption() ListBasicOption {
return func(opts *listBasicOptionSet) error {
opts.noValues = true
return nil
}
}

type ListBasicEntry struct {
Key string `json:"key"`
Value string `json:"value"`
}

type ListBasicResponse []ListBasicEntry

type listBasicOptionSet struct {
gt *string
gte *string
lt *string
lte *string
maxKeys *int
noKeys bool
noValues bool
}

func parseListBasicOptions(opts []ListBasicOption) (listBasicOptionSet, error) {
parsedOpts := listBasicOptionSet{}
for _, opt := range opts {
err := opt(&parsedOpts)
if err != nil {
return parsedOpts, err
}
}
return parsedOpts, nil
}

func (client *BucketClient) ListBasic(ctx context.Context,
bucketName string, opts ...ListBasicOption) (*ListBasicResponse, error) {
resource := fmt.Sprintf("/default/bucket/%s", bucketName)
query := url.Values{}
query.Set("listingType", "Basic")

options, err := parseListBasicOptions(opts)
if err != nil {
return nil, &BucketClientError{
"ListBasic", "GET", client.Endpoint, resource, 0, "", err,
}
}
if options.gt != nil {
query.Set("gt", *options.gt)
}
if options.gte != nil {
query.Set("gte", *options.gte)
}
if options.lt != nil {
query.Set("lt", *options.lt)
}
if options.lte != nil {
query.Set("lte", *options.lte)
}
if options.maxKeys != nil {
query.Set("maxKeys", strconv.Itoa(*options.maxKeys))
}
if options.noKeys {
query.Set("keys", "false")
}
if options.noValues {
query.Set("values", "false")
}
u, _ := url.Parse(resource)
u.RawQuery = query.Encode()
resource = u.String()
responseBody, err := client.Request(ctx, "ListBasic", "GET", resource)
if err != nil {
return nil, err
}
var parsedResponse ListBasicResponse
jsonErr := json.Unmarshal(responseBody, &parsedResponse)
if jsonErr != nil {
return nil, ErrorMalformedResponse("ListBasic", "GET",
client.Endpoint, resource, jsonErr)
}
return &parsedResponse, nil
}
71 changes: 71 additions & 0 deletions go/listbasic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package bucketclient_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/jarcoal/httpmock"

"github.com/scality/bucketclient/go"
)

var _ = Describe("ListBasic()", func() {
It("returns an empty listing result", func(ctx SpecContext) {
httpmock.RegisterResponder(
"GET", "/default/bucket/my-bucket?listingType=Basic",
httpmock.NewStringResponder(200, "[]"))

Expect(client.ListBasic(ctx, "my-bucket")).To(Equal(
&bucketclient.ListBasicResponse{}))
})

It("returns a non-empty listing result with no param", func(ctx SpecContext) {
httpmock.RegisterResponder(
"GET", "/default/bucket/my-bucket?listingType=Basic",
httpmock.NewStringResponder(200, `[
{"key": "fop", "value": "fopvalue"},
{"key": "goo", "value": "goovalue"},
{"key": "hop", "value": "hopvalue"}
]
`))

Expect(client.ListBasic(ctx, "my-bucket")).To(Equal(&bucketclient.ListBasicResponse{
bucketclient.ListBasicEntry{Key: "fop", Value: "fopvalue"},
bucketclient.ListBasicEntry{Key: "goo", Value: "goovalue"},
bucketclient.ListBasicEntry{Key: "hop", Value: "hopvalue"},
}))
})

It("returns a non-empty listing result with URL-encoded gt, lt, maxKeys params, without values", func(ctx SpecContext) {
httpmock.RegisterResponder(
"GET", "/default/bucket/my-bucket?gt=eee%2F123&listingType=Basic&lt=ijk+456&maxKeys=3&values=false",
httpmock.NewStringResponder(200, `[
{"key": "fop"},
{"key": "goo"},
{"key": "hop"}
]
`))

Expect(client.ListBasic(ctx, "my-bucket",
bucketclient.ListBasicGTOption("eee/123"),
bucketclient.ListBasicLTOption("ijk 456"),
bucketclient.ListBasicMaxKeysOption(3),
bucketclient.ListBasicNoValuesOption(),
)).To(Equal(&bucketclient.ListBasicResponse{
bucketclient.ListBasicEntry{Key: "fop"},
bucketclient.ListBasicEntry{Key: "goo"},
bucketclient.ListBasicEntry{Key: "hop"},
}))
})

It("returns an error with malformed response", func(ctx SpecContext) {
httpmock.RegisterResponder(
"GET", "/default/bucket/my-bucket?listingType=Basic",
httpmock.NewStringResponder(200, "[OOPS"),
)

_, err := client.ListBasic(ctx, "my-bucket")
Expect(err).To(MatchError(ContainSubstring("malformed response body")))
})

})
50 changes: 25 additions & 25 deletions go/listobjectversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ type ListObjectVersionsOption func(*listObjectVersionsOptionSet) error

func ListObjectVersionsMarkerOption(keyMarker string, versionIdMarker string) ListObjectVersionsOption {
return func(opts *listObjectVersionsOptionSet) error {
opts.keyMarker = keyMarker
opts.versionIdMarker = versionIdMarker
opts.keyMarker = &keyMarker
opts.versionIdMarker = &versionIdMarker
return nil
}
}
Expand All @@ -23,7 +23,7 @@ func ListObjectVersionsMaxKeysOption(maxKeys int) ListObjectVersionsOption {
if maxKeys < 0 || maxKeys > 10000 {
return fmt.Errorf("maxKeys=%d is out of the valid range [0, 10000]", maxKeys)
}
opts.maxKeys = maxKeys
opts.maxKeys = &maxKeys
return nil
}
}
Expand All @@ -38,8 +38,8 @@ func ListObjectVersionsMaxKeysOption(maxKeys int) ListObjectVersionsOption {
// client.
func ListObjectVersionsLastMarkerOption(lastKeyMarker string, lastVersionIdMarker string) ListObjectVersionsOption {
return func(opts *listObjectVersionsOptionSet) error {
opts.lastKeyMarker = lastKeyMarker
opts.lastVersionIdMarker = lastVersionIdMarker
opts.lastKeyMarker = &lastKeyMarker
opts.lastVersionIdMarker = &lastVersionIdMarker
return nil
}
}
Expand All @@ -59,17 +59,15 @@ type ListObjectVersionsResponse struct {
}

type listObjectVersionsOptionSet struct {
keyMarker string
versionIdMarker string
maxKeys int
lastKeyMarker string
lastVersionIdMarker string
keyMarker *string
versionIdMarker *string
maxKeys *int
lastKeyMarker *string
lastVersionIdMarker *string
}

func parseListObjectVersionsOptions(opts []ListObjectVersionsOption) (listObjectVersionsOptionSet, error) {
parsedOpts := listObjectVersionsOptionSet{
maxKeys: -1,
}
parsedOpts := listObjectVersionsOptionSet{}
for _, opt := range opts {
err := opt(&parsedOpts)
if err != nil {
Expand All @@ -91,27 +89,29 @@ func (client *BucketClient) ListObjectVersions(ctx context.Context,
"ListObjectVersions", "GET", client.Endpoint, resource, 0, "", err,
}
}
if options.keyMarker != "" {
query.Set("keyMarker", options.keyMarker)
query.Set("versionIdMarker", options.versionIdMarker)
if options.keyMarker != nil {
query.Set("keyMarker", *options.keyMarker)
query.Set("versionIdMarker", *options.versionIdMarker)
}
if options.maxKeys != -1 {
query.Set("maxKeys", strconv.Itoa(options.maxKeys))
if options.maxKeys != nil {
query.Set("maxKeys", strconv.Itoa(*options.maxKeys))
}
resource += "?" + query.Encode()
u, _ := url.Parse(resource)
u.RawQuery = query.Encode()
resource = u.String()
responseBody, err := client.Request(ctx, "ListObjectVersions", "GET", resource)
if err != nil {
return nil, err
}
var parsedResponse = new(ListObjectVersionsResponse)
jsonErr := json.Unmarshal(responseBody, parsedResponse)
var parsedResponse ListObjectVersionsResponse
jsonErr := json.Unmarshal(responseBody, &parsedResponse)
if jsonErr != nil {
return nil, ErrorMalformedResponse("ListObjectVersions", "GET",
client.Endpoint, resource, jsonErr)
}
if options.lastKeyMarker != "" {
truncateListObjectVersionsResponse(parsedResponse,
options.lastKeyMarker, options.lastVersionIdMarker)
if options.lastKeyMarker != nil {
truncateListObjectVersionsResponse(&parsedResponse,
*options.lastKeyMarker, *options.lastVersionIdMarker)
}
return parsedResponse, nil
return &parsedResponse, nil
}

0 comments on commit 4c2df87

Please sign in to comment.