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 17

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 138

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

#client

[ GitHub ]

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

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 130

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 236

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

#delete(key)

[ GitHub ]

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

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 65

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 35

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 49

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 216

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 75

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 185

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 119

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 toApplication 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 155

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 212

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 164

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 181

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

#signer (private)

[ GitHub ]

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

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 198

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_for(key, content_type:, disposition: nil, filename: nil, custom_metadata: {}) (private)

[ GitHub ]

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

def (key, content_type:, disposition: nil, filename: nil, custom_metadata: {})
  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

#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 22

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 83

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