-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ft: BKTCLT-22 implement ListObjectVersions
Add the Go binding for ListObjectVersions (`listingType=DelimiterVersions`). An extra option is provided to truncate the result to a specific last marker (useful for metadata migration tasks).
- Loading branch information
1 parent
3d67c95
commit 73572b8
Showing
4 changed files
with
535 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package bucketclient | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/url" | ||
) | ||
|
||
type ListObjectVersionsOption func(*listObjectVersionsOptionSet) error | ||
|
||
func ListObjectVersionsMarkerOption(keyMarker string, versionIdMarker string) ListObjectVersionsOption { | ||
return func(opts *listObjectVersionsOptionSet) error { | ||
opts.keyMarker = keyMarker | ||
opts.versionIdMarker = versionIdMarker | ||
return nil | ||
} | ||
} | ||
|
||
func ListObjectVersionsMaxKeysOption(maxKeys int) ListObjectVersionsOption { | ||
return func(opts *listObjectVersionsOptionSet) 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 | ||
} | ||
} | ||
|
||
// ListObjectVersionsLastMarkerOption option makes the listing behave | ||
// as if the bucket contains no object which key/versionId is strictly | ||
// higher than the pair "lastKeyMarker/lastVersionIdMarker". | ||
// | ||
// Note: this option is not implemented natively by bucketd, hence the | ||
// Go client may truncate the result and adjust the "IsTruncated" | ||
// field accordingly, before returning the truncated response to the | ||
// client. | ||
func ListObjectVersionsLastMarkerOption(lastKeyMarker string, lastVersionIdMarker string) ListObjectVersionsOption { | ||
return func(opts *listObjectVersionsOptionSet) error { | ||
opts.lastKeyMarker = lastKeyMarker | ||
opts.lastVersionIdMarker = lastVersionIdMarker | ||
return nil | ||
} | ||
} | ||
|
||
type ListObjectVersionsEntry struct { | ||
Key string `json:"key"` | ||
VersionId string `json:"versionId"` | ||
Value string `json:"value"` | ||
} | ||
|
||
type ListObjectVersionsResponse struct { | ||
Versions []ListObjectVersionsEntry | ||
CommonPrefixes []string | ||
IsTruncated bool | ||
NextKeyMarker string `json:",omitempty"` | ||
NextVersionIdMarker string `json:",omitempty"` | ||
} | ||
|
||
type listObjectVersionsOptionSet struct { | ||
keyMarker string | ||
versionIdMarker string | ||
maxKeys int | ||
lastKeyMarker string | ||
lastVersionIdMarker string | ||
} | ||
|
||
func parseListObjectVersionsOptions(opts []ListObjectVersionsOption) (listObjectVersionsOptionSet, error) { | ||
parsedOpts := listObjectVersionsOptionSet{ | ||
keyMarker: "", | ||
versionIdMarker: "", | ||
maxKeys: -1, | ||
} | ||
for _, opt := range opts { | ||
err := opt(&parsedOpts) | ||
if err != nil { | ||
return parsedOpts, err | ||
} | ||
} | ||
return parsedOpts, nil | ||
} | ||
|
||
func (client *BucketClient) ListObjectVersions(ctx context.Context, | ||
bucketName string, opts ...ListObjectVersionsOption) (*ListObjectVersionsResponse, error) { | ||
resource := fmt.Sprintf("/default/bucket/%s?listingType=DelimiterVersions", bucketName) | ||
options, err := parseListObjectVersionsOptions(opts) | ||
if err != nil { | ||
return nil, &BucketClientError{ | ||
"ListObjectVersions", "GET", client.Endpoint, resource, 0, "", err, | ||
} | ||
} | ||
if options.keyMarker != "" { | ||
resource += fmt.Sprintf("&keyMarker=%s&versionIdMarker=%s", | ||
url.QueryEscape(options.keyMarker), | ||
url.QueryEscape(options.versionIdMarker)) | ||
} | ||
if options.maxKeys != -1 { | ||
resource += fmt.Sprintf("&maxKeys=%d", options.maxKeys) | ||
} | ||
responseBody, err := client.Request(ctx, "ListObjectVersions", "GET", resource) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var parsedResponse = new(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) | ||
} | ||
return parsedResponse, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package bucketclient_test | ||
|
||
import ( | ||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
|
||
"github.com/jarcoal/httpmock" | ||
|
||
"github.com/scality/bucketclient/go" | ||
) | ||
|
||
var _ = Describe("ListObjectVersions()", func() { | ||
It("returns an empty listing result", func(ctx SpecContext) { | ||
httpmock.RegisterResponder( | ||
"GET", "/default/bucket/my-bucket?listingType=DelimiterVersions", | ||
httpmock.NewStringResponder(200, `{ | ||
"Versions": [], | ||
"CommonPrefixes": [], | ||
"IsTruncated": false | ||
} | ||
`)) | ||
|
||
Expect(client.ListObjectVersions(ctx, "my-bucket")).To(Equal( | ||
&bucketclient.ListObjectVersionsResponse{ | ||
Versions: []bucketclient.ListObjectVersionsEntry{}, | ||
CommonPrefixes: []string{}, | ||
IsTruncated: false, | ||
})) | ||
}) | ||
|
||
It("returns a non-empty listing result with URL-encoded marker, maxKeys and truncation", func(ctx SpecContext) { | ||
httpmock.RegisterResponder( | ||
"GET", "/default/bucket/my-bucket?listingType=DelimiterVersions"+ | ||
"&keyMarker=foo%2Fbar&versionIdMarker=123+4&maxKeys=3", | ||
httpmock.NewStringResponder(200, `{ | ||
"Versions": [ | ||
{"key": "fop", "versionId": "123"}, | ||
{"key": "goo", "versionId": "124"}, | ||
{"key": "hop", "versionId": "125"} | ||
], | ||
"CommonPrefixes": [], | ||
"IsTruncated": true, | ||
"NextKeyMarker": "hop", | ||
"NextVersionIdMarker": "125" | ||
} | ||
`)) | ||
|
||
Expect(client.ListObjectVersions(ctx, "my-bucket", | ||
bucketclient.ListObjectVersionsMarkerOption("foo/bar", "123 4"), | ||
bucketclient.ListObjectVersionsMaxKeysOption(3), | ||
bucketclient.ListObjectVersionsLastMarkerOption("hoo", "126"), | ||
)).To(Equal(&bucketclient.ListObjectVersionsResponse{ | ||
Versions: []bucketclient.ListObjectVersionsEntry{ | ||
bucketclient.ListObjectVersionsEntry{Key: "fop", VersionId: "123"}, | ||
bucketclient.ListObjectVersionsEntry{Key: "goo", VersionId: "124"}, | ||
}, | ||
CommonPrefixes: []string{}, | ||
IsTruncated: false, | ||
})) | ||
}) | ||
|
||
It("returns an error with malformed response", func(ctx SpecContext) { | ||
httpmock.RegisterResponder( | ||
"GET", "/default/bucket/my-bucket?listingType=DelimiterVersions", | ||
httpmock.NewStringResponder(200, "{OOPS"), | ||
) | ||
|
||
_, err := client.ListObjectVersions(ctx, "my-bucket") | ||
Expect(err).To(MatchError(ContainSubstring("malformed response body"))) | ||
}) | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package bucketclient | ||
|
||
import () | ||
|
||
// CompareVersionsListingMarkers is a helper function that returns -1, | ||
// 0, or 1 if the pair keyMarker1/versionIdMarker1 is | ||
// lexicographically, respectively strictly lower, equal, or strictly | ||
// higher than the pair keyMarker2/versionIdMarker2. | ||
func CompareVersionsListingMarkers(keyMarker1 string, versionIdMarker1 string, | ||
keyMarker2 string, versionIdMarker2 string) int { | ||
// if key markers are different, versionId markers are ignored | ||
if keyMarker1 != keyMarker2 { | ||
if keyMarker1 < keyMarker2 { | ||
return -1 | ||
} | ||
return 1 | ||
} | ||
// if key markers are equal, versionId markers are compared | ||
if versionIdMarker1 != versionIdMarker2 { | ||
if versionIdMarker1 < versionIdMarker2 { | ||
return -1 | ||
} | ||
return 1 | ||
} | ||
return 0 | ||
} | ||
|
||
// truncateListObjectVersionsResponse discards entries which | ||
// key/versionId pair is strictly higher than the | ||
// lastKeyMarker/lastVersionIdMarker pair, and may also change the | ||
// IsTruncated attribute. | ||
func truncateListObjectVersionsResponse(listResponse *ListObjectVersionsResponse, | ||
lastKeyMarker string, lastVersionIdMarker string) { | ||
if listResponse.IsTruncated { | ||
cmp := CompareVersionsListingMarkers( | ||
listResponse.NextKeyMarker, listResponse.NextVersionIdMarker, | ||
lastKeyMarker, lastVersionIdMarker) | ||
if cmp < 0 { | ||
return | ||
} | ||
} | ||
var i int | ||
for i = len(listResponse.Versions) - 1; i >= 0; i -= 1 { | ||
cmp := CompareVersionsListingMarkers( | ||
listResponse.Versions[i].Key, listResponse.Versions[i].VersionId, | ||
lastKeyMarker, lastVersionIdMarker) | ||
if cmp <= 0 { | ||
break | ||
} | ||
} | ||
listResponse.IsTruncated = false | ||
listResponse.NextKeyMarker = "" | ||
listResponse.NextVersionIdMarker = "" | ||
if i+1 < len(listResponse.Versions) { | ||
listResponse.Versions = listResponse.Versions[0 : i+1] | ||
} | ||
} |
Oops, something went wrong.