Skip to content
This repository
Fetching contributors…

Cannot retrieve contributors at this time

file 318 lines (284 sloc) 14.304 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
# Paperclip allows file attachments that are stored in the filesystem. All graphical
# transformations are done using the Graphics/ImageMagick command line utilities and
# are stored in Tempfiles until the record is saved. Paperclip does not require a
# separate model for storing the attachment's information, instead adding a few simple
# columns to your table.
#
# Author:: Jon Yurek
# Copyright:: Copyright (c) 2008 thoughtbot, inc.
# License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
#
# Paperclip defines an attachment as any file, though it makes special considerations
# for image files. You can declare that a model has an attached file with the
# +has_attached_file+ method:
#
# class User < ActiveRecord::Base
# has_attached_file :avatar, :styles => { :thumb => "100x100" }
# end
#
# user = User.new
# user.avatar = params[:user][:avatar]
# user.avatar.url
# # => "/users/avatars/4/original_me.jpg"
# user.avatar.url(:thumb)
# # => "/users/avatars/4/thumb_me.jpg"
#
# See the +has_attached_file+ documentation for more details.

require 'tempfile'
require 'paperclip/upfile'
require 'paperclip/iostream'
require 'paperclip/geometry'
require 'paperclip/processor'
require 'paperclip/thumbnail'
require 'paperclip/storage'
require 'paperclip/attachment'
if defined? RAILS_ROOT
  Dir.glob(File.join(File.expand_path(RAILS_ROOT), "lib", "paperclip_processors", "*.rb")).each do |processor|
    require processor
  end
end

# The base module that gets included in ActiveRecord::Base. See the
# documentation for Paperclip::ClassMethods for more useful information.
module Paperclip

  VERSION = "2.2.4"

  class << self
    # Provides configurability to Paperclip. There are a number of options available, such as:
    # * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of
    # an uploaded image. Defaults to true.
    # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
    # log levels, etc. Defaults to true.
    # * command_path: Defines the path at which to find the command line
    # programs if they are not visible to Rails the system's search path. Defaults to
    # nil, which uses the first executable found in the user's search path.
    # * image_magick_path: Deprecated alias of command_path.
    def options
      @options ||= {
        :whiny_thumbnails => true,
        :image_magick_path => nil,
        :command_path => nil,
        :log => true,
        :swallow_stderr => true
      }
    end

    def path_for_command command #:nodoc:
      if options[:image_magick_path]
        warn("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead")
      end
      path = [options[:command_path] || options[:image_magick_path], command].compact
      File.join(*path)
    end

    def interpolates key, &block
      Paperclip::Attachment.interpolations[key] = block
    end

    # The run method takes a command to execute and a string of parameters
    # that get passed to it. The command is prefixed with the :command_path
    # option from Paperclip.options. If you have many commands to run and
    # they are in different paths, the suggested course of action is to
    # symlink them so they are all in the same directory.
    #
    # If the command returns with a result code that is not one of the
    # expected_outcodes, a PaperclipCommandLineError will be raised. Generally
    # a code of 0 is expected, but a list of codes may be passed if necessary.
    def run cmd, params = "", expected_outcodes = 0
      command = %Q<#{%Q[#{path_for_command(cmd)} #{params}].gsub(/\s+/, " ")}>
      command = "#{command} 2>#{bit_bucket}" if Paperclip.options[:swallow_stderr]
      output = `#{command}`
      unless [expected_outcodes].flatten.include?($?.exitstatus)
        raise PaperclipCommandLineError, "Error while running #{cmd}"
      end
      output
    end

    def bit_bucket #:nodoc:
      File.exists?("/dev/null") ? "/dev/null" : "NUL"
    end

    def included base #:nodoc:
      base.extend ClassMethods
      unless base.respond_to?(:define_callbacks)
        base.send(:include, Paperclip::CallbackCompatability)
      end
    end

    def processor name #:nodoc:
      name = name.to_s.camelize
      processor = Paperclip.const_get(name)
      unless processor.ancestors.include?(Paperclip::Processor)
        raise PaperclipError.new("Processor #{name} was not found")
      end
      processor
    end
  end

  class PaperclipError < StandardError #:nodoc:
  end

  class PaperclipCommandLineError < StandardError #:nodoc:
  end

  class NotIdentifiedByImageMagickError < PaperclipError #:nodoc:
  end

  module ClassMethods
    # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
    # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
    # The attribute returns a Paperclip::Attachment object which handles the management of
    # that file. The intent is to make the attachment as much like a normal attribute. The
    # thumbnails will be created when the new file is assigned, but they will *not* be saved
    # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
    # called on it, the attachment will *not* be deleted until +save+ is called. See the
    # Paperclip::Attachment documentation for more specifics. There are a number of options
    # you can set to change the behavior of a Paperclip attachment:
    # * +url+: The full URL of where the attachment is publically accessible. This can just
    # as easily point to a directory served directly through Apache as it can to an action
    # that can control permissions. You can specify the full domain and path, but usually
    # just an absolute path is sufficient. The leading slash *must* be included manually for
    # absolute paths. The default value is
    # "/system/:attachment/:id/:style/:basename.:extension". See
    # Paperclip::Attachment#interpolate for more information on variable interpolaton.
    # :url => "/:class/:attachment/:id/:style_:basename.:extension"
    # :url => "http://some.other.host/stuff/:class/:id_:extension"
    # * +default_url+: The URL that will be returned if there is no attachment assigned.
    # This field is interpolated just as the url is. The default value is
    # "/:attachment/:style/missing.png"
    # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
    # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
    # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
    # geometry strings at the ImageMagick website
    # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
    # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
    # inside the dimensions and then crop the rest off (weighted at the center). The
    # default value is to generate no thumbnails.
    # * +default_style+: The thumbnail style that will be used by default URLs.
    # Defaults to +original+.
    # has_attached_file :avatar, :styles => { :normal => "100x100#" },
    # :default_style => :normal
    # user.avatar.url # => "/avatars/23/normal_me.png"
    # * +whiny_thumbnails+: Will raise an error if Paperclip cannot post_process an uploaded file due
    # to a command line error. This will override the global setting for this attachment.
    # Defaults to true.
    # * +convert_options+: When creating thumbnails, use this free-form options
    # field to pass in various convert command options. Typical options are "-strip" to
    # remove all Exif data from the image (save space for thumbnails and avatars) or
    # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
    # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
    # Note that this option takes a hash of options, each of which correspond to the style
    # of thumbnail being generated. You can also specify :all as a key, which will apply
    # to all of the thumbnails being generated. If you specify options for the :original,
    # it would be best if you did not specify destructive options, as the intent of keeping
    # the original around is to regenerate all the thumbnails when requirements change.
    # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
    # :convert_options => {
    # :all => "-strip",
    # :negative => "-negate"
    # }
    # * +storage+: Chooses the storage backend where the files will be stored. The current
    # choices are :filesystem and :s3. The default is :filesystem. Make sure you read the
    # documentation for Paperclip::Storage::Filesystem and Paperclip::Storage::S3
    # for backend-specific options.
    def has_attached_file name, options = {}
      include InstanceMethods

      write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil?
      attachment_definitions[name] = {:validations => {}}.merge(options)

      after_save :save_attached_files
      before_destroy :destroy_attached_files

      define_callbacks :before_post_process, :after_post_process
      define_callbacks :"before_#{name}_post_process", :"after_#{name}_post_process"
     
      define_method name do |*args|
        a = attachment_for(name)
        (args.length > 0) ? a.to_s(args.first) : a
      end

      define_method "#{name}=" do |file|
        attachment_for(name).assign(file)
      end

      define_method "#{name}?" do
        attachment_for(name).file?
      end

      validates_each(name) do |record, attr, value|
        attachment = record.attachment_for(name)
        attachment.send(:flush_errors) unless attachment.valid?
      end
    end

    # Places ActiveRecord-style validations on the size of the file assigned. The
    # possible options are:
    # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
    # * +less_than+: equivalent to :in => 0..options[:less_than]
    # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
    # * +message+: error message to display, use :min and :max as replacements
    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] = lambda do |attachment, instance|
        if attachment.file? && !range.include?(attachment.size.to_i)
          message.gsub(/:min/, min.to_s).gsub(/:max/, max.to_s)
        end
      end
    end

    # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true.
    def validates_attachment_thumbnails name, options = {}
      attachment_definitions[name][:whiny_thumbnails] = true
    end

    # Places ActiveRecord-style validations on the presence of a file.
    def validates_attachment_presence name, options = {}
      message = options[:message] || "must be set."
      attachment_definitions[name][:validations][:presence] = lambda do |attachment, instance|
        message unless attachment.file?
      end
    end
    
    # Places ActiveRecord-style validations on the content type of the file
    # assigned. The possible options are:
    # * +content_type+: Allowed content types. Can be a single content type
    # or an array. Each type can be a String or a Regexp. It should be
    # noted that Internet Explorer upload files with content_types that you
    # may not expect. For example, JPEG images are given image/pjpeg and
    # PNGs are image/x-png, so keep that in mind when determining how you
    # match. Allows all by default.
    # * +message+: The message to display when the uploaded file has an invalid
    # content type.
    # 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] = lambda do |attachment, instance|
        valid_types = [options[:content_type]].flatten
        
        unless attachment.original_filename.blank?
          unless valid_types.blank?
            content_type = attachment.instance_read(:content_type)
            unless valid_types.any?{|t| content_type.nil? || t === content_type }
              options[:message] || "is not one of the allowed file types."
            end
          end
        end
      end
    end

    # Returns the attachment definitions defined by each call to
    # has_attached_file.
    def attachment_definitions
      read_inheritable_attribute(:attachment_definitions)
    end
  end

  module InstanceMethods #:nodoc:
    def attachment_for name
      @_paperclip_attachments ||= {}
      @_paperclip_attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
    end
    
    def each_attachment
      self.class.attachment_definitions.each do |name, definition|
        yield(name, attachment_for(name))
      end
    end

    def save_attached_files
      logger.info("[paperclip] Saving attachments.")
      each_attachment do |name, attachment|
        attachment.send(:save)
      end
    end

    def destroy_attached_files
      logger.info("[paperclip] Deleting attachments.")
      each_attachment do |name, attachment|
        attachment.send(:queue_existing_for_delete)
        attachment.send(:flush_deletes)
      end
    end
  end

end

# Set it all up.
if Object.const_defined?("ActiveRecord")
  ActiveRecord::Base.send(:include, Paperclip)
  File.send(:include, Paperclip::Upfile)
end
Something went wrong with that request. Please try again.