Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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__
68 changes: 68 additions & 0 deletions google-cloud-storage/lib/google/cloud/storage/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading