require 'fileutils' require 'tempfile' module UploadColumn Column = Struct.new(:name, :options) module ActiveRecordExtension def self.append_features(base) #:nodoc: super base.extend(ClassMethods) base.after_save :save_uploaded_files end private def save_uploaded_files @files.each { |k, v| v.send(:save) if v and v.tempfile? } if @files end def get_upload_column(name) options = options_for_column(name) #TODO: Spec this! @files ||= {} return nil if @files[name].is_a?(UploadColumn::IntegrityError) @files[name] ||= if self[name] then UploadColumn::UploadedFile.retrieve(self[name], self, name, options) else nil end end def set_upload_column(name, file) options = options_for_column(name) @files ||= {} if file.nil? @files[name], self[name] = nil else begin if uploaded_file = UploadColumn::UploadedFile.upload(file, self, name, options) self[name] = uploaded_file.actual_filename @files[name] = uploaded_file end rescue IntegrityError => e @files[name] = e end end end def get_upload_column_temp(name) @files[name].temp_value if @files and @files[name].respond_to?(:temp_value) end def set_upload_column_temp(name, path) options = options_for_column(name) @files ||= {} return if path.nil? or path.empty? unless @files[name] and @files[name].new_file? @files[name] = UploadColumn::UploadedFile.retrieve_temp(path, self, name, options) self[name] = @files[name].actual_filename end end def options_for_column(name) return self.class.reflect_on_upload_columns[name].options.reverse_merge(UploadColumn.configuration) end # weave in the magic column methods include UploadColumn::MagicColumns module ClassMethods # handle the +attr+ attribute as an "upload-column" field, generating additional methods as explained # in the README. You should pass the attribute's name as a symbol, like this: # # upload_column :picture # # +upload_column+ can manipulate file with the following options: # [+versions+] Creates different versions of the file, can be an Array or a Hash, in the latter case the values of the Hash will be passed to the manipulator # [+manipulator+] Takes a module that must have a method called process! that takes a single argument. Use this in conjucntion with :versions and :process # [+process+] This instrucion is passed to the manipulators process! method. # # you can customize file storage with the following: # [+store_dir+] Determines where the file will be stored permanently, you can pass a String or a Proc that takes the current instance and the attribute name as parameters, see the +README+ for detaills. # [+tmp_dir+] Determines where the file will be stored temporarily before it is stored to its final location, you can pass a String or a Proc that takes the current instance and the attribute name as parameters, see the +README+ for detaills. # [+old_files+] Determines what happens when a file becomes outdated. It can be set to one of :keep, :delete and :replace. If set to :keep UploadColumn will always keep old files, and if set to :delete it will always delete them. If it's set to :replace, the file will be replaced when a new one is uploaded, but will be kept when the associated object is deleted. Default to :delete. # [+permissions+] Specify the Unix permissions to be used with UploadColumn. Defaults to 0644. Remember that permissions are usually counted in octal and that in Ruby octal numbers start with a zero, so 0644 != 644. # [+root_dir+] The root path where image will be stored, it will be prepended to store_dir and tmp_dir # # it also accepts the following, less common options: # [+web_root+] Prepended to all addresses returned by UploadColumn::UploadedFile.url # [+extensions+] A white list of files that can be used together with validates_integrity_of to secure your uploads against malicious files. # [+fix_file_extensions+] Try to fix the file's extension based on its mime-type, note that this does not give you any security, to make sure that no dangerous files are uploaded, use +validates_integrity_of+. This defaults to true. # [+get_content_type_from_file_exec+] If this is set to true, UploadColumn::SanitizedFile will use a *nix exec to try to figure out the content type of the uploaded file. def upload_column(name, options = {}) @upload_columns ||= {} @upload_columns[name] = Column.new(name, options) define_method( name ) { get_upload_column(name) } define_method( "#{name}=" ) { |file| set_upload_column(name, file) } define_submethod( name, "temp" ) { get_upload_column_temp(name) } define_submethod( name, "temp=" ) { |path| set_upload_column_temp(name, path) } define_submethod( name, "public_path" ) { get_upload_column(name).public_path rescue nil } define_submethod( name, "path" ) { get_upload_column(name).path rescue nil } if options[:versions] options[:versions].each do |k, v| define_submethod( name, k ) { get_upload_column(name).send(k) rescue nil } define_submethod( name, k, "public_path" ) { get_upload_column(name).send(k).public_path rescue nil } define_submethod( name, k, "path" ) { get_upload_column(name).send(k).path rescue nil } end end end def image_column(name, options={}) upload_column(name, options.reverse_merge(UploadColumn.image_column_configuration)) end # Validates whether the images extension is in the array passed to :extensions. # By default this is the UploadColumn.extensions array # # Use this to prevent upload of files which could potentially damage your system, # such as executables or script files (.rb, .php, etc...). def validates_integrity_of(*attr_names) configuration = { :message => "is not of a valid file type." } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) attr_names.each { |name| self.reflect_on_upload_columns[name].options[:validate_integrity] = true } validates_each(attr_names, configuration) do |record, attr, value| value = record.instance_variable_get('@files')[attr] record.errors.add(attr, value.message) if value.is_a?(IntegrityError) end end # returns a hash of all UploadColumns defined on the model and their options. def reflect_on_upload_columns @upload_columns || {} end private def define_submethod(name, *subs, &b) define_method([name, subs].join('_'), &b) end # This is mostly for testing def reset_upload_columns @upload_columns = {} end end end end