diff --git a/storage/integration_test.go b/storage/integration_test.go index eb5b27557d20..b851fecbcafb 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -1659,18 +1659,26 @@ func TestIntegration_Compose(t *testing.T) { // Compose should work even if the user sets no destination attributes. compDst := b.Object("composed1") c := compDst.ComposerFrom(compSrcs...) - if _, err := c.Run(ctx); err != nil { + attrs, err := c.Run(ctx) + if err != nil { t.Fatalf("ComposeFrom error: %v", err) } + if attrs.ComponentCount != int64(len(objects)) { + t.Errorf("mismatching ComponentCount: got %v, want %v", attrs.ComponentCount, int64(len(objects))) + } checkCompose(compDst, "application/octet-stream") // It should also work if we do. compDst = b.Object("composed2") c = compDst.ComposerFrom(compSrcs...) c.ContentType = "text/json" - if _, err := c.Run(ctx); err != nil { + attrs, err = c.Run(ctx) + if err != nil { t.Fatalf("ComposeFrom error: %v", err) } + if attrs.ComponentCount != int64(len(objects)) { + t.Errorf("mismatching ComponentCount: got %v, want %v", attrs.ComponentCount, int64(len(objects))) + } checkCompose(compDst, "text/json") }) } diff --git a/storage/storage.go b/storage/storage.go index c4a771593249..7fc3fc4cb9b3 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1315,6 +1315,11 @@ type ObjectAttrs struct { // later value but not to an earlier one. For more information see // https://cloud.google.com/storage/docs/metadata#custom-time . CustomTime time.Time + + // ComponentCount is the number of objects contained within a composite object. + // For non-composite objects, the value will be zero. + // This field is read-only. + ComponentCount int64 } // convertTime converts a time in RFC3339 format to time.Time. @@ -1385,6 +1390,7 @@ func newObject(o *raw.Object) *ObjectAttrs { Updated: convertTime(o.Updated), Etag: o.Etag, CustomTime: convertTime(o.CustomTime), + ComponentCount: o.ComponentCount, } } @@ -1419,6 +1425,7 @@ func newObjectFromProto(o *storagepb.Object) *ObjectAttrs { Deleted: convertProtoTime(o.GetDeleteTime()), Updated: convertProtoTime(o.GetUpdateTime()), CustomTime: convertProtoTime(o.GetCustomTime()), + ComponentCount: int64(o.ComponentCount), } } @@ -1547,6 +1554,7 @@ var attrToFieldMap = map[string]string{ "Updated": "updated", "Etag": "etag", "CustomTime": "customTime", + "ComponentCount": "componentCount", } // attrToProtoFieldMap maps the field names of ObjectAttrs to the underlying field @@ -1578,6 +1586,7 @@ var attrToProtoFieldMap = map[string]string{ "Owner": "owner", "CustomerKeySHA256": "customer_encryption", "CustomTime": "custom_time", + "ComponentCount": "component_count", // MediaLink was explicitly excluded from the proto as it is an HTTP-ism. // "MediaLink": "mediaLink", } diff --git a/storage/storage_test.go b/storage/storage_test.go index 30cf209db50f..0414e0de0e7e 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -1747,6 +1747,7 @@ func TestRawObjectToObjectAttrs(t *testing.T) { TimeCreated: "2019-03-31T19:32:10Z", TimeDeleted: "2019-03-31T19:33:39Z", TemporaryHold: true, + ComponentCount: 2, }, want: &ObjectAttrs{ Bucket: "Test", @@ -1763,6 +1764,7 @@ func TestRawObjectToObjectAttrs(t *testing.T) { RetentionExpirationTime: time.Date(2019, 3, 31, 19, 33, 36, 0, time.UTC), Size: 1 << 20, TemporaryHold: true, + ComponentCount: 2, }, }, } @@ -1833,6 +1835,7 @@ func TestProtoObjectToObjectAttrs(t *testing.T) { CreateTime: timestamppb.New(now), DeleteTime: timestamppb.New(now), TemporaryHold: true, + ComponentCount: 2, }, want: &ObjectAttrs{ Bucket: "Test", @@ -1848,6 +1851,7 @@ func TestProtoObjectToObjectAttrs(t *testing.T) { RetentionExpirationTime: now, Size: 1 << 20, TemporaryHold: true, + ComponentCount: 2, }, }, }