class ActiveStorage::Service::CloudinaryService
Attributes
Public Class Methods
Source
# File lib/active_storage/service/cloudinary_service.rb, line 199 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 39 def initialize(**options) @options = options end
Public Instance Methods
Source
# File lib/active_storage/service/cloudinary_service.rb, line 108 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 120 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 142 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 162 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
Return the partial content in the byte range
of the file at the key
.
Source
# File lib/active_storage/service/cloudinary_service.rb, line 125 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 101 def headers_for_direct_upload(key, content_type:, checksum:, **) { Headers::CONTENT_TYPE => content_type, Headers::CONTENT_MD5 => checksum, } end
Source
# File lib/active_storage/service/cloudinary_service.rb, line 190 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
Returns the public id of the asset.
Public id includes both folder(defined globally in the configuration) and the active storage key. For raw files it also includes the file extension, since that’s how Cloudinary
stores raw files.
@param [ActiveStorage::BlobKey] key The blob key with attributes. @param [ActiveStorage::Filename] filename The original filename. @param [string] content_type The content type of the file.
@return [string] The public id of the asset.
Source
# File lib/active_storage/service/cloudinary_service.rb, line 43 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 64 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 80 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
Private Instance Methods
Source
# File lib/active_storage/service/cloudinary_service.rb, line 228 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 284 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 248 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
Helper method for getting the filename extension.
It does the best effort when original filename does not include extension, but we know the mime-type.
@param [ActiveStorage::BlobKey] key The blob key with attributes. @param [ActiveStorage::Filename] filename The original filename. @param [string] content_type The content type of the file.
@return [string] The extension of the filename.
Source
# File lib/active_storage/service/cloudinary_service.rb, line 315 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
Source
# File lib/active_storage/service/cloudinary_service.rb, line 269 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
Returns the full public id including folder.
Source
# File lib/active_storage/service/cloudinary_service.rb, line 279 def public_id_internal(key) # TODO: Allow custom manipulation of key to obscure how we store in Cloudinary key end
Source
# File lib/active_storage/service/cloudinary_service.rb, line 307 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