diff --git a/google-cloud-storage/lib/google/cloud/storage/bucket.rb b/google-cloud-storage/lib/google/cloud/storage/bucket.rb index f5ab081698a7..ca8461354bb7 100644 --- a/google-cloud-storage/lib/google/cloud/storage/bucket.rb +++ b/google-cloud-storage/lib/google/cloud/storage/bucket.rb @@ -3143,6 +3143,51 @@ def reload! end alias refresh! reload! + ## + # Moves File from source to destination path within the same HNS-enabled bucket + # This Operation is being performed at server side + # @param [String] source_file The file name in existing bucket + # @param [String] destination_file The new filename to be created on bucket + # If the destination path includes non-existent parent folders, they will be created. + # @example + # require "google/cloud/storage" + # storage = Google::Cloud::Storage.new + # bucket = storage.bucket bucket_name, skip_lookup: true + # bucket.move_file source_file_name, destination_file_name + def move_file source_file, + destination_file, + if_generation_match: nil, + if_generation_not_match: nil, + if_metageneration_match: nil, + if_metageneration_not_match: nil, + if_source_generation_match: nil, + if_source_generation_not_match: nil, + if_source_metageneration_match: nil, + if_source_metageneration_not_match: nil, + user_project: nil, + fields: nil, + quota_user: nil, + user_ip: nil, + options: {} + ensure_service! + service.move_file name, + source_file, + destination_file, + if_generation_match: if_generation_match, + if_generation_not_match: if_generation_not_match, + if_metageneration_match: if_metageneration_match, + if_metageneration_not_match: if_metageneration_not_match, + if_source_generation_match: if_source_generation_match, + if_source_generation_not_match: if_source_generation_not_match, + if_source_metageneration_match: if_source_metageneration_match, + if_source_metageneration_not_match: if_source_metageneration_not_match, + user_project: user_project, + fields: fields, + quota_user: quota_user, + user_ip: user_ip, + options: options + end + ## # Determines whether the bucket exists in the Storage service. # diff --git a/google-cloud-storage/lib/google/cloud/storage/service.rb b/google-cloud-storage/lib/google/cloud/storage/service.rb index 5175b08928c9..d9adf2acff65 100644 --- a/google-cloud-storage/lib/google/cloud/storage/service.rb +++ b/google-cloud-storage/lib/google/cloud/storage/service.rb @@ -630,6 +630,44 @@ def patch_file bucket_name, end end + ## + # Moves file from source to destination path within bucket + def move_file name, + source_file, + destination_file, + if_generation_match: nil, + if_generation_not_match: nil, + if_metageneration_match: nil, + if_metageneration_not_match: nil, + if_source_generation_match: nil, + if_source_generation_not_match: nil, + if_source_metageneration_match: nil, + if_source_metageneration_not_match: nil, + user_project: nil, + fields: nil, + quota_user: nil, + user_ip: nil, + options: {} + execute do + service.move_object name, + source_file, + destination_file, + if_generation_match: if_generation_match, + if_generation_not_match: if_generation_not_match, + if_metageneration_match: if_metageneration_match, + if_metageneration_not_match: if_metageneration_not_match, + if_source_generation_match: if_source_generation_match, + if_source_generation_not_match: if_source_generation_not_match, + if_source_metageneration_match: if_source_metageneration_match, + if_source_metageneration_not_match: if_source_metageneration_not_match, + user_project: user_project(user_project), + fields: fields, + quota_user: quota_user, + user_ip: user_ip, + options: options + end + end + ## # Permanently deletes a file. def delete_file bucket_name, diff --git a/google-cloud-storage/samples/acceptance/buckets_test.rb b/google-cloud-storage/samples/acceptance/buckets_test.rb index f4f416744fd1..7afc71d14415 100644 --- a/google-cloud-storage/samples/acceptance/buckets_test.rb +++ b/google-cloud-storage/samples/acceptance/buckets_test.rb @@ -53,6 +53,7 @@ require_relative "../storage_set_retention_policy" require_relative "../storage_get_autoclass" require_relative "../storage_set_autoclass" +require_relative "../storage_move_object" describe "Buckets Snippets" do let(:storage_client) { Google::Cloud::Storage.new } @@ -581,4 +582,38 @@ bucket.public_access_prevention = :inherited end end + + describe "storage move file" do + let(:source_file) { "file_1_name_#{SecureRandom.hex}.txt" } + let(:destination_file) { "file_2_name_#{SecureRandom.hex}.txt" } + let :hns_bucket do + hierarchical_namespace = Google::Apis::StorageV1::Bucket::HierarchicalNamespace.new enabled: true + storage_client.create_bucket random_bucket_name do |b| + b.uniform_bucket_level_access = true + b.hierarchical_namespace = hierarchical_namespace + end + end + let :create_source_file do + file_content = "A" * (3 * 1024 * 1024) # 3 MB of 'A' characters + file = StringIO.new file_content + hns_bucket.create_file file, source_file + end + it "file is moved and old file is deleted" do + create_source_file + out, _err = capture_io do + move_object bucket_name: hns_bucket.name, source_file_name: source_file, destination_file_name: destination_file + end + assert_includes out, "New File #{destination_file} created\n" + refute_nil(hns_bucket.file(destination_file)) + assert_nil(hns_bucket.file(source_file)) + end + + it "raises error if source and destination are having same filename" do + create_source_file + exception = assert_raises Google::Cloud::InvalidArgumentError do + move_object bucket_name: hns_bucket.name, source_file_name: source_file, destination_file_name: source_file + end + assert_equal "invalid: Source and destination object names must be different.", exception.message + end + end end diff --git a/google-cloud-storage/samples/storage_move_object.rb b/google-cloud-storage/samples/storage_move_object.rb new file mode 100644 index 000000000000..727f3dc7d891 --- /dev/null +++ b/google-cloud-storage/samples/storage_move_object.rb @@ -0,0 +1,40 @@ +# Copyright 2025 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 +# +# http://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. + +# [START storage_move_object] +def move_object bucket_name:, source_file_name:, destination_file_name: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + + # The name of your GCS object + # source_file_name = "your-file-name" + + # The new object name which you want to craete + # destination_file_name = "your-new-file-name" + + require "google/cloud/storage" + + storage = Google::Cloud::Storage.new + bucket = storage.bucket bucket_name, skip_lookup: true + + bucket.move_file source_file_name, destination_file_name + fetch_file = bucket.file destination_file_name + puts "New File #{fetch_file.name} created\n" +end +# [END storage_move_object] + +if $PROGRAM_NAME == __FILE__ + move_object bucket_name: ARGV.shift, source_file_name: ARGV.shift, + destination_file_name: ARGV.shift +end diff --git a/google-cloud-storage/samples/storage_remove_bucket_conditional_iam_binding.rb b/google-cloud-storage/samples/storage_remove_bucket_conditional_iam_binding.rb index 298450305845..0ee0f5a97ec5 100644 --- a/google-cloud-storage/samples/storage_remove_bucket_conditional_iam_binding.rb +++ b/google-cloud-storage/samples/storage_remove_bucket_conditional_iam_binding.rb @@ -37,10 +37,10 @@ def remove_bucket_conditional_iam_binding bucket_name: description: description, expression: expression } - if (b.role == role) && (b.condition && - b.condition.title == title && - b.condition.description == description && - b.condition.expression == expression) + if b.role == role && b.condition && + b.condition.title == title && + b.condition.description == description && + b.condition.expression == expression binding_to_remove = b end end diff --git a/google-cloud-storage/test/google/cloud/storage/bucket_test.rb b/google-cloud-storage/test/google/cloud/storage/bucket_test.rb index 1dc5babfab0b..203e423467e5 100644 --- a/google-cloud-storage/test/google/cloud/storage/bucket_test.rb +++ b/google-cloud-storage/test/google/cloud/storage/bucket_test.rb @@ -1381,6 +1381,32 @@ _(bucket.api_url).must_equal "#{new_url_root}/b/#{bucket_name}" mock.verify end + describe "storage move file" do + it "moves a file for bucket" do + file_name = "file.ext" + file_2_name = "file1.ext" + mock = Minitest::Mock.new + mock.expect :move_object,Google::Apis::StorageV1::Object.from_json(random_file_hash(bucket.name, file_2_name).to_json),[ bucket.name, file_name,file_2_name],**move_object_args + bucket.service.mocked_service = mock + file = bucket.move_file file_name, file_2_name + mock.verify + _(file).must_be_kind_of Google::Apis::StorageV1::Object + end + + it "raises error if source and destination are having same filename" do + file_name = "file.ext" + file_2_name = "file1.ext" + mock = Minitest::Mock.new + mock.expect :move_object, [ bucket.name, file_name,file_name] do + raise Google::Cloud::InvalidArgumentError, "invalid: Source and destination object names must be different." + end + bucket.service.mocked_service = mock + exception = assert_raises(Google::Cloud::InvalidArgumentError) do + bucket.move_file file_name, file_name + end + assert_equal "invalid: Source and destination object names must be different.", exception.message + end + end def create_file_gapi bucket=nil, name = nil Google::Apis::StorageV1::Object.from_json random_file_hash(bucket, name).to_json diff --git a/google-cloud-storage/test/helper.rb b/google-cloud-storage/test/helper.rb index 239104d6171b..5d37f92feb26 100644 --- a/google-cloud-storage/test/helper.rb +++ b/google-cloud-storage/test/helper.rb @@ -457,6 +457,36 @@ def patch_object_args generation: nil, } end + def move_object_args if_generation_match: nil, + if_generation_not_match: nil, + if_metageneration_match: nil, + if_metageneration_not_match: nil, + if_source_generation_match: nil, + if_source_generation_not_match: nil, + if_source_metageneration_match: nil, + if_source_metageneration_not_match: nil, + user_project: nil, + fields: nil, + quota_user: nil, + user_ip: nil, + options: {} + { + if_generation_match: if_generation_match, + if_generation_not_match: if_generation_not_match, + if_metageneration_match: if_metageneration_match, + if_metageneration_not_match: if_metageneration_not_match, + if_source_generation_match: if_source_generation_match, + if_source_generation_not_match: if_source_generation_not_match, + if_source_metageneration_match: if_source_metageneration_match, + if_source_metageneration_not_match: if_source_metageneration_not_match, + user_project: user_project, + fields: fields, + quota_user: quota_user, + user_ip: user_ip, + options: options + } + end + def delete_object_args generation: nil, if_generation_match: nil, if_generation_not_match: nil,