diff --git a/google-cloud-storage-control/samples/acceptance/storage_control_folders_test.rb b/google-cloud-storage-control/samples/acceptance/storage_control_folders_test.rb index fa4ca9f223b0..bb297193690e 100644 --- a/google-cloud-storage-control/samples/acceptance/storage_control_folders_test.rb +++ b/google-cloud-storage-control/samples/acceptance/storage_control_folders_test.rb @@ -18,6 +18,7 @@ require_relative "../storage_control_list_folders" require_relative "../storage_control_rename_folder" require_relative "../storage_control_delete_folder" +require_relative "../storage_control_delete_folder_recursive" describe "Storage Control Folders" do let(:bucket_name) { random_bucket_name } @@ -64,5 +65,33 @@ assert_output "Deleted folder: #{new_folder_name}\n" do delete_folder bucket_name: bucket_name, folder_name: new_folder_name end + + # create parent folder for recursive delete + capture_io do + create_folder bucket_name: bucket_name, folder_name: folder_name + end + + # create a child folder inside parent folder + child_folder_name = "#{folder_name}/child-folder" + capture_io do + create_folder bucket_name: bucket_name, folder_name: child_folder_name + end + + # delete parent folder recursively + begin + assert_output "Deleted folder recursively: #{folder_name}\n" do + delete_folder_recursive bucket_name: bucket_name, folder_name: folder_name + end + rescue Minitest::UnexpectedError => e + is_invalid_arg = e.error.is_a? Google::Cloud::InvalidArgumentError + is_not_enabled = e.error.message.include? "Recursive folder delete is not enabled for this bucket" + raise e unless is_invalid_arg && is_not_enabled + + skip "Skipping recursive delete test because the feature is not enabled for this bucket." + rescue Google::Cloud::InvalidArgumentError => e + raise e unless e.message.include? "Recursive folder delete is not enabled for this bucket" + + skip "Skipping recursive delete test because the feature is not enabled for this bucket." + end end end diff --git a/google-cloud-storage-control/samples/storage_control_delete_folder_recursive.rb b/google-cloud-storage-control/samples/storage_control_delete_folder_recursive.rb new file mode 100644 index 000000000000..ebeee124ab08 --- /dev/null +++ b/google-cloud-storage-control/samples/storage_control_delete_folder_recursive.rb @@ -0,0 +1,40 @@ +# 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 +# +# 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_control_delete_folder_recursive] +def delete_folder_recursive bucket_name:, folder_name: + # The ID of your GCS bucket + # bucket_name = "your-unique-bucket-name" + # + # Name of the folder you want to delete recursively + # folder_name = "name-of-the-folder" + + require "google/cloud/storage/control" + + storage_control = Google::Cloud::Storage::Control.storage_control + + # The storage folder path uses the global access pattern, in which the "_" + # denotes this bucket exists in the global namespace. + folder_path = storage_control.folder_path project: "_", bucket: bucket_name, folder: folder_name + + request = Google::Cloud::Storage::Control::V2::DeleteFolderRecursiveRequest.new name: folder_path + + result = storage_control.delete_folder_recursive request + result.wait_until_done! timeout: 60 + + puts "Deleted folder recursively: #{folder_name}" +end +# [END storage_control_delete_folder_recursive] + +delete_folder_recursive bucket_name: ARGV.shift if $PROGRAM_NAME == __FILE__ diff --git a/google-cloud-storage/lib/google/cloud/storage/service.rb b/google-cloud-storage/lib/google/cloud/storage/service.rb index fd87490a5c62..fdca61b76bfc 100644 --- a/google-cloud-storage/lib/google/cloud/storage/service.rb +++ b/google-cloud-storage/lib/google/cloud/storage/service.rb @@ -1044,3 +1044,71 @@ def retry? query_params end end end + +# rubocop:disable all + +# @private +module Google + module Apis + module Core + class StorageDownloadCommand < DownloadCommand + # Monkey patch to skip writing chunk and updating offset on error status codes (status >= 300) + def execute_once(client, &block) + request_header = header.dup + apply_request_options(request_header) + download_offset = nil + + if @offset > 0 + logger.debug { sprintf('Resuming download from offset %d', @offset) } + request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset) + end + + http_res = client.get(url.to_s, query, request_header) do |request| + request.options.on_data = proc do |chunk, _size, res| + status = res ? res.status.to_i : 200 + next if chunk.nil? || status >= 300 + + download_offset ||= (status == 206 ? @offset : 0) + download_offset += chunk.bytesize + + if download_offset - chunk.bytesize == @offset + next_chunk = chunk + else + # Oh no! Requested a chunk, but received the entire content + chunk_index = @offset - (download_offset - chunk.bytesize) + next_chunk = chunk.byteslice(chunk_index..-1) + next if next_chunk.nil? + end + + # logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) } + @download_io.write(next_chunk) + + @offset += next_chunk.bytesize + end + end + + @download_io.flush if @download_io.respond_to?(:flush) + + if @close_io_on_finish + result = nil + else + result = @download_io + end + check_status(http_res.status.to_i, http_res.headers, http_res.body) + # In case of file download in storage, we need to respond back with + # the http response object along with the result IO object, because + # google-cloud-storage uses the HTTP info. + # Also, older versions of google-cloud-storage assume this object + # conforms to the old httpclient response API instead of the Faraday + # response API. Return a subclass that provides the needed methods. + http_res = Core::Response.new http_res.env + success([result, http_res], &block) + rescue => e + @download_io.flush if @download_io.respond_to?(:flush) + error(e, rethrow: true, &block) + end + end + end + end +end +# rubocop:enable all