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)
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)
-
(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 = {})
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 = '')
-
(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