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 ||= { :convert_options => {}, :default_style => :original, :default_url => "/:attachment/:style/missing.png", :escape_url => true, :restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/, :filename_cleaner => nil, :hash_data => ":class/:attachment/:id/:style/:updated_at", :hash_digest => "SHA1", :interpolator => Paperclip::Interpolations, :only_process => [], :path => ":rails_root/public:url", :preserve_files => false, :processors => [:thumbnail], :source_file_options => {}, :storage => :filesystem, :styles => {}, :url => "/system/:class/:attachment/:id_partition/:style/:filename", :url_generator => Paperclip::UrlGenerator, :use_default_time_zone => true, :use_timestamp => true, :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails], :validate_media_type => true, :adapter_options => { hash_digest: Digest::MD5 }, :check_validity_before_processing => true } end
def able_to_store_created_at?
def able_to_store_created_at? @instance.respond_to?("#{name}_created_at".to_sym) end
def active_validator_classes
def active_validator_classes @instance.class.validators.map(&:class) end
def after_flush_writes
def after_flush_writes unlink_files(@queued_for_write.values) end
def as_json(options = nil)
def as_json(options = nil) to_s((options && options[:style]) || default_style) end
def assign(uploaded_file)
attachment:
addition to form uploads, you can also assign another Paperclip
previous file for deletion, to be flushed away on #save of its host. In
errors, assigns attributes, and processes the file. It also queues up the
What gets called when you call instance.attachment = File. It clears
def assign(uploaded_file) @file = Paperclip.io_adapters.for(uploaded_file, @options[:adapter_options]) ensure_required_accessors! ensure_required_validations! if @file.assignment? clear(*only_process) if @file.nil? nil else assign_attributes post_process_file reset_file_if_original_reprocessed end else nil end end
def assign_attributes
def assign_attributes @queued_for_write[:original] = @file assign_file_information assign_fingerprint { @file.fingerprint } assign_timestamps end
def assign_file_information
def assign_file_information instance_write(:file_name, cleanup_filename(@file.original_filename)) instance_write(:content_type, @file.content_type.to_s.strip) instance_write(:file_size, @file.size) end
def assign_fingerprint
def assign_fingerprint if instance_respond_to?(:fingerprint) instance_write(:fingerprint, yield) end end
def assign_timestamps
def assign_timestamps if has_enabled_but_unset_created_at? instance_write(:created_at, Time.now) end instance_write(:updated_at, Time.now) end
def blank?
def blank? not present? end
def cleanup_filename(filename)
def cleanup_filename(filename) filename_cleaner.call(filename) end
def clear(*styles_to_clear)
nil to the attachment. Does NOT save. If you wish to clear AND save,
Clears out the attachment. Has the same effect as previously assigning
def clear(*styles_to_clear) if styles_to_clear.any? queue_some_for_delete(*styles_to_clear) else queue_all_for_delete @queued_for_write = {} @errors = {} end 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 created_at
Returns the creation time of the file as originally assigned, and
def created_at if able_to_store_created_at? time = instance_read(:created_at) time && time.to_f.to_i end end
def default_options
def default_options { :timestamp => @options[:use_timestamp], :escape => @options[:escape_url] } end
def default_style
def default_style @options[:default_style] end
def destroy
nil to the attachment *and saving*. This is permanent. If you wish to
Destroys the attachment. Has the same effect as previously assigning
def destroy clear save end
def dirty!
def dirty! @dirty = true end
def dirty?
def dirty? @dirty end
def ensure_required_accessors! #:nodoc:
def ensure_required_accessors! #:nodoc: %w(file_name).each do |field| unless @instance.respond_to?("#{@name_string}_#{field}") && @instance.respond_to?("#{@name_string}_#{field}=") raise Paperclip::Error.new("#{@instance.class} model missing required attr_accessor for '#{@name_string}_#{field}'") end end end
def ensure_required_validations!
def ensure_required_validations! if missing_required_validator? raise Paperclip::Errors::MissingRequiredValidatorError end end
def errors
def errors @errors end
def expiring_url(time = 3600, style_name = default_style)
storage implementations, but keep using filesystem storage for development and
Alias to +url+ that allows using the expiring_url method provided by the cloud
def expiring_url(time = 3600, style_name = default_style) url(style_name) end
def extra_options_for(style) #:nodoc:
def extra_options_for(style) #:nodoc: process_options(:convert_options, style) end
def extra_source_file_options_for(style) #:nodoc:
def extra_source_file_options_for(style) #:nodoc: process_options(:source_file_options, style) end
def file?
def file? original_filename.present? end
def filename_cleaner
:filename_cleaner object. This object needs to respond to #call and takes
You can either specifiy :restricted_characters or you can define your own
def filename_cleaner @options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters]) end
def fingerprint
Returns the fingerprint of the file, if one's defined. The fingerprint is
def fingerprint instance_read(:fingerprint) end
def flush_errors #:nodoc:
def flush_errors #:nodoc: @errors.each do |error, message| [message].flatten.each {|m| instance.errors.add(name, m) } end end
def has_enabled_but_unset_created_at?
def has_enabled_but_unset_created_at? able_to_store_created_at? && !instance_read(:created_at) end
def hash_key(style_name = default_style)
Returns a unique hash suitable for obfuscating the URL of an otherwise
def hash_key(style_name = default_style) raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options[:hash_secret] require 'openssl' unless defined?(OpenSSL) data = interpolate(@options[:hash_data], style_name) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options[:hash_digest]).new, @options[:hash_secret], data) end
def initialize(name, instance, options = {})
+url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
+interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
+filename_cleaner+ - An object that responds to #call(filename) that will strip unacceptable charcters from filename
+preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
+processors+ - classes that transform the attachment. Defaults to [:thumbnail]
+source_file_options+ - flags passed to the +convert+ command that controls how the file is read
+convert_options+ - flags passed to the +convert+ command for processing
+hash_secret+ - a secret passed to the +hash_digest+
+hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
+hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
+use_default_time_zone+ - related to +use_timestamp+. Defaults to true
+whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
+use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
+storage+ - the storage mechanism. Defaults to :filesystem
+default_style+ - the style to use when an argument is not specified e.g. #url, #path
+default_url+ - a URL for the missing image
a special case that indicates all styles should be processed)
+only_process+ - style args to be run through the post-processor. This defaults to the empty list (which is
+styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
+path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
+url+ - a relative URL of the attachment. This is interpolated using +interpolator+
Options include:
+options+ is the same as the hash passed to +has_attached_file+.
+instance+ is the model 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.to_sym @name_string = name.to_s @instance = instance options = self.class.default_options.deep_merge(options) @options = options @post_processing = true @queued_for_delete = [] @queued_for_write = {} @errors = {} @dirty = false @interpolator = options[:interpolator] @url_generator = options[:url_generator].new(self) @source_file_options = options[:source_file_options] @whiny = options[:whiny] initialize_storage end
def initialize_storage #:nodoc:
def initialize_storage #:nodoc: storage_class_name = @options[:storage].to_s.downcase.camelize begin storage_module = Paperclip::Storage.const_get(storage_class_name) rescue NameError raise Errors::StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'" end 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_string}_#{attr}" if instance.respond_to?(getter) instance.send(getter) end end
def instance_respond_to?(attr)
Determines whether the instance responds to this attribute. Used to prevent
def instance_respond_to?(attr) instance.respond_to?(:"#{name}_#{attr}") 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_string}_#{attr}=" if instance.respond_to?(setter) instance.send(setter, value) end end
def interpolate(pattern, style_name = default_style) #:nodoc:
def interpolate(pattern, style_name = default_style) #:nodoc: interpolator.interpolate(pattern, self, style_name) end
def log message #:nodoc:
def log message #:nodoc: Paperclip.log(message) end
def missing_required_validator?
def missing_required_validator? (active_validator_classes.flat_map(&:ancestors) & Paperclip::REQUIRED_VALIDATORS).empty? end
def only_process
def only_process only_process = @options[:only_process].dup only_process = only_process.call(self) if only_process.respond_to?(:call) only_process.map(&:to_sym) 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_name = default_style)
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_name = default_style) path = original_filename.nil? ? nil : interpolate(path_option, style_name) path.respond_to?(:unescape) ? path.unescape : path end
def path_option
def path_option @options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path] end
def post_process(*style_args) #:nodoc:
def post_process(*style_args) #:nodoc: return if @queued_for_write[:original].nil? instance.run_paperclip_callbacks(:post_process) do instance.run_paperclip_callbacks(:"#{name}_post_process") do if !@options[:check_validity_before_processing] || !instance.errors.any? post_process_styles(*style_args) end end end end
def post_process_file
def post_process_file dirty! if post_processing post_process(*only_process) end end
def post_process_style(name, style) #:nodoc:
def post_process_style(name, style) #:nodoc: begin raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank? intermediate_files = [] original = @queued_for_write[:original] @queued_for_write[name] = style.processors. reduce(original) do |file, processor| file = Paperclip.processor(processor).make(file, style.processor_options, self) intermediate_files << file unless file == @queued_for_write[:original] # if we're processing the original, close + unlink the source tempfile if name == :original @queued_for_write[:original].close(true) end file end unadapted_file = @queued_for_write[name] @queued_for_write[name] = Paperclip.io_adapters. for(@queued_for_write[name], @options[:adapter_options]) unadapted_file.close if unadapted_file.respond_to?(:close) @queued_for_write[name] rescue Paperclip::Errors::NotIdentifiedByImageMagickError => e log("An error was received while processing: #{e.inspect}") (@errors[:processing] ||= []) << e.message if @options[:whiny] ensure unlink_files(intermediate_files) end end
def post_process_styles(*style_args) #:nodoc:
def post_process_styles(*style_args) #:nodoc: post_process_style(:original, styles[:original]) if styles.include?(:original) && process_style?(:original, style_args) styles.reject{ |name, style| name == :original }.each do |name, style| post_process_style(name, style) if process_style?(name, style_args) end end
def process_options(options_type, style) #:nodoc:
def process_options(options_type, style) #:nodoc: all_options = @options[options_type][:all] all_options = all_options.call(instance) if all_options.respond_to?(:call) style_options = @options[options_type][style] style_options = style_options.call(instance) if style_options.respond_to?(:call) [ style_options, all_options ].compact.join(" ") end
def process_style?(style_name, style_args) #:nodoc:
def process_style?(style_name, style_args) #:nodoc: style_args.empty? || style_args.include?(style_name) end
def processors
def processors processing_option = @options[:processors] if processing_option.respond_to?(:call) processing_option.call(instance) else processing_option end end
def queue_all_for_delete #:nodoc:
def queue_all_for_delete #:nodoc: return if !file? unless @options[:preserve_files] @queued_for_delete += [:original, *styles.keys].uniq.map do |style| path(style) if exists?(style) end.compact end instance_write(:file_name, nil) instance_write(:content_type, nil) instance_write(:file_size, nil) instance_write(:fingerprint, nil) instance_write(:created_at, nil) if has_enabled_but_unset_created_at? instance_write(:updated_at, nil) end
def queue_some_for_delete(*styles)
def queue_some_for_delete(*styles) @queued_for_delete += styles.uniq.map do |style| path(style) if exists?(style) end.compact end
def reprocess!(*style_args)
inconsistencies in timing of S3 commands. It's possible that calling
NOTE: Calling reprocess WILL NOT delete existing files. This is due to
the post-process again.
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. Its expected use is
def reprocess!(*style_args) saved_flags = @options.slice( :only_process, :preserve_files, :check_validity_before_processing ) @options[:only_process] = style_args @options[:preserve_files] = true @options[:check_validity_before_processing] = false begin assign(self) save instance.save rescue Errno::EACCES => e warn "#{e} - skipping file." false ensure @options.merge!(saved_flags) end end
def reset_file_if_original_reprocessed
def reset_file_if_original_reprocessed instance_write(:file_size, @queued_for_write[:original].size) assign_fingerprint { @queued_for_write[:original].fingerprint } reset_updater end
def reset_updater
def reset_updater if instance.respond_to?(updater) instance.send(updater) end end
def save
Saves the file, if there are no errors. If there are, it flushes them to
def save flush_deletes unless @options[:keep_old_files] process = only_process if process.any? && !process.include?(:original) @queued_for_write.except!(:original) end flush_writes @dirty = false true 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 staged?
def staged? ! @queued_for_write.empty? end
def staged_path(style_name = default_style)
def staged_path(style_name = default_style) if staged? @queued_for_write[style_name].path end end
def styles
def styles if @options[:styles].respond_to?(:call) || @normalized_styles.nil? styles = @options[:styles] styles = styles.call(self) if styles.respond_to?(:call) @normalized_styles = styles.dup styles.each_pair do |name, options| @normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self) end end @normalized_styles end
def time_zone
The time zone to use for timestamp interpolation. Using the default
def time_zone @options[:use_default_time_zone] ? Time.zone_default : Time.zone end
def to_s style_name = default_style
def to_s style_name = default_style url(style_name) end
def unlink_files(files)
def unlink_files(files) Array(files).each do |file| file.close unless file.closed? begin file.unlink if file.respond_to?(:unlink) rescue Errno::ENOENT end end 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_f.to_i end
def updater
def updater :"#{name}_file_name_will_change!" end
def uploaded_file
def uploaded_file instance_read(:uploaded_file) end
def url(style_name = default_style, options = {})
def url(style_name = default_style, options = {}) if options == true || options == false # Backwards compatibility. @url_generator.for(style_name, default_options.merge(:timestamp => options)) else @url_generator.for(style_name, default_options.merge(options)) end end