require 'tempfile' require 'data_base/attachment/upfile' require 'data_base/attachment/iostream' require 'data_base/attachment/geometry' require 'data_base/attachment/processor' require 'data_base/attachment/thumbnail' require 'data_base/attachment/storage' require 'data_base/attachment/attach' if defined? RAILS_ROOT Dir.glob(File.join(File.expand_path(RAILS_ROOT), "lib", "attachment_processors", "*.rb")).each do |processor| require processor end end module Lipsiadmin module DataBase module Attachment class << self#:nodoc: def included(base) #:nodoc: base.extend(ClassMethods) end end module ClassMethods # Attach a single file/image to your model. # # Examples: # # has_one_attachment :image # attachment_styles_for :image, :normal, "128x128!" # validates_attachment_presence_for :image # validates_attachment_size_for :image, :greater_than => 10.megabytes # validates_attachment_content_type_for :image, "image/png" # # Then in your form (with multipart) you can simply add: # # =file_field_tag "yourmodel[image_attributes][file]" # # or # # -fields_for "yourmodel[image_attributes][file]", @yourmodel.build_image do |image| # =image.file_field :file # def has_one_attachment(name, options={}) options[:as] ||= :attacher options[:class_name] ||= "Attachment" # We need to check if the attachment model allow multiple attachments multi_attachments = options[:class_name].constantize.column_names.include?("attacher_name") options[:conditions] = "attacher_name = '#{name}'" if multi_attachments has_one name, options before_save "before_save_for_#{name}" attr_accessor "#{name}_attributes" write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil? attachment_definitions[name] = {:validations => {}}.merge(reflections[name].class_name.constantize.attachment_definitions) validates_each(name) do |record, attr, value| attributes = record.send("#{name}_attributes") attributes ||= {} attributes.merge!(:attachment_definitions => record.class.attachment_definitions[name]) file_column = reflections[name].class_name.constantize.new(attributes) unless file_column.valid? file_column.errors.each do |error, message| record.errors.add(name, message) if message end end end define_method "before_save_for_#{name}" do attributes = send("#{name}_attributes") attributes ||= {} return if attributes[:file].blank? attributes.merge!(:attachment_definitions => self.class.attachment_definitions[name]) # We need to add the new attacher_name attributes.merge!(:attacher_name => name.to_s) if multi_attachments if file_column = self.send(name) file_column.update_attributes(attributes) else self.send("build_#{name}", attributes) end end end # Attach a many files/images to your model. # # Examples: # # has_one_attachment :images # attachment_styles_for :images, :normal, "128x128!" # validates_attachment_presence_for :images # validates_attachment_size_for :images, :greater_than => 10.megabytes # validates_attachment_content_type_for :images, "image/png" # # Then in your form (with multipart) you can simply add: # # -fields_for "yourmodel[images_attributes][]", @yourmodel.images.build do |image| # =image.file_field :file # def has_many_attachments(name, options = {}) options[:as] ||= :attacher options[:class_name] ||= "Attachment" # We need to check if the attachment model allow multiple attachments multi_attachments = options[:class_name].constantize.column_names.include?("attacher_name") options[:conditions] = "attacher_name = '#{name}'" if multi_attachments has_many name, options before_save "before_save_for_#{name}" attr_accessor "#{name}_attributes" write_inheritable_attribute(:attachment_definitions, {}) if attachment_definitions.nil? attachment_definitions[name] = {:validations => {}}.merge(reflections[name].class_name.constantize.attachment_definitions) validates_each(name) do |record, attr, value| attributes = record.send("#{name}_attributes") attributes ||= {} file_column = nil for attribute in attributes attribute.merge!(:attachment_definitions => record.class.attachment_definitions[name]) file_column = reflections[name].class_name.constantize.new(attribute) unless file_column.valid? file_column.errors.each do |error, message| record.errors.add(name, message) if message end end end end define_method "before_save_for_#{name}" do attributes = send("#{name}_attributes") attributes ||= {} file_column = nil for attribute in attributes next if attribute["file"].blank? attribute.merge!(:attachment_definitions => self.class.attachment_definitions[name]) # We need to add the new attacher_name attribute.merge!(:attacher_name => name.to_s) if multi_attachments self.send(name).build(attribute) end end end # 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 # "/uploads/:id_:attachment_:style_:basename.:extension". See # Lipsiadmin::Attachment::Attach#interpolate for more information on variable interpolaton. # :url => "/:class/:attachment/:id/:style_:basename.:extension" # :url => "http://some.other.host/stuff/:class/:id_:extension" def attachment_url_for(name, url) attachment_definitions[name][:url] = url end # 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 # "/images/backend/no-image.png" # has_one_attached_file :avatar # attachment_default_url :avatar, "/images/backend/no-image.png" # User.new.avatar.url(:small) # => "/images/backend/no-image.png" def attachment_default_url_for(name, url) attachment_definitions[name][:default_url] = url end # The path were the attachment are stored. # The default value is # :rails_root/public/uploads/:id_:attachment_:style_:basename.:extension # This value must be in consistency with url def attachment_path_for(name, path) attachment_definitions[name][:path] = path end # 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). Attachment # 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. def attachment_styles_for(name, style_name, styles) attachment_definitions[name][:styles] ||= {} attachment_definitions[name][:styles].merge!(style_name => styles) end # The thumbnail style that will be used by default URLs. # Defaults to +original+. # attachment_styles :avatar, :normal, "100x100#" # attachment_default_style :normal # user.avatar.url # => "/avatars/23/normal_me.png" def attachment_default_style_for(name, default_style) attachment_definitions[name][:default_style] = default_style end # Will raise an error if Attachment cannot post_process an uploaded file due # to a command line error. This will override the global setting for this attachment. # Defaults to false. def attachment_whiny_for(name, whiny_thumbnails) attachment_definitions[name][:whiny_thumbnails] = whiny_thumbnails end # 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 Lipsiadmin::Attachment::Storage::Filesystem and Lipsiadmin::Attachment::Storage::S3 # for backend-specific options. def attachment_storage_for(name, storage) attachment_definitions[name][:storage] = storage end # 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. # attachment_styles :avatar, :large, "300x300" # attachment_styles :avatar, :negative, "100x100" # attachment_convert_options :avatar, :all, "-strip" # attachment_convert_options :avatar, :negative, "-negate" def attachment_convert_options_for(name, convert_name, convert_options) attachment_definitions[name][:convert_options] ||= {} attachment_definitions[name][:convert_options].merge!(convert_name => convert_options) end # When processing, if the spawn plugin is installed, processing can be done in # a background fork or thread if desired. def attachment_background_for(name, background) attachment_definitions[name][:background] = background end # Attachment supports an extendible selection of post-processors. When you define # a set of styles for an attachment, by default it is expected that those # "styles" are actually "thumbnails". However, you can do more than just # thumbnail images. By defining a subclass of Lipsiadmin::Attachment::Processor, you can # perform any processing you want on the files that are attached. Any file in # your Rails app's lib/attachment_processors directory is automatically loaded by # Attachment, allowing you to easily define custom processors. You can specify a # processor with the :processors option to has_attached_file: # # attachment_styles :avatar, :text, { :quality => :better } # attachment_processors :avatar, :ocr # # This would load the hypothetical class Lipsiadmin::Ocr, which would have the # hash "{ :quality => :better }" passed to it along with the uploaded file. For # more information about defining processors, see Lipsiadmin::Attachment::Processor. # # The default processor is Lipsiadmin::Attachment::Thumbnail. For backwards compatability # reasons, you can pass a single geometry string or an array containing a # geometry and a format, which the file will be converted to, like so: # # attachment_styles :avatar, :thumb, ["32x32#", :png] # # This will convert the "thumb" style to a 32x32 square in png format, regardless # of what was uploaded. If the format is not specified, it is kept the same (i.e. # jpgs will remain jpgs). # # Multiple processors can be specified, and they will be invoked in the order # they are defined in the :processors array. Each successive processor will # be given the result of the previous processor's execution. All processors will # receive the same parameters, which are what you define in the :styles hash. # For example, assuming we had this definition: # # attachment_styles :avatar, :text, { :quality => :better } # attachment_processors :avatar, :rotator # attachment_processors :avatar, :ocr # # then both the :rotator processor and the :ocr processor would receive the # options "{ :quality => :better }". This parameter may not mean anything to one # or more or the processors, and they are free to ignore it. def attachment_processors_for(name, processor_name, processors) attachment_definitions[name][:processors] ||= {} attachment_definitions[name][:processors].merge!(processor_name => processor_processors) 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_for(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) helper = ActionController::Base.helpers if options[:message].blank? message = "" message << I18n.t("activerecord.errors.messages.greater_than", :count => helper.number_to_human_size(min)) if min > 0 message << " " + I18n.t("activerecord.errors.messages.less_than", :count => helper.number_to_human_size(max)) if max.to_f.infinite? != 1 else message = options[:message] end attachment_definitions[name][:validations][:size] = lambda do |attachment, instance| if attachment.exist? && !range.include?(attachment.size.to_i) message end end end # Adds errors if thumbnail creation fails. The same as specifying :whiny_thumbnails => true. def validates_attachment_thumbnails_for(name, options = {}) attachment_definitions[name][:whiny_thumbnails] = true end # Places ActiveRecord-style validations on the presence of a file. def validates_attachment_presence_for(name, options = {}) message = options[:message] || I18n.t("activerecord.errors.messages.blank") attachment_definitions[name][:validations][:presence] = lambda do |attachment, instance| message unless attachment.exist? 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. def validates_attachment_content_type_for(name, *args) options = {} valid_types = [] args.each do |variable| case variable when Hash then options.merge!(variable) else valid_types << variable end end attachment_definitions[name][:validations][:content_type] = lambda do |attachment, instance| unless attachment.original_filename.blank? content_type = attachment.instance_read(:content_type) unless valid_types.any?{ |t| t === content_type } options[:message] || I18n.t("activerecord.errors.messages.content_type") 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 end end end