diff --git a/lib/paperclip.rb b/lib/paperclip.rb index 7989ac4..e87be76 100644 --- a/lib/paperclip.rb +++ b/lib/paperclip.rb @@ -42,12 +42,21 @@ require 'paperclip/matchers' require 'paperclip/callback_compatability' require 'paperclip/railtie' if defined?(Rails) +require 'active_support/concern' +require 'active_support/core_ext/class/attribute' + # The base module that gets included in ActiveRecord::Base. See the # documentation for Paperclip::ClassMethods for more useful information. module Paperclip - VERSION = "2.2.9.2" + extend ActiveSupport::Concern + + included do + class_attribute :attachment_definitions + Paperclip::CallbackCompatability.install_to(self) + end + class << self # Provides configurability to Paperclip. There are a number of options available, such as: # * whiny: Will raise an error if Paperclip cannot process thumbnails of @@ -109,17 +118,6 @@ module Paperclip File.exist?("/dev/null") ? "/dev/null" : "NUL" end - def included base #:nodoc: - base.extend ClassMethods - base.class_attribute :attachment_definitions - - if base.respond_to?(:set_callback) - base.send :include, Paperclip::CallbackCompatability::Rails3 - else - base.send :include, Paperclip::CallbackCompatability::Rails21 - end - end - def processor name #:nodoc: name = name.to_s.camelize processor = Paperclip.const_get(name) @@ -217,11 +215,7 @@ module Paperclip def has_attached_file name, options = {} include InstanceMethods - if attachment_definitions.nil? - self.attachment_definitions = {} - else - self.attachment_definitions = self.attachment_definitions.dup - end + self.attachment_definitions = self.attachment_definitions&.dup || {} attachment_definitions[name] = {:validations => []}.merge(options) after_save :save_attached_files @@ -258,16 +252,13 @@ module Paperclip # * +if+: A lambda or name of a method on the instance. Validation will only # be run is this lambda or method returns true. # * +unless+: Same as +if+ but validates if lambda or method returns false. - def validates_attachment_size name, options = {} + def validates_attachment_size(name, **options) min = options[:greater_than] || (options[:in] && options[:in].first) || 0 max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0) - range = (min..max) - message = options[:message] || "file size must be between :min and :max bytes." - - attachment_definitions[name][:validations] << [:size, {:range => range, - :message => message, - :if => options[:if], - :unless => options[:unless]}] + _add_attachment_validation(name, :size, options, + message: "file size must be between :min and :max bytes.", + range: (min..max) + ) end # Places ActiveRecord-style validations on the presence of a file. @@ -275,12 +266,8 @@ module Paperclip # * +if+: A lambda or name of a method on the instance. Validation will only # be run is this lambda or method returns true. # * +unless+: Same as +if+ but validates if lambda or method returns false. - def validates_attachment_presence name, options = {} - message = options[:message] || :blank - - attachment_definitions[name][:validations] << [:presence, {:message => message, - :if => options[:if], - :unless => options[:unless]}] + def validates_attachment_presence(name, **options) + _add_attachment_validation(name, :presence, options, message: :blank) end # Places ActiveRecord-style validations on the content type of the file @@ -299,15 +286,16 @@ module Paperclip # NOTE: If you do not specify an [attachment]_content_type field on your # model, content_type validation will work _ONLY upon assignment_ and # re-validation after the instance has been reloaded will always succeed. - def validates_attachment_content_type name, options = {} - attachment_definitions[name][:validations] << [:content_type, {:content_type => options[:content_type], - :message => options[:message], - :if => options[:if], - :unless => options[:unless]}] + def validates_attachment_content_type(name, content_type:, **options) + _add_attachment_validation(name, :content_type, options, content_type: content_type) end - def attachment_definitions - self.attachment_definitions + def _add_attachment_validation(name, type, default_options, options) + attachment_definitions[name][:validations] << [ + type, + **options, + **default_options.slice(:message, :if, :unless) + ] end end diff --git a/lib/paperclip/attachment.rb b/lib/paperclip/attachment.rb index 8a9e604..cca3f48 100644 --- a/lib/paperclip/attachment.rb +++ b/lib/paperclip/attachment.rb @@ -1,5 +1,7 @@ require 'fastimage' +require 'paperclip/styles_parser' + module Paperclip # The Attachment class manages the files for a given attachment. It saves # when the model saves, deletes when the model is destroyed, and processes @@ -14,13 +16,11 @@ module Paperclip @default_options ||= { :url => "/system/:attachment/:id/:style/:filename", :path => ":rails_root/public:url", - :style_order => [], :styles => {}, :default_url => "/:attachment/:style/missing.png", :default_style => :original, :validations => [], :storage => :filesystem, -# :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails], :whiny => true, :restricted_characters => /[^\w\p{Word}\d\.\-]|(^\.{0,2}$)+/, :filename_sanitizer => nil @@ -48,8 +48,7 @@ module Paperclip attachment_class_cache[storage].new(name, instance, options) end - attr_reader :name, :instance, :style_order, :styles, :default_style, - :convert_options, :queued_for_write, :options + attr_reader :name, :instance, :styles, :default_style, :queued_for_write, :options attr_accessor :post_processing @@ -63,20 +62,13 @@ module Paperclip options = Attachment.default_options.merge(options) @url = options[:url] - @url = @url.call(self) if @url.is_a?(Proc) @path = options[:path] - @path = @path.call(self) if @path.is_a?(Proc) - @style_order = options[:style_order] - @style_order = @style_order.call(self) if @style_order.is_a?(Proc) - @styles = options[:styles] - @styles = @styles.call(self) if @styles.is_a?(Proc) + @styles = StylesParser.new(options).styles @default_url = options[:default_url] @validations = options[:validations] @default_style = options[:default_style] @storage = options[:storage] @whiny = options[:whiny_thumbnails] || options[:whiny] - @convert_options = options[:convert_options] || {} - @processors = options[:processors] || [:thumbnail] @options = options @queued_for_delete = [] @queued_for_write = {} @@ -86,8 +78,6 @@ module Paperclip @post_processing = true @processing_url = options[:processing_url] || @default_url - - normalize_style_definition end # What gets called when you call instance.attachment = File. It clears @@ -159,10 +149,6 @@ module Paperclip !sizes || (sizes[0] <= MAX_IMAGE_RESOLUTION && sizes[1] <= MAX_IMAGE_RESOLUTION) end - def most_appropriate_url - # stub for delayed_paperclip - end - # Returns the public URL of the attachment, with a given style. Note that # this does not necessarily need to point to a file that your web server # can access and can point to an action in your app, if you need fine @@ -274,18 +260,6 @@ module Paperclip time && time.to_i end - # Paths and URLs can have a number of variables interpolated into them - # to vary the storage location based on name, id, style, class, etc. - # This method is a deprecated access into supplying and retrieving these - # interpolations. Future access should use either Paperclip.interpolates - # or extend the Paperclip::Interpolations module directly. - def self.interpolations - warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' + - 'and will be removed from future versions. ' + - 'Use Paperclip.interpolates instead') - Paperclip::Interpolations - end - def sanitize_filename(file_name) file_name = file_name.strip file_name.gsub!(@options[:restricted_characters], '_') if @options[:restricted_characters] @@ -415,48 +389,9 @@ module Paperclip end end - def normalize_style_definition #:nodoc: - styles.each do |name, args| - unless args.is_a? Hash - dimensions, format = [args, nil].flatten[0..1] - format = nil if format.blank? - @styles[name] = { - :processors => @processors, - :geometry => dimensions, - :format => format, - :whiny => @whiny, - :convert_options => extra_options_for(name) - } - else - @styles[name] = { - :processors => @processors, - :whiny => @whiny, - :convert_options => extra_options_for(name) - }.merge(@styles[name]) - end - end - end - - def solidify_style_definitions #:nodoc: - @styles.each do |name, args| - @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call) - @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call) - end - end - - def extra_options_for(style) #:nodoc: - all_options = convert_options[:all] - all_options = all_options.call(instance) if all_options.respond_to?(:call) - style_options = convert_options[style] - style_options = style_options.call(instance) if style_options.respond_to?(:call) - - [ style_options, all_options ].compact.join(" ") - end - def post_process #:nodoc: return unless content_type.match(/image/) return if @queued_for_write[:original].nil? - solidify_style_definitions instance.run_paperclip_callbacks(:post_process) do instance.run_paperclip_callbacks(:"#{name}_post_process") do @@ -466,8 +401,7 @@ module Paperclip end def post_process_styles #:nodoc: - styles_in_order = @style_order.empty? ? @styles : @styles.sort_by{|s| @style_order.index(s.first)} - styles_in_order.each do |name, args| + styles.each do |name, args| begin raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank? @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor| diff --git a/lib/paperclip/callback_compatability.rb b/lib/paperclip/callback_compatability.rb index eb9fe58..1e437d7 100644 --- a/lib/paperclip/callback_compatability.rb +++ b/lib/paperclip/callback_compatability.rb @@ -1,11 +1,14 @@ module Paperclip module CallbackCompatability - module Rails21 - def self.included(base) - base.extend(Defining) - base.send(:include, Running) - end + module_function + def install_to(base) + mod = base.respond_to?(:set_callback) ? Rails3 : Rails21 + base.extend(mod::Defining) + base.send(:include, mod::Running) + end + + module Rails21 module Defining def define_paperclip_callbacks(*args) args.each do |callback| @@ -28,29 +31,24 @@ module Paperclip end module Rails3 - def self.included(base) - base.extend(Defining) - base.send(:include, Running) - end - module Defining + TERMINATOR = + if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new('4.1') + ->(target, result) { result == false } + else + 'result == false' + end + def define_paperclip_callbacks(*callbacks) - terminator = - if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new('4.1') - ->(target, result) { result == false } - else - 'result == false' + define_callbacks *[callbacks, {terminator: TERMINATOR}].flatten + callbacks.map(&:to_sym).each do |callback| + define_singleton_method "before_#{callback}" do |*args, &blk| + set_callback(callback, :before, *args, &blk) + end + + define_singleton_method "after_#{callback}" do |*args, &blk| + set_callback(callback, :after, *args, &blk) end - define_callbacks *[callbacks, {terminator: terminator}].flatten - callbacks.each do |callback| - eval <<-end_callbacks - def before_#{callback}(*args, &blk) - set_callback(:#{callback}, :before, *args, &blk) - end - def after_#{callback}(*args, &blk) - set_callback(:#{callback}, :after, *args, &blk) - end - end_callbacks end end end @@ -60,9 +58,7 @@ module Paperclip run_callbacks(callback, &block) end end - end - end end diff --git a/lib/paperclip/storage/delayeds3.rb b/lib/paperclip/storage/delayeds3.rb index 72cf2d1..f8abaac 100644 --- a/lib/paperclip/storage/delayeds3.rb +++ b/lib/paperclip/storage/delayeds3.rb @@ -14,9 +14,8 @@ module Paperclip end def parse_credentials creds - return @parsed_credentials if @parsed_credentials creds = find_credentials(creds).stringify_keys - @parsed_credentials ||= (creds[Rails.env] || creds).symbolize_keys + (creds[Rails.env] || creds).symbolize_keys end def find_credentials creds @@ -33,12 +32,6 @@ module Paperclip end end - class WriteToS3Job < Struct.new(:class_name, :name, :id) - def perform - WriteToS3Worker.new.perform(class_name, name, id) - end - end - class UploadWorker include ::Sidekiq::Worker sidekiq_options queue: :paperclip @@ -81,10 +74,6 @@ module Paperclip @s3_credentials = Delayeds3.parse_credentials(@options[:s3_credentials]) @bucket = @options[:bucket] || @s3_credentials[:bucket] - @bucket = @bucket.call(self) if @bucket.is_a?(Proc) - @s3_permissions = @options[:s3_permissions] || 'public-read' - @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https') - @s3_host_alias = @options[:s3_host_alias] @fog_provider = @options[:fog_provider] @fog_directory = @options[:fog_directory] @@ -141,10 +130,6 @@ module Paperclip @bucket end - def s3_host_alias - @s3_host_alias - end - def synced_to_s3_field @synced_to_s3_field ||= "#{name}_synced_to_s3".freeze end @@ -166,10 +151,6 @@ module Paperclip alias_method :to_io, :to_file - def s3_protocol - @s3_protocol - end - def exists?(style = default_style) File.exist?(filesystem_path(style)) end @@ -181,7 +162,8 @@ module Paperclip def filesystem_paths h = {} [:original, *@styles.keys].uniq.map do |style| - h[style] = filesystem_path(style) if File.exist?(filesystem_path(style)) + path = filesystem_path(style) + h[style] = path if File.exist?(path) end h end @@ -237,8 +219,9 @@ module Paperclip end end - def flush_writes #:nodoc: + return if @queued_for_write.empty? + @queued_for_write.each do |style, file| file.close FileUtils.mkdir_p(File.dirname(filesystem_path(style))) @@ -247,7 +230,7 @@ module Paperclip FileUtils.chmod(0644, filesystem_path(style)) end - unless @queued_for_write.empty? || (delay_processing? && dirty?) + unless delay_processing? && dirty? instance.update_column(synced_to_s3_field, false) if instance_read(:synced_to_s3) if instance.respond_to?(synced_to_fog_field) && instance_read(:synced_to_fog) instance.update_column(synced_to_fog_field, false) @@ -285,10 +268,9 @@ module Paperclip def flush_deletes #:nodoc: # если мы картинку заливали в облака, значит мы скорее всего ее уже удалили # и можно не нагружать хранилище проверками - if !instance.is_a?(AccountFile) && instance_read(:synced_to_fog) && - instance_read(:synced_to_s3) - @queued_for_delete = [] - return + if instance_read(:synced_to_fog) && instance_read(:synced_to_s3) + @queued_for_delete = [] + return end @queued_for_delete.each do |path| @@ -299,7 +281,6 @@ module Paperclip end def delete_local_files! - return if instance.is_a?(AccountFile) instance.reload if instance_read(:synced_to_fog) && instance_read(:synced_to_s3) filesystem_paths.values.each do |filename| diff --git a/lib/paperclip/storage/filesystem.rb b/lib/paperclip/storage/filesystem.rb index 8662eae..39cb911 100644 --- a/lib/paperclip/storage/filesystem.rb +++ b/lib/paperclip/storage/filesystem.rb @@ -34,10 +34,11 @@ module Paperclip def flush_writes #:nodoc: @queued_for_write.each do |style, file| file.close - FileUtils.mkdir_p(File.dirname(path(style))) - log("saving #{path(style)}") - FileUtils.mv(file.path, path(style)) - FileUtils.chmod(0644, path(style)) + filename = path(style) + FileUtils.mkdir_p(File.dirname(filename)) + log("saving #{filename}") + FileUtils.mv(file.path, filename) + FileUtils.chmod(0644, filename) end @queued_for_write = {} end diff --git a/lib/paperclip/styles_parser.rb b/lib/paperclip/styles_parser.rb new file mode 100644 index 0000000..2359464 --- /dev/null +++ b/lib/paperclip/styles_parser.rb @@ -0,0 +1,42 @@ +module Paperclip + class StylesParser + attr_reader :styles, :convert_options, :processors, :whiny + + def initialize(options) + @styles = options[:styles] + @convert_options = options[:convert_options] || {} + @processors = options[:processors] || [:thumbnail] + @whiny = options[:whiny_thumbnails] || options[:whiny] + + normalize_style_definition + end + + def normalize_style_definition #:nodoc: + styles.each do |name, args| + styles[name] = + if args.is_a? Hash + { + processors: processors, + whiny: whiny, + convert_options: extra_options_for(name) + }.merge(args) + else + dimensions, format = args + { + processors: processors, + geometry: dimensions, + format: format.presence, + whiny: whiny, + convert_options: extra_options_for(name) + } + end + end + end + + def extra_options_for(style) #:nodoc: + all_options = convert_options[:all] + style_options = convert_options[style] + [style_options, all_options].compact.join(' ') + end + end +end diff --git a/paperclip.gemspec b/paperclip.gemspec index be76b92..4818a22 100644 --- a/paperclip.gemspec +++ b/paperclip.gemspec @@ -17,5 +17,6 @@ Gem::Specification.new do |s| s.rubygems_version = %q{1.3.1} s.summary = %q{File attachments as attributes for ActiveRecord} + s.add_dependency 'activesupport' s.add_dependency 'fastimage' end