123456789_123456789_123456789_123456789_123456789_

Class: ActiveStorage::Service::GCSService

Relationships & Source Files
Namespace Children
Exceptions:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Inherits: ActiveStorage::Service
Defined in: activestorage/lib/active_storage/service/gcs_service.rb

Overview

Active Storage GCS Service

Wraps the Google Cloud Storage as an Active Storage service. See ::ActiveStorage::Service for the generic API documentation that applies to all services.

Class Method Summary

::ActiveStorage::Service - Inherited

.configure

Configure an Active Storage service by name from a set of configurations, typically loaded from a YAML file.

.build

Override in subclasses that stitch together multiple services and hence need to build additional services using the configurator.

::ActiveSupport::Autoload - Extended

Instance Attribute Summary

::ActiveStorage::Service - Inherited

Instance Method Summary

::ActiveStorage::Service - Inherited

#compose

Concatenate multiple files into a single “composed” file.

#delete

Delete the file at the key.

#delete_prefixed

Delete files at keys starting with the prefix.

#download

Return the content of the file at the key.

#download_chunk

Return the partial content in the byte range of the file at the key.

#exist?

Return true if a file exists at the key.

#headers_for_direct_upload

Returns a ::Hash of headers for #url_for_direct_upload requests.

#open,
#update_metadata

Update metadata for the file identified by key in the service.

#upload

Upload the io to the key specified.

#url

Returns the URL for the file at the key.

#url_for_direct_upload

Returns a signed, temporary URL that a direct upload file can be PUT to on the key.

#content_disposition_with, #custom_metadata_headers, #instrument, #private_url, #public_url, #service_name, #inspect

Constructor Details

.new(public: false, **config) ⇒ GCSService

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 16

def initialize(public: false, **config)
  @config = config
  @public = public
end

Instance Attribute Details

#config (readonly, private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 185

attr_reader :config

Instance Method Details

#bucket (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 206

def bucket
  @bucket ||= client.bucket(config.fetch(:bucket), skip_lookup: true)
end

#client (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 210

def client
  @client ||= Google::Cloud::Storage.new(**config.except(:bucket, :cache_control, :iam, :gsa_email))
end

#compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 139

def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
  bucket.compose(source_keys, destination_key).update do |file|
    file.content_type = content_type
    file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
    file. = 
  end
end

#custom_metadata_headers(metadata) (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 238

def ()
  .transform_keys { |key| "x-goog-meta-#{key}" }
end

#delete(key)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 66

def delete(key)
  instrument :delete, key: key do
    file_for(key).delete
  rescue Google::Cloud::NotFoundError
    # Ignore files already deleted
  end
end

#delete_prefixed(prefix)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 74

def delete_prefixed(prefix)
  instrument :delete_prefixed, prefix: prefix do
    bucket.files(prefix: prefix).all do |file|
      file.delete
    rescue Google::Cloud::NotFoundError
      # Ignore concurrently-deleted files
    end
  end
end

#download(key, &block)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 34

def download(key, &block)
  if block_given?
    instrument :streaming_download, key: key do
      stream(key, &block)
    end
  else
    instrument :download, key: key do
      file_for(key).download.string
    rescue Google::Cloud::NotFoundError
      raise ActiveStorage::FileNotFoundError
    end
  end
end

#download_chunk(key, range)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 58

def download_chunk(key, range)
  instrument :download_chunk, key: key, range: range do
    file_for(key).download(range: range).string
  rescue Google::Cloud::NotFoundError
    raise ActiveStorage::FileNotFoundError
  end
end

#email_from_metadata_server (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 218

def 
  env = Google::Cloud.env
  raise MetadataServerNotFoundError if !env.metadata?

  email = env.("instance", "service-accounts/default/email")
  email.presence or raise MetadataServerError
end

#exist?(key) ⇒ Boolean

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 84

def exist?(key)
  instrument :exist, key: key do |payload|
    answer = file_for(key).exists?
    payload[:exist] = answer
    answer
  end
end

#file_for(key, skip_lookup: true) (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 187

def file_for(key, skip_lookup: true)
  bucket.file(key, skip_lookup: skip_lookup)
end

#headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, custom_metadata: {})

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 128

def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename

  headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **() }
  if @config[:cache_control].present?
    headers["Cache-Control"] = @config[:cache_control]
  end

  headers
end

#iam_client

Returns the IAM client used for direct uploads and signed URLs. By default, the authorization for the IAM client is set to Application Default Credentials, fetched once at instantiation time, then refreshed automatically when expired. This can be set to a different value to use other authorization methods.

ActiveStorage::Blob.service.iam_client.authorization = Google::Auth::ImpersonatedServiceAccountCredentials.new(options)
[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 154

def iam_client
  @iam_client ||= Google::Apis::IamcredentialsV1::IAMCredentialsService.new.tap do |client|
    client.authorization ||= Google::Auth.get_application_default(["https://www.googleapis.com/auth/iam"])
  rescue
    nil
  end
end

#issuer (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 214

def issuer
  @issuer ||= @config[:gsa_email].presence || 
end

#private_url(key, expires_in:, filename:, content_type:, disposition:) (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 163

def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
  args = {
    expires: expires_in,
    query: {
      "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
      "response-content-type" => content_type
    }
  }

  if @config[:iam]
    args[:issuer] = issuer
    args[:signer] = signer
  end

  file_for(key).signed_url(**args)
end

#public_url(key) (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 180

def public_url(key, **)
  file_for(key).public_url
end

#signer (private)

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 226

def signer
  # https://googleapis.dev/ruby/google-cloud-storage/latest/Google/Cloud/Storage/Project.html#signed_url-instance_method
  lambda do |string_to_sign|
    request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
      payload: string_to_sign
    )
    resource = "projects/-/serviceAccounts/#{issuer}"
    response = iam_client.(resource, request)
    response.signed_blob
  end
end

#stream(key) (private)

Reads the file for the given key in chunks, yielding each to the block.

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 192

def stream(key)
  file = file_for(key, skip_lookup: false)

  chunk_size = 5.megabytes
  offset = 0

  raise ActiveStorage::FileNotFoundError unless file.present?

  while offset < file.size
    yield file.download(range: offset..(offset + chunk_size - 1)).string
    offset += chunk_size
  end
end

#update_metadata(key, content_type:, disposition: nil, filename: nil, custom_metadata: {})

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 48

def (key, content_type:, disposition: nil, filename: nil, custom_metadata: {})
  instrument :, key: key, content_type: content_type, disposition: disposition do
    file_for(key).update do |file|
      file.content_type = content_type
      file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
      file. = 
    end
  end
end

#upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 21

def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})
  instrument :upload, key: key, checksum: checksum do
    # GCS's signed URLs don't include params such as response-content-type response-content_disposition
    # in the signature, which means an attacker can modify them and bypass our effort to force these to
    # binary and attachment when the file's content type requires it. The only way to force them is to
    # store them as object's metadata.
    content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
    bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition, metadata: )
  rescue Google::Cloud::InvalidArgumentError
    raise ActiveStorage::IntegrityError
  end
end

#url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {})

[ GitHub ]

  
# File 'activestorage/lib/active_storage/service/gcs_service.rb', line 92

def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
  instrument :url, key: key do |payload|
    headers = {}
    version = :v2

    if @config[:cache_control].present?
      headers["Cache-Control"] = @config[:cache_control]
      # v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
      # if necessary for back-compat; v4 limits the expiration of the URL to 7 days
      # whereas v2 has no limit
      version = :v4
    end

    headers.merge!(())

    args = {
      content_md5: checksum,
      expires: expires_in,
      headers: headers,
      method: "PUT",
      version: version,
    }

    if @config[:iam]
      args[:issuer] = issuer
      args[:signer] = signer
    end

    generated_url = bucket.signed_url(key, **args)

    payload[:url] = generated_url

    generated_url
  end
end