require "active_support/core_ext/object/deep_dup" module CarrierWave module Uploader module Versions class Builder def initialize(name) @name = name @options = {} @blocks = [] @klass = nil end def configure(options, &block) @options.merge!(options) @blocks << block if block @klass = nil end def build(superclass) return @klass if @klass @klass = Class.new(superclass) superclass.const_set("VersionUploader#{@name.to_s.camelize}", @klass) @klass.version_names += [@name] @klass.versions = {} @klass.processors = [] @klass.version_options = @options @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1 # Define the enable_processing method for versions so they get the # value from the parent class unless explicitly overwritten def self.enable_processing(value=nil) self.enable_processing = value if value if defined?(@enable_processing) && !@enable_processing.nil? @enable_processing else superclass.enable_processing end end # Regardless of what is set in the parent uploader, do not enforce the # move_to_cache config option on versions because it moves the original # file to the version's target file. # # If you want to enforce this setting on versions, override this method # in each version: # # version :thumb do # def move_to_cache # true # end # end # def move_to_cache false end # Need to rely on the parent version's identifier, as versions don't have its own one. def identifier parent_version.identifier end RUBY @blocks.each { |block| @klass.class_eval(&block) } @klass end def deep_dup other = dup other.instance_variable_set(:@blocks, @blocks.dup) other end def method_missing(name, *args) super rescue NoMethodError => e raise e.exception <<~ERROR #{e.message} If you're trying to configure a version, do it inside a block like `version(:thumb) { self.#{name} #{args.map(&:inspect).join(', ')} }`. ERROR end def respond_to_missing?(*) super end end extend ActiveSupport::Concern include CarrierWave::Uploader::Callbacks included do class_attribute :versions, :version_names, :version_options, :instance_reader => false, :instance_writer => false self.versions = {} self.version_names = [] attr_accessor :parent_version after :cache, :cache_versions! after :store, :store_versions! after :remove, :remove_versions! after :retrieve_from_cache, :retrieve_versions_from_cache! after :retrieve_from_store, :retrieve_versions_from_store! prepend Module.new { def initialize(*) super @versions = nil end } end module ClassMethods ## # Adds a new version to this uploader # # === Parameters # # [name (#to_sym)] name of the version # [options (Hash)] optional options hash # [&block (Proc)] a block to eval on this version of the uploader # # === Examples # # class MyUploader < CarrierWave::Uploader::Base # # version :thumb do # process :scale => [200, 200] # end # # version :preview, :if => :image? do # process :scale => [200, 200] # end # # version :square, :unless => :invalid_image_type? do # process :scale => [100, 100] # end # # end # def version(name, options = {}, &block) name = name.to_sym versions[name] ||= Builder.new(name) versions[name].configure(options, &block) class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} versions[:#{name}] end RUBY versions[name] end private def inherited(subclass) # To prevent subclass version changes affecting superclass versions subclass.versions = versions.deep_dup super end end # ClassMethods ## # Returns a hash mapping the name of each version of the uploader to an instance of it # # === Returns # # [Hash{Symbol => CarrierWave::Uploader}] a list of uploader instances # def versions return @versions if @versions @versions = {} self.class.versions.each do |name, version| @versions[name] = version.build(self.class).new(model, mounted_as) @versions[name].parent_version = self end @versions end ## # === Returns # # [String] the name of this version of the uploader # def version_name self.class.version_names.join('_').to_sym unless self.class.version_names.blank? end ## # # === Parameters # # [name (#to_sym)] name of the version # # === Returns # # [Boolean] True when the version satisfy its :if or :unless condition # def version_active?(name) name = name.to_sym return false unless versions.has_key?(name) if_condition = versions[name].class.version_options[:if] unless_condition = versions[name].class.version_options[:unless] if if_condition if if_condition.respond_to?(:call) if_condition.call(self, :version => name, :file => file) else send(if_condition, file) end elsif unless_condition if unless_condition.respond_to?(:call) !unless_condition.call(self, :version => name, :file => file) else !send(unless_condition, file) end else true end end alias_method :version_exists?, :version_active? CarrierWave.deprecator.deprecate_methods(self, version_exists?: :version_active?) ## # Copies the parent's cache_id when caching a version file. # This behavior is not essential but it makes easier to understand # that the cached files are generated by the single upload attempt. # def cache!(*args) self.cache_id = parent_version.cache_id if parent_version super end ## # When given a version name as a parameter, will return the url for that version # This also works with nested versions. # When given a query hash as a parameter, will return the url with signature that contains query params # Query hash only works with AWS (S3 storage). # # === Example # # my_uploader.url # => /path/to/my/uploader.gif # my_uploader.url(:thumb) # => /path/to/my/thumb_uploader.gif # my_uploader.url(:thumb, :small) # => /path/to/my/thumb_small_uploader.gif # my_uploader.url(:query => {"response-content-disposition" => "attachment"}) # my_uploader.url(:version, :sub_version, :query => {"response-content-disposition" => "attachment"}) # # === Parameters # # [*args (Symbol)] any number of versions # OR/AND # [Hash] query params # # === Returns # # [String] the location where this file is accessible via a url # def url(*args) if (version = args.first) && version.respond_to?(:to_sym) raise ArgumentError, "Version #{version} doesn't exist!" if versions[version.to_sym].nil? # recursively proxy to version versions[version.to_sym].url(*args[1..-1]) elsif args.first super(args.first) else super end end ## # Recreate versions and reprocess them. This can be used to recreate # versions if their parameters somehow have changed. # def recreate_versions!(*names) # As well as specified versions, we need to reprocess versions # that are the source of another version. self.cache_id = CarrierWave.generate_cache_id derived_versions.each_value do |v| v.cache!(file) if names.empty? || !(v.descendant_version_names & names).empty? end active_versions.each do |name, v| v.store! if names.empty? || names.include?(name) end ensure @cache_id = nil end protected def descendant_version_names [version_name] + derived_versions.flat_map do |name, version| version.descendant_version_names end end def active_versions versions.select do |name, uploader| version_active?(name) end end private def derived_versions active_versions.reject do |name, v| v.class.version_options[:from_version] end.merge(active_sibling_versions.select do |name, v| v.class.version_options[:from_version] == self.class.version_names.last end) end def active_sibling_versions parent_version&.active_versions || {} end def full_filename(for_file) [version_name, super(for_file)].compact.join('_') end def full_original_filename [version_name, super].compact.join('_') end def cache_versions!(new_file) derived_versions.each_value { |v| v.cache!(new_file) } end def store_versions!(new_file) active_versions.each_value { |v| v.store!(new_file) } end def remove_versions! versions.each_value { |v| v.remove! } end def retrieve_versions_from_cache!(cache_name) active_versions.each_value { |v| v.retrieve_from_cache!(cache_name) } end def retrieve_versions_from_store!(identifier) active_versions.each_value { |v| v.retrieve_from_store!(identifier) } end end # Versions end # Uploader end # CarrierWave