-
Notifications
You must be signed in to change notification settings - Fork 0
feat(storage): support Object contexts #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2e2d447
9112800
598851a
b2fe54c
72a33af
6ac24d3
ff09cf5
4270457
c763477
489b521
79db1fa
882d2f2
cd6ad1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # https://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| require "storage_helper" | ||
|
|
||
| describe Google::Cloud::Storage::Bucket, :contexts, :storage do | ||
| let(:bucket_name) { $bucket_names[0] } | ||
| let :bucket do | ||
| storage.bucket(bucket_name) || | ||
| storage.create_bucket(bucket_name) | ||
| end | ||
| let(:custom_context_key1) { "my-custom-key" } | ||
| let(:custom_context_value1) { "my-custom-value" } | ||
| let(:custom_context_key2) { "my-custom-key-2" } | ||
| let(:custom_context_value2) { "my-custom-value-2" } | ||
| let(:local_file) { "acceptance/data/CloudPlatform_128px_Retina.png" } | ||
| let(:file_name) { "CloudLogo1" } | ||
| let(:file_name2) { "CloudLogo2" } | ||
|
|
||
| before(:all) do | ||
| bucket.create_file local_file, file_name | ||
| bucket.create_file local_file, file_name2 | ||
| custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1 | ||
| custom_hash2 = context_custom_hash custom_context_key: custom_context_key2, custom_context_value: custom_context_value2 | ||
|
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| set_object_contexts bucket_name: bucket.name, file_name: file_name, custom_context_key: custom_context_key1, custom_context_value: custom_context_value1 | ||
| set_object_contexts bucket_name: bucket.name, file_name: file_name2, custom_context_key: custom_context_key2, custom_context_value: custom_context_value2 | ||
| end | ||
|
|
||
| it "lists objects with a specific context key and value" do | ||
| list = bucket.files filter: "contexts.\"#{custom_context_key1}\"=\"#{custom_context_value1}\"" | ||
| list.each do |file| | ||
| _(file.name).must_equal file_name | ||
| end | ||
| end | ||
|
|
||
| it "lists objects with a specific context key" do | ||
| list = bucket.files filter: "contexts.\"#{custom_context_key1}\":*" | ||
| list.each do |file| | ||
| _(file.name).must_equal file_name | ||
| end | ||
| end | ||
|
|
||
| it "lists objects that do not have a specific context key" do | ||
| list = bucket.files filter: "-contexts.\"#{custom_context_key1}\":*" | ||
| list.each do |file| | ||
| _(file.name).wont_equal file_name | ||
| end | ||
| end | ||
|
|
||
| it "lists objects that do not have a specific context key and value" do | ||
| list = bucket.files filter: "-contexts.\"#{custom_context_key2}\"=\"#{custom_context_value2}\"" | ||
| list.each do |file| | ||
| _(file.name).must_equal file_name | ||
| _(file.name).wont_equal file_name2 | ||
| end | ||
| end | ||
|
|
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1044,4 +1044,161 @@ | |
| expect { uploaded_file.retention = retention }.must_raise Google::Cloud::PermissionDeniedError | ||
| end | ||
| end | ||
|
|
||
| describe "object contexts" do | ||
| let(:custom_context_key1) { "my-custom-key" } | ||
| let(:custom_context_value1) { "my-custom-value" } | ||
| let(:custom_context_key2) { "my-custom-key-2" } | ||
| let(:custom_context_value2) { "my-custom-value-2" } | ||
| let(:local_file) { "acceptance/data/CloudPlatform_128px_Retina.png" } | ||
| let(:file_name) { "CloudLogo1" } | ||
|
|
||
| before do | ||
| bucket.create_file local_file, file_name | ||
| end | ||
|
|
||
| it "sets and retrieves custom context key and value" do | ||
| file = bucket.file file_name | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: custom_context_key1 ,custom_context_value: custom_context_value1) | ||
| ) | ||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value1 | ||
| end | ||
|
|
||
| it "rejects special characters in custom context key and value" do | ||
| invalid_key = 'my"-invalid-key' | ||
| custom_value = 'my-custom-value' | ||
|
|
||
| file = bucket.file file_name | ||
|
|
||
| err = _ { | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: invalid_key, custom_context_value: custom_value) | ||
| ) | ||
| }.must_raise Google::Cloud::InvalidArgumentError | ||
|
|
||
| _(err.message).must_match(/Object context key cannot contain/) | ||
|
|
||
| invalid_key = 'my-custom-key' | ||
| custom_value = 'my-invalid/value' | ||
|
|
||
| err = _ { | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: invalid_key, custom_context_value: custom_value) | ||
| ) | ||
| }.must_raise Google::Cloud::InvalidArgumentError | ||
|
|
||
| _(err.message).must_match(/Object context value cannot contain/) | ||
| end | ||
|
|
||
| it "rejects unicode characters in keys and values" do | ||
| invalid_key = '🚀-launcher' | ||
| custom_value = 'my-custom-value' | ||
| file = bucket.file file_name | ||
| err = _ { | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: invalid_key, custom_context_value: custom_value) | ||
| ) | ||
| }.must_raise Google::Cloud::InvalidArgumentError | ||
| _(err.message).must_match(/Object context key must start with an alphanumeric character./) | ||
|
|
||
| invalid_key = "my-custom-key" | ||
| custom_value = '✨-sparkle' | ||
|
|
||
| err = _ { | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: invalid_key, custom_context_value: custom_value) | ||
| ) | ||
| }.must_raise Google::Cloud::InvalidArgumentError | ||
|
|
||
| _(err.message).must_match(/Object context value must start with an alphanumeric character./) | ||
| end | ||
|
|
||
| it "modifies existing custom context key and value" do | ||
| file = bucket.file file_name | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: custom_context_key1 ,custom_context_value: custom_context_value1) | ||
| ) | ||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value1 | ||
|
|
||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: custom_context_key1 ,custom_context_value: custom_context_value2) | ||
| ) | ||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value2 | ||
| end | ||
|
|
||
| it "overwrites existing context key and value" do | ||
| file = bucket.file file_name | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: custom_context_key1 ,custom_context_value: custom_context_value1) | ||
| ) | ||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value1 | ||
|
|
||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: context_custom_hash(custom_context_key: custom_context_key2 ,custom_context_value: custom_context_value2) | ||
| ) | ||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key2].value).must_equal custom_context_value2 | ||
| end | ||
|
|
||
| it "sets and retrieves multiple custom context keys and values" do | ||
| file = bucket.file file_name | ||
| custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1 | ||
| custom_hash2 = context_custom_hash custom_context_key: custom_context_key2, custom_context_value: custom_context_value2 | ||
|
|
||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: { | ||
| custom_context_key1 => custom_hash1[custom_context_key1], | ||
| custom_context_key2 => custom_hash2[custom_context_key2] | ||
| } | ||
| ) | ||
|
Comment on lines
+1150
to
+1158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The construction of the custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1
custom_hash2 = context_custom_hash custom_context_key: custom_context_key2, custom_context_value: custom_context_value2
file.contexts = Google::Apis::StorageV1::Object::Contexts.new(
custom: custom_hash1.merge(custom_hash2)
) |
||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value1 | ||
| _(file.contexts.custom[custom_context_key2].value).must_equal custom_context_value2 | ||
| end | ||
|
|
||
| it "removes individual context" do | ||
| file = bucket.file file_name | ||
| custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1 | ||
| custom_hash2 = context_custom_hash custom_context_key: custom_context_key2, custom_context_value: custom_context_value2 | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: { | ||
| custom_context_key1 => custom_hash1[custom_context_key1], | ||
| custom_context_key2 => custom_hash2[custom_context_key2] | ||
| } | ||
| ) | ||
|
Comment on lines
+1166
to
+1173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The construction of the custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1
custom_hash2 = context_custom_hash custom_context_key: custom_context_key2, custom_context_value: custom_context_value2
file.contexts = Google::Apis::StorageV1::Object::Contexts.new(
custom: custom_hash1.merge(custom_hash2)
) |
||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value1 | ||
| _(file.contexts.custom[custom_context_key2].value).must_equal custom_context_value2 | ||
|
|
||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: { | ||
| custom_context_key1 => nil | ||
| } | ||
| ) | ||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1]).must_be_nil | ||
| _(file.contexts.custom[custom_context_key2].value).must_equal custom_context_value2 | ||
| end | ||
|
|
||
| it "clears all contexts" do | ||
| file = bucket.file file_name | ||
| custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1 | ||
| file.contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| custom: { | ||
| custom_context_key1=> custom_hash1[custom_context_key1] | ||
| } | ||
| ) | ||
|
Comment on lines
+1190
to
+1195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be simplified. Since custom_hash1 = context_custom_hash custom_context_key: custom_context_key1, custom_context_value: custom_context_value1
file.contexts = Google::Apis::StorageV1::Object::Contexts.new(
custom: custom_hash1
) |
||
| file.reload! | ||
| _(file.contexts.custom[custom_context_key1].value).must_equal custom_context_value1 | ||
|
|
||
| file.contexts = nil | ||
| file.reload! | ||
| _(file.contexts).must_be_nil | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1439,7 +1439,23 @@ def delete if_metageneration_match: nil, if_metageneration_not_match: nil | |
| # Only applicable if delimiter is set to '/'. | ||
| # @param [Boolean] soft_deleted If true, only soft-deleted object | ||
| # versions will be listed. The default is false. | ||
| # @param [String] filter An optional string for filtering listed objects. | ||
| # Supported fields: contexts | ||
| # If delimiter is set, the returned prefixes are exempt from this filter | ||
| # List any object that has a context with the specified key attached | ||
| # filter = "contexts.\"KEY\":*"; | ||
| # | ||
| # List any object that has a context with the specified key attached and value attached | ||
| # filter = "contexts.\"keyA\"=\"valueA\"" | ||
| # | ||
| # List any object that does not have a context with the specified key attached | ||
| # filter = "-contexts.\"KEY\":*"; | ||
| # | ||
| # List any object that has a context with the specified key and value attached | ||
| # filter = "contexts.\"KEY\"=\"VALUE\""; | ||
| # | ||
| # List any object that does not have a context with the specified key and value attached | ||
| # filter = "-contexts.\"KEY\"=\"VALUE\""; | ||
|
shubhangi-google marked this conversation as resolved.
Comment on lines
+1445
to
+1458
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The semicolons at the end of some of the filter examples are inconsistent and not idiomatic for Ruby code examples. Please remove them for consistency. # List any object that has a context with the specified key attached
# filter = "contexts.\"KEY\":*"
#
# List any object that has a context with the specified key attached and value attached
# filter = "contexts.\"keyA\"=\"valueA\""
#
# List any object that does not have a context with the specified key attached
# filter = "-contexts.\"KEY\":*"
#
# List any object that has a context with the specified key and value attached
# filter = "contexts.\"KEY\"=\"VALUE\""
#
# List any object that does not have a context with the specified key and value attached
# filter = "-contexts.\"KEY\"=\"VALUE\"" |
||
| # @return [Array<Google::Cloud::Storage::File>] (See | ||
| # {Google::Cloud::Storage::File::List}) | ||
| # | ||
|
|
@@ -1465,23 +1481,34 @@ def delete if_metageneration_match: nil, if_metageneration_not_match: nil | |
| # puts file.name | ||
| # end | ||
| # | ||
| # @example Filter files by context: | ||
| # require "google/cloud/storage" | ||
| # storage = Google::Cloud::Storage.new | ||
| # bucket = storage.bucket "my-bucket" | ||
| # files = bucket.files filter: "contexts.\"myKey\"=\"myValue\"" | ||
| # files.each do |file| | ||
| # puts file.name | ||
| # end | ||
| # | ||
| def files prefix: nil, delimiter: nil, token: nil, max: nil, | ||
| versions: nil, match_glob: nil, include_folders_as_prefixes: nil, | ||
| soft_deleted: nil | ||
| soft_deleted: nil, filter: nil | ||
| ensure_service! | ||
| gapi = service.list_files name, prefix: prefix, delimiter: delimiter, | ||
| token: token, max: max, | ||
| versions: versions, | ||
| user_project: user_project, | ||
| match_glob: match_glob, | ||
| include_folders_as_prefixes: include_folders_as_prefixes, | ||
| soft_deleted: soft_deleted | ||
| soft_deleted: soft_deleted, | ||
| filter: filter | ||
| File::List.from_gapi gapi, service, name, prefix, delimiter, max, | ||
| versions, | ||
| user_project: user_project, | ||
| match_glob: match_glob, | ||
| include_folders_as_prefixes: include_folders_as_prefixes, | ||
| soft_deleted: soft_deleted | ||
| soft_deleted: soft_deleted, | ||
| filter: filter | ||
| end | ||
| alias find_files files | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -370,6 +370,45 @@ def content_type= content_type | |
| update_gapi! :content_type | ||
| end | ||
|
|
||
| ## | ||
| # User-defined object contexts. Each object context is a key- | ||
| # payload pair, where the key provides the identification and the payload holds | ||
| # the associated value and additional metadata. | ||
| # Object contexts are used to provide additional information about an object | ||
| # @return [Google::Apis::StorageV1::Object::Contexts, nil] The object contexts, or `nil` if there are none. | ||
|
|
||
| def contexts | ||
| @gapi.contexts | ||
| end | ||
|
|
||
| ## | ||
| # Sets the object context. | ||
| # To pass generation and/or metageneration preconditions, call this | ||
| # method within a block passed to {#update}. | ||
| # @param [Google::Apis::StorageV1::Object::Contexts] contexts The object contexts to set. | ||
| # @see https://docs.cloud.google.com/storage/docs/use-object-contexts#attach-modify-contexts Object Contexts documentation | ||
| # @example | ||
| # require "google/cloud/storage" | ||
| # storage = Google::Cloud::Storage.new | ||
|
Comment on lines
+379
to
+392
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| # bucket = storage.bucket "my-bucket" | ||
| # file = bucket.file "path/to/my-file.ext" | ||
| # payload = Google::Apis::StorageV1::ObjectCustomContextPayload.new( | ||
| # value: "your-custom-context-value" | ||
| # ) | ||
| # custom_hash = { | ||
| # "your-custom-context-key" => payload | ||
| # } | ||
| # contexts = Google::Apis::StorageV1::Object::Contexts.new( | ||
| # custom: custom_hash | ||
| # ) | ||
| # file.update do |file| | ||
| # file.contexts = contexts | ||
| # end | ||
| def contexts= contexts | ||
| @gapi.contexts = contexts | ||
| update_gapi! :contexts | ||
| end | ||
|
|
||
| ## | ||
| # A custom time specified by the user for the file, or `nil`. | ||
| # | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.