123456789_123456789_123456789_123456789_123456789_

Class: ActiveStorage::Service::GCSService

Relationships & Source Files
Namespace Children
Exceptions:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Service
Instance Chain:
self, Service
Inherits: Service
  • ::Object
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

Instance Method Summary

Constructor Details

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

[ GitHub ]

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

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

Instance Method Details

#bucket

[ GitHub ]

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

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

#client

[ GitHub ]

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

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 235

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 215

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 192

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 162

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 211

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 171

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 188

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

#signer (private)

[ GitHub ]

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

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 197

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