1 module Technoweenie # :nodoc:
2 module AttachmentFu # :nodoc:
3 @@default_processors = %w(ImageScience Rmagick MiniMagick)
4 @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu')
5 @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/jpg']
6 mattr_reader :content_types, :tempfile_path, :default_processors
7 mattr_writer :tempfile_path
9 class ThumbnailError < StandardError; end
10 class AttachmentError < StandardError; end
14 # * <tt>:content_type</tt> - Allowed content types. Allows all by default. Use :image to allow all standard image types.
15 # * <tt>:min_size</tt> - Minimum size allowed. 1 byte is the default.
16 # * <tt>:max_size</tt> - Maximum size allowed. 1.megabyte is the default.
17 # * <tt>:size</tt> - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
18 # * <tt>:resize_to</tt> - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string.
19 # * <tt>:thumbnails</tt> - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options.
20 # * <tt>:thumbnail_class</tt> - Set what class to use for thumbnails. This attachment class is used by default.
21 # * <tt>:path_prefix</tt> - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name}
22 # for the S3 backend. Setting this sets the :storage to :file_system.
23 # * <tt>:storage</tt> - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system.
26 # has_attachment :max_size => 1.kilobyte
27 # has_attachment :size => 1.megabyte..2.megabytes
28 # has_attachment :content_type => 'application/pdf'
29 # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
30 # has_attachment :content_type => :image, :resize_to => [50,50]
31 # has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50'
32 # has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
33 # has_attachment :storage => :file_system, :path_prefix => 'public/files'
34 # has_attachment :storage => :file_system, :path_prefix => 'public/files',
35 # :content_type => :image, :resize_to => [50,50]
36 # has_attachment :storage => :file_system, :path_prefix => 'public/files',
37 # :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }
38 # has_attachment :storage => :s3
39 def has_attachment(options = {})
40 # this allows you to redefine the acts' options for each subclass, however
41 options[:min_size] ||= 1
42 options[:max_size] ||= 1.megabyte
43 options[:size] ||= (options[:min_size]..options[:max_size])
44 options[:thumbnails] ||= {}
45 options[:thumbnail_class] ||= self
46 options[:s3_access] ||= :public_read
47 options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? Technoweenie::AttachmentFu.content_types : t }.flatten unless options[:content_type].nil?
49 unless options[:thumbnails].is_a?(Hash)
50 raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }"
53 # doing these shenanigans so that #attachment_options is available to processors and backends
54 class_inheritable_accessor :attachment_options
55 self.attachment_options = options
57 # only need to define these once on a class
58 unless included_modules.include?(InstanceMethods)
59 attr_accessor :thumbnail_resize_options
61 attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file
62 attachment_options[:path_prefix] ||= attachment_options[:file_system_path]
63 if attachment_options[:path_prefix].nil?
64 attachment_options[:path_prefix] = attachment_options[:storage] == :s3 ? table_name : File.join("public", table_name)
66 attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/'
68 with_options :foreign_key => 'parent_id' do |m|
69 m.has_many :thumbnails, :class_name => attachment_options[:thumbnail_class].to_s
70 m.belongs_to :parent, :class_name => base_class.to_s
72 before_destroy :destroy_thumbnails
74 before_validation :set_size_from_temp_path
75 after_save :after_process_attachment
76 after_destroy :destroy_file
78 include InstanceMethods
79 include Technoweenie::AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend")
80 case attachment_options[:processor]
83 processors = Technoweenie::AttachmentFu.default_processors.dup
86 attachment_options[:processor] = "#{processors.first}Processor"
87 include Technoweenie::AttachmentFu::Processors.const_get(attachment_options[:processor])
89 rescue LoadError, MissingSourceFile
95 include Technoweenie::AttachmentFu::Processors.const_get("#{options[:processor].to_s.classify}Processor")
96 rescue LoadError, MissingSourceFile
97 puts "Problems loading #{options[:processor]}Processor: #{$!}"
100 after_validation :process_attachment
106 delegate :content_types, :to => Technoweenie::AttachmentFu
108 # Performs common validations for attachment models.
109 def validates_as_attachment
110 validates_presence_of :size, :content_type, :filename
111 validate :attachment_attributes_valid?
114 # Returns true or false if the given content type is recognized as an image.
115 def image?(content_type)
116 content_types.include?(content_type)
119 # Callback after an image has been resized.
121 # class Foo < ActiveRecord::Base
123 # after_resize do |record, img|
124 # record.aspect_ratio = img.columns.to_f / img.rows.to_f
127 def after_resize(&block)
128 write_inheritable_array(:after_resize, [block])
131 # Callback after an attachment has been saved either to the file system or the DB.
132 # Only called if the file has been changed, not necessarily if the record is updated.
134 # class Foo < ActiveRecord::Base
136 # after_attachment_saved do |record|
140 def after_attachment_saved(&block)
141 write_inheritable_array(:after_attachment_saved, [block])
144 # Callback before a thumbnail is saved. Use this to pass any necessary extra attributes that may be required.
146 # class Foo < ActiveRecord::Base
148 # before_thumbnail_saved do |record, thumbnail|
152 def before_thumbnail_saved(&block)
153 write_inheritable_array(:before_thumbnail_saved, [block])
156 # Get the thumbnail class, which is the current attachment class by default.
157 # Configure this with the :thumbnail_class option.
159 attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class)
160 attachment_options[:thumbnail_class]
163 # Copies the given file path to a new tempfile, returning the closed tempfile.
164 def copy_to_temp_file(file, temp_base_name)
165 returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
167 FileUtils.cp file, tmp.path
171 # Writes the given data to a new tempfile, returning the closed tempfile.
172 def write_to_temp_file(data, temp_base_name)
173 returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp|
181 module InstanceMethods
182 # Checks whether the attachment's content type is an image content type
184 self.class.image?(content_type)
187 # Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute.
189 image? && respond_to?(:parent_id) && parent_id.nil?
192 # Returns the class used to create new thumbnails for this attachment.
194 self.class.thumbnail_class
197 # Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg'
198 def thumbnail_name_for(thumbnail = nil)
199 return filename if thumbnail.blank?
201 basename = filename.gsub /\.\w+$/ do |s|
204 # ImageScience doesn't create gif thumbnails, only pngs
205 ext.sub!(/gif$/, 'png') if attachment_options[:processor] == "ImageScienceProcessor"
206 "#{basename}_#{thumbnail}#{ext}"
209 # Creates or updates the thumbnail for the current attachment.
210 def create_or_update_thumbnail(temp_file, file_name_suffix, *size)
211 thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column"))
212 returning find_or_initialize_thumbnail(file_name_suffix) do |thumb|
214 :content_type => content_type,
215 :filename => thumbnail_name_for(file_name_suffix),
216 :temp_path => temp_file,
217 :thumbnail_resize_options => size
219 callback_with_args :before_thumbnail_saved, thumb
224 # Sets the content type.
225 def content_type=(new_type)
226 write_attribute :content_type, new_type.to_s.strip
229 # Sanitizes a filename.
230 def filename=(new_name)
231 write_attribute :filename, sanitize_filename(new_name)
234 # Returns the width/height in a suitable format for the image_tag helper: (100x100)
236 [width.to_s, height.to_s] * 'x'
239 # Returns true if the attachment data will be written to the storage system on the next save
241 File.file?(temp_path.to_s)
244 # nil placeholder in case this field is used in a form.
245 def uploaded_data() nil; end
247 # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need
248 # any special code in your controller.
250 # <% form_for :attachment, :html => { :multipart => true } do |f| -%>
251 # <p><%= f.file_field :uploaded_data %></p>
252 # <p><%= submit_tag :Save %>
255 # @attachment = Attachment.create! params[:attachment]
257 # TODO: Allow it to work with Merb tempfiles too.
258 def uploaded_data=(file_data)
259 return nil if file_data.nil? || file_data.size == 0
260 self.content_type = file_data.content_type
261 self.filename = file_data.original_filename if respond_to?(:filename)
262 if file_data.is_a?(StringIO)
264 self.temp_data = file_data.read
266 self.temp_path = file_data.path
270 # Gets the latest temp path from the collection of temp paths. While working with an attachment,
271 # multiple Tempfile objects may be created for various processing purposes (resizing, for example).
272 # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until
273 # it's not needed anymore. The collection is cleared after saving the attachment.
276 p.respond_to?(:path) ? p.path : p.to_s
279 # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename.
281 @temp_paths ||= (new_record? || !File.exist?(full_filename)) ? [] : [copy_to_temp_file(full_filename)]
284 # Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no
285 # attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope.
286 # You can also use string paths for temporary files, such as those used for uploaded files in a web server.
287 def temp_path=(value)
288 temp_paths.unshift value
292 # Gets the data from the latest temp file. This will read the file into memory.
294 save_attachment? ? File.read(temp_path) : nil
297 # Writes the given data to a Tempfile and adds it to the collection of temp files.
299 self.temp_path = write_to_temp_file data unless data.nil?
302 # Copies the given file to a randomly named Tempfile.
303 def copy_to_temp_file(file)
304 self.class.copy_to_temp_file file, random_tempfile_filename
307 # Writes the given file to a randomly named Tempfile.
308 def write_to_temp_file(data)
309 self.class.write_to_temp_file data, random_tempfile_filename
312 # Stub for creating a temp file from the attachment data. This should be defined in the backend module.
313 def create_temp_file() end
315 # Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block.
317 # @attachment.with_image do |img|
318 # self.data = img.thumbnail(100, 100).to_blob
321 def with_image(&block)
322 self.class.with_image(temp_path, &block)
326 # Generates a unique filename for a Tempfile.
327 def random_tempfile_filename
328 "#{rand Time.now.to_i}#{filename || 'attachment'}"
331 def sanitize_filename(filename)
332 returning filename.strip do |name|
333 # NOTE: File.basename doesn't work right with Windows paths on Unix
334 # get only the filename, not the whole path
335 name.gsub! /^.*(\\|\/)/, ''
337 # Finally, replace all non alphanumeric, underscore or periods with underscore
338 name.gsub! /[^\w\.\-]/, '_'
342 # before_validation callback.
343 def set_size_from_temp_path
344 self.size = File.size(temp_path) if save_attachment?
347 # validates the size and content_type attributes according to the current model's options
348 def attachment_attributes_valid?
349 [:size, :content_type].each do |attr_name|
350 enum = attachment_options[attr_name]
351 errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name))
355 # Initializes a new thumbnail with the given suffix.
356 def find_or_initialize_thumbnail(file_name_suffix)
357 respond_to?(:parent_id) ?
358 thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) :
359 thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s)
362 # Stub for a #process_attachment method in a processor
363 def process_attachment
364 @saved_attachment = save_attachment?
367 # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
368 def after_process_attachment
370 if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
371 temp_file = temp_path || create_temp_file
372 attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
376 @saved_attachment = nil
377 callback :after_attachment_saved
381 # Resizes the given processed img object with either the attachment resize options or the thumbnail resize options.
382 def resize_image_or_thumbnail!(img)
383 if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image
384 resize_image(img, attachment_options[:resize_to])
385 elsif thumbnail_resize_options # thumbnail
386 resize_image(img, thumbnail_resize_options)
390 # Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self.
391 # Only accept blocks, however
392 def callback_with_args(method, arg = self)
396 callbacks_for(method).each do |callback|
397 result = callback.call(self, arg)
398 return false if result == false
404 # Removes the thumbnails for the attachment, if it has any
405 def destroy_thumbnails
406 self.thumbnails.each { |thumbnail| thumbnail.destroy } if thumbnailable?