Skip to content

Commit

Permalink
feat(storage): add support for MatchGlob (#8097)
Browse files Browse the repository at this point in the history
Adds support for MatchGlob in listing objects. Only JSON is supported because the parameter is not yet available via the gRPC API.

Updates #7727

Supercedes #7728
  • Loading branch information
tritone committed Jun 21, 2023
1 parent 1f050ea commit 9426a5a
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 2 deletions.
5 changes: 5 additions & 0 deletions storage/grpc_client.go
Expand Up @@ -415,6 +415,11 @@ func (c *grpcStorageClient) ListObjects(ctx context.Context, bucket string, q *Q
}
gitr := c.raw.ListObjects(it.ctx, req, s.gax...)
fetch := func(pageSize int, pageToken string) (token string, err error) {
// MatchGlob not yet supported for gRPC.
// TODO: add support when b/287306063 resolved.
if q != nil && q.MatchGlob != "" {
return "", status.Errorf(codes.Unimplemented, "MatchGlob is not supported for gRPC")
}
var objects []*storagepb.Object
err = run(it.ctx, func() error {
objects, token, err = gitr.InternalFetch(pageSize, pageToken)
Expand Down
1 change: 1 addition & 0 deletions storage/http_client.go
Expand Up @@ -347,6 +347,7 @@ func (c *httpStorageClient) ListObjects(ctx context.Context, bucket string, q *Q
req.EndOffset(it.query.EndOffset)
req.Versions(it.query.Versions)
req.IncludeTrailingDelimiter(it.query.IncludeTrailingDelimiter)
req.MatchGlob(it.query.MatchGlob)
if selection := it.query.toFieldSelection(); selection != "" {
req.Fields("nextPageToken", googleapi.Field(selection))
}
Expand Down
62 changes: 62 additions & 0 deletions storage/integration_test.go
Expand Up @@ -1293,6 +1293,68 @@ func TestIntegration_ObjectIteration(t *testing.T) {
})
}

func TestIntegration_ObjectIterationMatchGlob(t *testing.T) {
// This is a separate test from the Object Iteration test above because
// MatchGlob is not yet implemented for gRPC.
ctx := skipGRPC("https://github.com/googleapis/google-cloud-go/issues/7727")
multiTransportTest(skipJSONReads(ctx, "no reads in test"), t, func(t *testing.T, ctx context.Context, _ string, prefix string, client *Client) {
// Reset testTime, 'cause object last modification time should be within 5 min
// from test (test iteration if -count passed) start time.
testTime = time.Now().UTC()
newBucketName := prefix + uidSpace.New()
h := testHelper{t}
bkt := client.Bucket(newBucketName).Retryer(WithPolicy(RetryAlways))

h.mustCreate(bkt, testutil.ProjID(), nil)
defer func() {
if err := killBucket(ctx, client, newBucketName); err != nil {
log.Printf("deleting %q: %v", newBucketName, err)
}
}()
const defaultType = "text/plain"

// Populate object names and make a map for their contents.
objects := []string{
"obj1",
"obj2",
"obj/with/slashes",
"obj/",
"other/obj1",
}
contents := make(map[string][]byte)

// Test Writer.
for _, obj := range objects {
c := randomContents()
if err := writeObject(ctx, bkt.Object(obj), defaultType, c); err != nil {
t.Errorf("Write for %v failed with %v", obj, err)
}
contents[obj] = c
}
query := &Query{MatchGlob: "**obj1"}

var gotNames []string
it := bkt.Objects(context.Background(), query)
for {
attrs, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
t.Fatalf("iterator.Next: %v", err)
}
if attrs.Name != "" {
gotNames = append(gotNames, attrs.Name)
}
}

sortedNames := []string{"obj1", "other/obj1"}
if !cmp.Equal(sortedNames, gotNames) {
t.Errorf("names = %v, want %v", gotNames, sortedNames)
}
})
}

func TestIntegration_ObjectUpdate(t *testing.T) {
ctx := skipJSONReads(context.Background(), "no reads in test")
multiTransportTest(ctx, t, func(t *testing.T, ctx context.Context, bucket string, _ string, client *Client) {
Expand Down
12 changes: 10 additions & 2 deletions storage/storage.go
Expand Up @@ -1486,6 +1486,8 @@ type Query struct {
// aside from the prefix, contain delimiter will have their name,
// truncated after the delimiter, returned in prefixes.
// Duplicate prefixes are omitted.
// Must be set to / when used with the MatchGlob parameter to filter results
// in a directory-like mode.
// Optional.
Delimiter string

Expand All @@ -1499,9 +1501,9 @@ type Query struct {
Versions bool

// attrSelection is used to select only specific fields to be returned by
// the query. It is set by the user calling calling SetAttrSelection. These
// the query. It is set by the user calling SetAttrSelection. These
// are used by toFieldMask and toFieldSelection for gRPC and HTTP/JSON
// clients repsectively.
// clients respectively.
attrSelection []string

// StartOffset is used to filter results to objects whose names are
Expand All @@ -1527,6 +1529,12 @@ type Query struct {
// true, they will also be included as objects and their metadata will be
// populated in the returned ObjectAttrs.
IncludeTrailingDelimiter bool

// MatchGlob is a glob pattern used to filter results (for example, foo*bar). See
// https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-object-glob
// for syntax details. When Delimiter is set in conjunction with MatchGlob,
// it must be set to /.
MatchGlob string
}

// attrToFieldMap maps the field names of ObjectAttrs to the underlying field
Expand Down

0 comments on commit 9426a5a

Please sign in to comment.