class ActiveStorage::Service::CloudinaryService

def self.fetch_service_instance_and_config(source, options)

def self.fetch_service_instance_and_config(source, options)
  return [nil, options] unless defined?(ActiveStorage::BlobKey) && source.is_a?(ActiveStorage::BlobKey) &&
    source.respond_to?(:attributes) && source.attributes.key?(:service_name)
  service_name = source.attributes[:service_name]
  begin
    service_instance = ActiveStorage::Blob.services.fetch(service_name.to_sym)
    unless service_instance.instance_of?(ActiveStorage::Service::CloudinaryService)
      Rails.logger.error "Expected service instance #{service_instance.class.name} to be of type ActiveStorage::Service::CloudinaryService."
      return [nil, options]
    end
    service_config = Rails.application.config.active_storage.service_configurations.fetch(service_name)
    options        = service_config.merge(options)
  rescue NameError => e
    Rails.logger.error "Failed to retrieve the service instance for #{service_name}: #{e.message}"
    return [nil, options]
  rescue => e
    Rails.logger.error "An unexpected error occurred: #{e.message}"
    return [nil, options]
  end
  [service_instance, options]
end

def api_uri(action, options)

def api_uri(action, options)
  base_url = Cloudinary::Utils.cloudinary_api_url(action, options)
  upload_params = Cloudinary::Uploader.build_upload_params(options)
  upload_params.reject! { |k, v| Cloudinary::Utils.safe_blank?(v) }
  unless options[:unsigned]
    upload_params = Cloudinary::Utils.sign_request(upload_params, options)
  end
  "#{base_url}?#{upload_params.to_query}"
end

def content_type_to_resource_type(content_type)

def content_type_to_resource_type(content_type)
  return 'image' if content_type.nil?
  type, subtype = content_type.split('/')
  case type
  when 'video', 'audio'
    'video'
  when 'text', 'message'
    'raw'
  when 'application'
    case subtype
    when 'pdf', 'postscript'
      'image'
    when 'vnd.apple.mpegurl', 'x-mpegurl', 'mpegurl' # m3u8
      'video'
    else
      'raw'
    end
  else
    'image'
  end
end

def delete(key)

def delete(key)
  key = find_blob_or_use_key(key)
  instrument :delete, key: key do
    options = {
      resource_type: resource_type(nil, key),
      type: @options[:type]
    }.compact
    Cloudinary::Uploader.destroy public_id(key), **options
  end
end

def delete_prefixed(prefix)

def delete_prefixed(prefix)
  # This method is used by ActiveStorage to delete derived resources after the main resource was deleted.
  # In Cloudinary, the derived resources are deleted automatically when the main resource is deleted.
end

def download(key, &block)

def download(key, &block)
  uri = URI(url(key))
  if block_given?
    instrument :streaming_download, key: key do
      Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
        request = Net::HTTP::Get.new uri
        http.request request do |response|
          response.read_body &block
        end
      end
    end
  else
    instrument :download, key: key do
      res = Net::HTTP::get_response(uri)
      res.body
    end
  end
end

def download_chunk(key, range)

Return the partial content in the byte +range+ of the file at the +key+.
def download_chunk(key, range)
  uri = URI(url(key))
  instrument :download, key: key do
    req = Net::HTTP::Get.new(uri)
    range_end = case
                when range.end.nil? then ''
                when range.exclude_end? then range.end - 1
                else range.end
                end
    req['range'] = "bytes=#{[range.begin, range_end].join('-')}"
    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(req)
    end
    res.body.force_encoding(Encoding::BINARY)
  end
end

def exist?(key)

def exist?(key)
  key = find_blob_or_use_key(key)
  instrument :exist, key: key do |payload|
    begin
      options = {
        resource_type: resource_type(nil, key),
        type: @options[:type]
      }.compact
      Cloudinary::Api.resource public_id(key), **options
      true
    rescue Cloudinary::Api::NotFound => e
      false
    end
  end
end

def ext_for_file(key, filename = nil, content_type = nil)

Returns:
  • (string) - The extension of the filename.

Parameters:
  • content_type (string) -- The content type of the file.
  • filename (ActiveStorage::Filename) -- The original filename.
  • key (ActiveStorage::BlobKey) -- The blob key with attributes.
def ext_for_file(key, filename = nil, content_type = nil)
  if filename.blank?
    options = key.respond_to?(:attributes) ? key.attributes : {}
    filename = ActiveStorage::Filename.new(options[:filename]) if options.has_key?(:filename)
  end
  ext = filename.respond_to?(:extension_without_delimiter) ? filename.extension_without_delimiter : nil
  return ext unless ext.blank?
  # Raw files are not convertible, no extension guessing for them
  return nil if content_type_to_resource_type(content_type).eql?('raw')
  # Fallback when there is no extension.
  @formats ||= Hash.new do |h, key|
    ext = Rack::Mime::MIME_TYPES.invert[key]
    h[key] = ext.slice(1..-1) unless ext.nil?
  end
  @formats[content_type]
end

def find_blob_or_use_key(key)

def find_blob_or_use_key(key)
  if key.is_a?(ActiveStorage::BlobKey)
    key
  else
    begin
      blob = ActiveStorage::Blob.find_by(key: key)
      blob ? ActiveStorage::BlobKey.new(blob.attributes.as_json) : key
    rescue ActiveRecord::StatementInvalid => e
      # Return the original key if an error occurs
      key
    end
  end
end

def full_public_id_internal(key, options = {})

Returns the full public id including folder.
def full_public_id_internal(key, options = {})
  public_id = public_id_internal(key)
  options = @options.merge(options)
  return public_id if !options[:folder] || options.fetch(:type, "").to_s == "fetch"
  File.join(@options.fetch(:folder), public_id)
end

def headers_for_direct_upload(key, content_type:, checksum:, **)

def headers_for_direct_upload(key, content_type:, checksum:, **)
  {
    Headers::CONTENT_TYPE => content_type,
    Headers::CONTENT_MD5 => checksum,
  }
end

def initialize(**options)

def initialize(**options)
  @options = options
end

def public_id(key, filename = nil, content_type = '')

Returns:
  • (string) - The public id of the asset.

Parameters:
  • content_type (string) -- The content type of the file.
  • filename (ActiveStorage::Filename) -- The original filename.
  • key (ActiveStorage::BlobKey) -- The blob key with attributes.
def public_id(key, filename = nil, content_type = '')
  public_id = key
  if resource_type(nil, key) == 'raw'
    public_id = [key, ext_for_file(key, filename, content_type)].reject(&:blank?).join('.')
  end
  full_public_id_internal(public_id)
end

def public_id_internal(key)

def public_id_internal(key)
  # TODO: Allow custom manipulation of key to obscure how we store in Cloudinary
  key
end

def resource_type(io, key = "", content_type = "")

def resource_type(io, key = "", content_type = "")
  if content_type.blank?
    options = key.respond_to?(:attributes) ? key.attributes : {}
    content_type = options[:content_type] || (io.nil? ? '' : Marcel::MimeType.for(io))
  end
  content_type_to_resource_type(content_type)
end

def upload(key, io, filename: nil, checksum: nil, **options)

def upload(key, io, filename: nil, checksum: nil, **options)
  instrument :upload, key: key, checksum: checksum do
    begin
      extra_headers = checksum.nil? ? {} : {Headers::CONTENT_MD5 => checksum}
      options = @options.merge(options)
      resource_type = resource_type(io, key)
      options[:format] = ext_for_file(key) if resource_type == "raw"
      Cloudinary::Uploader.upload_large(
        io,
        public_id: public_id_internal(key),
        resource_type: resource_type,
        context: {active_storage_key: key, checksum: checksum},
        extra_headers: extra_headers,
        **options
      )
    rescue CloudinaryException => e
      raise ActiveStorage::IntegrityError, e.message, e.backtrace
    end
  end
end

def url(key, filename: nil, content_type: '', **options)

def url(key, filename: nil, content_type: '', **options)
  key = find_blob_or_use_key(key)
  instrument :url, key: key do |payload|
    url = Cloudinary::Utils.cloudinary_url(
      full_public_id_internal(key, options),
      resource_type: resource_type(nil, key, content_type),
      format: ext_for_file(key, filename, content_type),
      **@options.merge(options.symbolize_keys)
    )
    payload[:url] = url
    url
  end
end

def url_for_direct_upload(key, **options)

def url_for_direct_upload(key, **options)
  instrument :url, key: key do |payload|
    options = @options.merge(options.symbolize_keys)
    options[:resource_type] ||= resource_type(nil, key, options[:content_type])
    options[:public_id] = public_id_internal(key)
    # Provide file format for raw files, since js client does not include original file name.
    #
    # When the file is uploaded from the server, the request includes original filename. That allows Cloudinary
    # to identify file extension and append it to the public id of the file (raw files include file extension
    # in their public id, opposed to transformable assets (images/video) that use only basename). When uploading
    # through direct upload (client side js), filename is missing, and that leads to inconsistent/broken URLs.
    # To avoid that, we explicitly pass file format in options.
    options[:format] = ext_for_file(key) if options[:resource_type] == "raw"
    context = options.delete(:context)
    options[:context] = {active_storage_key: key}
    options[:context].reverse_merge!(context) if context.respond_to?(:merge)
    options.delete(:file)
    payload[:url] = api_uri("upload", options)
  end
end