class Paperclip::Attachment
the file upon assignment.
when the model saves, deletes when the model is destroyed, and processes
The Attachment class manages the files for a given attachment. It saves
def self.default_options
def self.default_options @default_options ||= { :url => "/system/:attachment/:id/:style/:basename.:extension", :path => ":rails_root/public/system/:attachment/:id/:style/:basename.:extension", :styles => {}, :default_url => "/:attachment/:style/missing.png", :default_style => :original, :validations => {}, :storage => :filesystem } end
def self.interpolations
style as arguments. This hash can be added to with your own proc if
the proc named ":name". Each lambda takes the attachment and the current
A variable of the format :name will be replaced with the return value of
A hash of procs that are run during the interpolation of a path or url.
def self.interpolations @interpolations ||= { :rails_root => lambda{|attachment,style| RAILS_ROOT }, :rails_env => lambda{|attachment,style| RAILS_ENV }, :class => lambda do |attachment,style| attachment.instance.class.name.underscore.pluralize end, :basename => lambda do |attachment,style| attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "") end, :extension => lambda do |attachment,style| ((style = attachment.styles[style]) && style[:format]) || File.extname(attachment.original_filename).gsub(/^\.+/, "") end, :id => lambda{|attachment,style| attachment.instance.id }, :id_partition => lambda do |attachment, style| ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/") end, :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize }, :style => lambda{|attachment,style| style || attachment.default_style }, } end
def assign uploaded_file
If the file that is assigned is not valid, the processing (i.e.
new_user.avatar = old_user.avatar
another Paperclip attachment:
#save of its host. In addition to form uploads, you can also assign
also queues up the previous file for deletion, to be flushed away on
errors, assigns attributes, processes the file, and runs validations. It
What gets called when you call instance.attachment = File. It clears
def assign uploaded_file %w(file_name).each do |field| unless @instance.class.column_names.include?("#{name}_#{field}") raise PaperclipError.new("#{@instance.class} model does not have required column '#{name}_#{field}'") end end if uploaded_file.is_a?(Paperclip::Attachment) uploaded_file = uploaded_file.to_file(:original) end return nil unless valid_assignment?(uploaded_file) log("Assigning #{uploaded_file.inspect} to #{name}") uploaded_file.binmode if uploaded_file.respond_to? :binmode queue_existing_for_delete @errors = {} @validation_errors = nil return nil if uploaded_file.nil? log("Writing attributes for #{name}") @queued_for_write[:original] = uploaded_file.to_tempfile instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^\w\d\.\-]+/, '_')) instance_write(:content_type, uploaded_file.content_type.to_s.strip) instance_write(:file_size, uploaded_file.size.to_i) instance_write(:updated_at, Time.now) @dirty = true post_process if valid? # Reset the file size if the original file was reprocessed. instance_write(:file_size, uploaded_file.size.to_i) ensure validate end
def callback which
def callback which instance.run_callbacks(which, @queued_for_write){|result, obj| result == false } end
def content_type
Returns the content_type of the file as originally assigned, and lives
def content_type instance_read(:content_type) end
def dirty?
def dirty? @dirty end
def errors
def errors @errors end
def extra_options_for(style) #:nodoc:
def extra_options_for(style) #:nodoc: [ convert_options[style], convert_options[:all] ].compact.join(" ") end
def file?
def file? !original_filename.blank? end
def flush_errors #:nodoc:
def flush_errors #:nodoc: @errors.each do |error, message| instance.errors.add(name, message) if message end end
def initialize name, instance, options = {}
+instance+ is the ActiveRecord object instance it's attached to, and
Creates an Attachment object. +name+ is the name of the attachment,
def initialize name, instance, options = {} @name = name @instance = instance options = self.class.default_options.merge(options) @url = options[:url] @path = options[:path] @styles = options[:styles] @default_url = options[:default_url] @validations = options[:validations] @default_style = options[:default_style] @storage = options[:storage] @whiny = options[:whiny_thumbnails] @convert_options = options[:convert_options] || {} @processors = options[:processors] || [:thumbnail] @options = options @queued_for_delete = [] @queued_for_write = {} @errors = {} @validation_errors = nil @dirty = false normalize_style_definition initialize_storage log("Paperclip attachment #{name} on #{instance.class} initialized.") end
def initialize_storage
def initialize_storage @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize) self.extend(@storage_module) end
def instance_read(attr)
Reads the attachment-specific attribute on the instance. See instance_write
def instance_read(attr) getter = :"#{name}_#{attr}" responds = instance.respond_to?(getter) instance.send(getter) if responds || attr.to_s == "file_name" end
def instance_write(attr, value)
instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
Writes the attachment-specific attribute on the instance. For example,
def instance_write(attr, value) setter = :"#{name}_#{attr}=" responds = instance.respond_to?(setter) instance.send(setter, value) if responds || attr.to_s == "file_name" end
def interpolate pattern, style = default_style #:nodoc:
def interpolate pattern, style = default_style #:nodoc: interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s } interpolations.reverse.inject( pattern.dup ) do |result, interpolation| tag, blk = interpolation result.gsub(/:#{tag}/) do |match| blk.call( self, style ) end end end
def log message
def log message logger.info("[paperclip] #{message}") end
def logger
def logger instance.logger end
def normalize_style_definition
def normalize_style_definition @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 original_filename
Returns the name of the file as originally assigned, and lives in the
def original_filename instance_read(:file_name) end
def path style = nil #:nodoc:
URL, and the :bucket option refers to the S3 bucket.
on disk. If the file is stored in S3, the path is the "key" part of the
file is stored in the filesystem the path refers to the path of the file
Returns the path of the attachment as defined by the :path option. If the
def path style = nil #:nodoc: original_filename.nil? ? nil : interpolate(@path, style) end
def post_process #:nodoc:
def post_process #:nodoc: return if @queued_for_write[:original].nil? return if callback(:before_post_process) == false return if callback(:"before_#{name}_post_process") == false log("Post-processing #{name}") @styles.each do |name, args| begin @queued_for_write[name] = @queued_for_write[:original] args[:processors].each do |processor| @queued_for_write[name] = Paperclip.processor(processor).make(@queued_for_write[name], args) end rescue PaperclipError => e (@errors[:processing] ||= []) << e.message if @whiny end end callback(:"after_#{name}_post_process") callback(:after_post_process) end
def queue_existing_for_delete #:nodoc:
def queue_existing_for_delete #:nodoc: return unless file? log("Queueing the existing files for #{name} for deletion.") @queued_for_delete += [:original, *@styles.keys].uniq.map do |style| path(style) if exists?(style) end.compact instance_write(:file_name, nil) instance_write(:content_type, nil) instance_write(:file_size, nil) instance_write(:updated_at, nil) end
def reprocess!
thumbnails forcefully, by reobtaining the original file and going through
in the paperclip:refresh rake task and that's it. It will regenerate all
This method really shouldn't be called that often. It's expected use is
def reprocess! new_original = Tempfile.new("paperclip-reprocess") if old_original = to_file(:original) new_original.write( old_original.read ) new_original.rewind @queued_for_write = { :original => new_original } post_process old_original.close if old_original.respond_to?(:close) save else true end end
def save
Saves the file, if there are no errors. If there are, it flushes them to
def save if valid? log("Saving files for #{name}") flush_deletes flush_writes @dirty = false true else log("Errors on #{name}. Not saving.") flush_errors false end end
def size
Returns the size of the file as originally assigned, and lives in the
def size instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size) end
def to_s style = nil
def to_s style = nil url(style) end
def updated_at
Returns the last modified time of the file as originally assigned, and
def updated_at time = instance_read(:updated_at) time && time.to_i end
def url style = default_style, include_updated_timestamp = true
include_updated_timestamp to false if you want to stop the attachment
security, however, for performance reasons. set
grained security. This is not recommended if you don't need the
can access and can point to an action in your app, if you need fine
this does not necessarily need to point to a file that your web server
Returns the public URL of the attachment, with a given style. Note that
def url style = default_style, include_updated_timestamp = true url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style) include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url end
def valid?
def valid? validate errors.empty? end
def valid_assignment? file #:nodoc:
def valid_assignment? file #:nodoc: file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type)) end
def validate #:nodoc:
def validate #:nodoc: unless @validation_errors @validation_errors = @validations.inject({}) do |errors, validation| name, block = validation errors[name] = block.call(self, instance) if block errors end @validation_errors.reject!{|k,v| v == nil } @errors.merge!(@validation_errors) end @validation_errors end