Extract attachment options to class level

This commit is contained in:
Max Melentiev
2018-11-21 18:03:15 +03:00
parent 762f5dde22
commit ef9fabacdd
6 changed files with 123 additions and 114 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 <attachment>_updated_at field

View File

@@ -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
# не вызываем колбеки и спокойно себя ведем если объект удален

View File

@@ -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

View File

@@ -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