diff --git a/google-cloud-storage/lib/google/cloud/storage/file/verifier.rb b/google-cloud-storage/lib/google/cloud/storage/file/verifier.rb index 52bf667bf976..c6d3626f7a77 100644 --- a/google-cloud-storage/lib/google/cloud/storage/file/verifier.rb +++ b/google-cloud-storage/lib/google/cloud/storage/file/verifier.rb @@ -48,32 +48,55 @@ def self.verify_md5 gcloud_file, local_file def self.verify_crc32c gcloud_file, local_file gcloud_file.crc32c == crc32c_for(local_file) end + # Calculates MD5 digest using either file path or open stream. + def self.md5_for(local_file) + _digest_for(local_file, ::Digest::MD5) + end - def self.md5_for local_file - if local_file.respond_to? :to_path - ::File.open Pathname(local_file).to_path, "rb" do |f| - ::Digest::MD5.file(f).base64digest - end - else # StringIO - (local_file = ::File.open Pathname(local_file)) unless local_file.respond_to? :rewind - local_file.rewind - md5 = ::Digest::MD5.base64digest local_file.read - local_file.rewind - md5 - end + # Calculates CRC32c digest using either file path or open stream. + def self.crc32c_for(local_file) + _digest_for(local_file, ::Digest::CRC32c) end - def self.crc32c_for local_file - if local_file.respond_to? :to_path + private + + # @private + # Computes a base64-encoded digest for a local file or IO stream. + # + # This method handles two types of inputs for `local_file`: + # 1. A file path (String or Pathname): It efficiently streams the file + # to compute the digest without loading the entire file into memory. + # 2. An IO-like stream (e.g., File, StringIO): It reads the stream's + # content to compute the digest. The stream is rewound before and after + # reading to ensure its position is not permanently changed. + # + # @param local_file [String, Pathname, IO] The local file path or IO + # stream for which to compute the digest. + # @param digest_class [Class] The digest class to use for the + # calculation (e.g., `Digest::MD5`). It must respond to `.file` and + # `.base64digest`. + # + # @return [String] The base64-encoded digest of the file's content. + # + def self._digest_for(local_file, digest_class) + if local_file.respond_to?(:to_path) + # Case 1: Input is a file path (or Pathname). Use the safe block form. ::File.open Pathname(local_file).to_path, "rb" do |f| - ::Digest::CRC32c.file(f).base64digest + digest_class.file(f).base64digest + end + else + # Case 2: Input is an open stream (like File or StringIO). + file_to_close = nil + file_to_close = local_file = ::File.open(Pathname(local_file).to_path, "rb") unless local_file.respond_to?(:rewind) + begin + local_file.rewind + digest = digest_class.base64digest local_file.read + local_file.rewind + digest + ensure + # Only close the stream if we explicitly opened it + file_to_close.close if file_to_close.respond_to?(:close) && !file_to_close.closed? end - else # StringIO - (local_file = ::File.open Pathname(local_file)) unless local_file.respond_to? :rewind - local_file.rewind - crc32c = ::Digest::CRC32c.base64digest local_file.read - local_file.rewind - crc32c end end end