From ef9fabacdd9621e04099e06bddf0b5fa023011df Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Wed, 21 Nov 2018 18:03:15 +0300 Subject: [PATCH] Extract attachment options to class level --- lib/paperclip.rb | 7 +- lib/paperclip/attachment.rb | 105 ++++++++++++------------- lib/paperclip/interpolations.rb | 2 +- lib/paperclip/storage/delayeds3.rb | 119 +++++++++++++++-------------- test/interpolations_test.rb | 2 +- test/test_helper.rb | 2 +- 6 files changed, 123 insertions(+), 114 deletions(-) diff --git a/lib/paperclip.rb b/lib/paperclip.rb index e87be76..20bac91 100644 --- a/lib/paperclip.rb +++ b/lib/paperclip.rb @@ -216,7 +216,8 @@ module Paperclip include InstanceMethods self.attachment_definitions = self.attachment_definitions&.dup || {} - attachment_definitions[name] = {:validations => []}.merge(options) + attachment_definitions[name] = Attachment.build_class(name, options) + const_set("#{name}_attachment".camelize, attachment_definitions[name]) after_save :save_attached_files after_commit :destroy_attached_files, on: :destroy @@ -291,7 +292,7 @@ module Paperclip end def _add_attachment_validation(name, type, default_options, options) - attachment_definitions[name][:validations] << [ + attachment_definitions[name].validations << [ type, **options, **default_options.slice(:message, :if, :unless) @@ -302,7 +303,7 @@ module Paperclip module InstanceMethods #:nodoc: def attachment_for name @_paperclip_attachments ||= {} - @_paperclip_attachments[name] ||= Attachment.build(name, self, self.class.attachment_definitions[name]) + @_paperclip_attachments[name] ||= self.class.attachment_definitions[name].new(self) end def each_attachment diff --git a/lib/paperclip/attachment.rb b/lib/paperclip/attachment.rb index 0e9ea94..6bd1a82 100644 --- a/lib/paperclip/attachment.rb +++ b/lib/paperclip/attachment.rb @@ -19,7 +19,6 @@ module Paperclip :styles => {}, :default_url => "/:attachment/:style/missing.png", :default_style => :original, - :validations => [], :storage => :filesystem, :whiny => true, :restricted_characters => /[^\w\p{Word}\d\.\-]|(^\.{0,2}$)+/, @@ -27,53 +26,57 @@ module Paperclip } end - def self.attachment_class_cache - @attachment_class_cache ||= Hash.new do |hash, storage| - storage_name = storage.to_s.downcase.camelize - unless Storage.const_defined?(storage_name, false) - raise "Cannot load storage module '#{storage_name}'" + class << self + # Every attachment definition creates separate class which stores configuration. + # This class is instantiated later with model instance. + def build_class(name, options) + options = default_options.merge(options) + storage_name = options.fetch(:storage).to_s.downcase.camelize + storage_module = Storage.const_get(storage_name, false) + Class.new(self) do + include storage_module + setup(name, options) end - hash[storage] = - if storage_name == storage - storage_module = Storage.const_get(storage_name) - Class.new(self) { include(storage_module) }.tap { |x| const_set(storage_name, x) } - else - hash[storage_name] - end end + + # Basic attrs + attr_reader :attachment_name, :options, :validations + + # Extracted from options + attr_reader :url_template, :path_template, :default_url, :processing_url, :styles, :default_style, :whiny + + def setup(name, options) + @attachment_name = name + @options = options + @validations = [] + @url_template = options[:url] + @path_template = options[:path] + @default_url = options[:default_url] + @processing_url = options[:processing_url] || default_url + @styles = StylesParser.new(options).styles + @default_style = options[:default_style] + @whiny = options[:whiny_thumbnails] || options[:whiny] + end + + # For delayed_paperclip + delegate :[], :[]=, to: :options end - def self.build(name, instance, options = {}) - storage = options[:storage] || default_options[:storage] - attachment_class_cache[storage].new(name, instance, options) - end + delegate :options, :styles, :default_style, to: :class - attr_reader :name, :instance, :options, :url_template, :path_template, - :styles, :default_style, :default_url, :validations, :whiny + attr_reader :instance attr_accessor :post_processing # Creates an Attachment object. +name+ is the name of the attachment, - # +instance+ is the ActiveRecord object instance it's attached to, and - # +options+ is the same as the hash passed to +has_attached_file+. - def initialize name, instance, options = {} - @name = name - @instance = instance + # +instance+ is the ActiveRecord object instance it's attached to. + def initialize(instance) + @instance = instance + @post_processing = true + end - options = Attachment.default_options.merge(options) - - @url_template = options[:url] - @path_template = options[:path] - @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] - @options = options - - @post_processing = true - @processing_url = options[:processing_url] || default_url + def name + self.class.attachment_name end def queued_for_delete @@ -120,11 +123,9 @@ module Paperclip queued_for_write[:original] = to_tempfile(uploaded_file) - file_name = if options[:filename_sanitizer] - options[:filename_sanitizer].call uploaded_file.original_filename, self - else - sanitize_filename uploaded_file.original_filename - end + file_name = uploaded_file.original_filename + sanitizer = self.class.options[:filename_sanitizer] + file_name = sanitizer ? sanitizer.call(file_name, self) : sanitize_filename(file_name) instance_write(:file_name, file_name) instance_write(:content_type, uploaded_file.content_type.to_s.strip) @@ -164,17 +165,17 @@ module Paperclip # update time appended to the url def url style = default_style, include_updated_timestamp = true # for delayed_paperclip - return interpolate(processing_url, style) if instance.try("#{name}_processing?") - interpolate_url(url_template, style, include_updated_timestamp) + return interpolate(self.class.processing_url, style) if instance.try("#{name}_processing?") + interpolate_url(self.class.url_template, style, include_updated_timestamp) end # Метод необходим в ассетах def filesystem_url style = default_style, include_updated_timestamp = true - interpolate_url(url_template, style, include_updated_timestamp) + interpolate_url(self.class.url_template, style, include_updated_timestamp) end def interpolate_url(template, style, include_updated_timestamp) - url = original_filename.nil? ? interpolate(default_url, style) : interpolate(template, style) + url = original_filename.nil? ? interpolate(self.class.default_url, style) : interpolate(template, style) include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url end @@ -184,7 +185,7 @@ module Paperclip # URL, and the :bucket option refers to the S3 bucket. def path style = default_style return if original_filename.nil? - interpolate(path_template, style) + interpolate(self.class.path_template, style) end alias_method :filesystem_path, :path @@ -268,7 +269,9 @@ module Paperclip def sanitize_filename(file_name) file_name = file_name.strip - file_name.gsub!(options[:restricted_characters], '_') if options[:restricted_characters] + + restricted_characters = self.class.options[:restricted_characters] + file_name.gsub!(restricted_characters, '_') if restricted_characters # Укорачиваем слишком длинные имена файлов. if file_name.length > MAX_FILE_NAME_LENGTH @@ -342,7 +345,7 @@ module Paperclip def validate #:nodoc: return if @validated - validations.each do |validation| + self.class.validations.each do |validation| name, options = validation error = send(:"validate_#{name}", options) if allow_validation?(options) errors[name] = error if error @@ -404,7 +407,7 @@ module Paperclip end rescue PaperclipError => e log("An error was received while processing: #{e.inspect}") - (errors[:processing] ||= []) << e.message if whiny + (errors[:processing] ||= []) << e.message if self.class.whiny end end end diff --git a/lib/paperclip/interpolations.rb b/lib/paperclip/interpolations.rb index 7df1fbf..d6d8f47 100644 --- a/lib/paperclip/interpolations.rb +++ b/lib/paperclip/interpolations.rb @@ -47,7 +47,7 @@ module Paperclip # So it just interpolates :url template without checking if preocessing and # file existence. def url attachment, style_name - interpolate(attachment.options[:url], attachment, style_name) + interpolate(attachment.class.url_template, attachment, style_name) end # Returns the timestamp as defined by the _updated_at field diff --git a/lib/paperclip/storage/delayeds3.rb b/lib/paperclip/storage/delayeds3.rb index 4d6b834..d933df5 100644 --- a/lib/paperclip/storage/delayeds3.rb +++ b/lib/paperclip/storage/delayeds3.rb @@ -1,16 +1,18 @@ +require "sidekiq" +begin + require "aws-sdk-s3" +rescue LoadError => e + e.message << " (You may need to install the aws-sdk-s3 gem)" + raise e +end + module Paperclip module Storage # Need to create boolean field synced_to_s3 module Delayeds3 class << self - def included(*) - require "sidekiq" - begin - require "aws-sdk-s3" - rescue LoadError => e - e.message << " (You may need to install the aws-sdk-s3 gem)" - raise e - end + def included(base) + base.extend(ClassMethods) end def parse_credentials creds @@ -32,6 +34,47 @@ module Paperclip end end + module ClassMethods + attr_reader :s3_url_template, :s3_path_template, + :filesystem_url_template, :filesystem_path_template, + :s3_credentials, :s3_bucket, + :fog_provider, :fog_credentials, :fog_directory, + :synced_to_s3_field, :synced_to_fog_field + + def setup(*) + super + + @s3_url_template = options[:s3_url] + @s3_path_template = options[:s3_path] + @filesystem_url_template = options[:filesystem_url] + @filesystem_path_template = options[:filesystem_path] + + @s3_credentials = Delayeds3.parse_credentials(options[:s3_credentials]) + @s3_bucket = options[:bucket] || @s3_credentials[:bucket] + + @fog_provider = options[:fog_provider] + @fog_directory = options[:fog_directory] + @fog_credentials = options[:fog_credentials].symbolize_keys + + @synced_to_s3_field ||= :"#{attachment_name}_synced_to_s3" + @synced_to_fog_field ||= :"#{attachment_name}_synced_to_fog" + end + + def fog_storage + @fog_storage ||= Fog::Storage.new(fog_credentials.merge(provider: fog_provider)) + end + + def aws_bucket + @aws_bucket ||= begin + params = s3_credentials.reject { |_k, v| v.blank? } + params[:region] ||= 'us-east-1' + s3_client = Aws::S3::Client.new(params) + s3_resource = Aws::S3::Resource.new(client: s3_client) + s3_resource.bucket(s3_bucket) + end + end + end + class UploadWorker include ::Sidekiq::Worker sidekiq_options queue: :paperclip @@ -69,40 +112,34 @@ module Paperclip end end + delegate :synced_to_s3_field, :synced_to_fog_field, to: :class + def initialize(*) super - - @s3_credentials = Delayeds3.parse_credentials(options[:s3_credentials]) - @bucket = options[:bucket] || @s3_credentials[:bucket] - - @fog_provider = options[:fog_provider] - @fog_directory = options[:fog_directory] - @fog_credentials = options[:fog_credentials] - - @queued_jobs = [] + @queued_jobs = [] end def url(style = default_style, include_updated_timestamp = true) # for delayed_paperclip - return interpolate(processing_url, style) if instance.try("#{name}_processing?") - template = instance_read(:synced_to_s3) ? options[:s3_url] : options[:filesystem_url] + return interpolate(self.class.processing_url, style) if instance.try("#{name}_processing?") + template = instance_read(:synced_to_s3) ? self.class.s3_url_template : self.class.filesystem_url_template interpolate_url(template, style, include_updated_timestamp) end # Метод необходим в ассетах def filesystem_url(style = default_style, include_updated_timestamp = true) - interpolate_url(options[:filesystem_url], style, include_updated_timestamp) + interpolate_url(self.class.filesystem_url_template, style, include_updated_timestamp) end def path(style = default_style) return if original_filename.nil? - path = instance_read(:synced_to_s3) ? options[:s3_path] : options[:filesystem_path] + path = instance_read(:synced_to_s3) ? self.class.s3_path_template : self.class.filesystem_path_template interpolate(path, style) end def filesystem_path(style = default_style) return if original_filename.nil? - interpolate(options[:filesystem_path], style) + interpolate(self.class.filesystem_path_template, style) end def reprocess! @@ -110,34 +147,6 @@ module Paperclip flush_jobs end - def aws_bucket - return @aws_bucket if @aws_bucket - - params = { region: @s3_credentials[:region] || 'us-east-1', - access_key_id: @s3_credentials[:access_key_id], - secret_access_key: @s3_credentials[:secret_access_key] } - - params[:endpoint] = @s3_credentials[:endpoint] if @s3_credentials[:endpoint].present? - params[:http_proxy] = @s3_credentials[:http_proxy] if @s3_credentials[:http_proxy].present? - - s3_client = Aws::S3::Client.new(params) - - s3_resource = Aws::S3::Resource.new(client: s3_client) - @aws_bucket = s3_resource.bucket(bucket_name) - end - - def bucket_name - @bucket - end - - def synced_to_s3_field - @synced_to_s3_field ||= "#{name}_synced_to_s3".freeze - end - - def synced_to_fog_field - @synced_to_fog_field ||= "#{name}_synced_to_fog".freeze - end - def to_file style = default_style queued_for_write[style] || (File.new(filesystem_path(style), 'rb') if exists?(style)) || download_file(style) end @@ -156,7 +165,7 @@ module Paperclip end def s3_path style - interpolate(options[:s3_path], style) + interpolate(self.class.s3_path_template, style) end def filesystem_paths @@ -176,7 +185,7 @@ module Paperclip end paths.each do |style, file| log("saving to s3 #{file}") - s3_object = aws_bucket.object(s3_path(style).gsub(/^\/+/,'')) + s3_object = self.class.aws_bucket.object(s3_path(style).gsub(/^\/+/,'')) s3_object.upload_file(file, cache_control: "max-age=#{10.year.to_i}", content_type: instance_read(:content_type), @@ -188,10 +197,6 @@ module Paperclip end end - def fog_storage - @fog_storage ||= Fog::Storage.new(@fog_credentials.merge(provider: @fog_provider).symbolize_keys) - end - def write_to_fog return unless instance.respond_to? synced_to_fog_field return true if instance_read(:synced_to_fog) @@ -210,7 +215,7 @@ module Paperclip } File.open(file, 'r') do |f| - fog_storage.put_object @fog_directory, path, f, options + self.class.fog_storage.put_object self.class.fog_directory, path, f, options end end # не вызываем колбеки и спокойно себя ведем если объект удален diff --git a/test/interpolations_test.rb b/test/interpolations_test.rb index ddaf026..1fa0367 100644 --- a/test/interpolations_test.rb +++ b/test/interpolations_test.rb @@ -67,7 +67,7 @@ class InterpolationsTest < Test::Unit::TestCase end should "reinterpolate :url" do - attachment = stub(options: {url: "/:id/"}, instance: stub(id: "1234")) + attachment = stub(class: stub(url_template: "/:id/"), instance: stub(id: "1234")) assert_equal "/1234/", Paperclip::Interpolations.url(attachment, :style) end diff --git a/test/test_helper.rb b/test/test_helper.rb index 01cc7ef..fe745e0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -83,5 +83,5 @@ class FakeModel end def attachment options - Paperclip::Attachment.new(:avatar, FakeModel.new, options) + Paperclip::Attachment.build_class(:avatar, options).new(FakeModel.new) end