Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial

  • Loading branch information...
commit f13bf961adc587f61a8fa0523c1fcba857340b4e 0 parents
@toy authored
73 CHANGELOG
@@ -0,0 +1,73 @@
+0.2.1
+
+* Added :force_format option to image_column
+
+* Various Rails 1.2.1 compatibility fixes (mainly in test)
+
+* Added :permissions option to upload_column.
+
+* You can now assign normal Ruby File objects to upload and image columns
+
+* upload_form_tag and remote_upload_form_tag now accept a block, just like Rails' form_tag
+
+* You can now pass :none to image_column versions so nothing will be done to your image.
+
+* FIXED #8109 Parallell uploads no longer wipe each other out
+
+* WARNING: Compatibility with Rails < 1.2.1 dropped
+
+============================
+
+0.2
+
+* A freaking huge refactoring of the code, basically ALL of the methods for accessing paths have changed, except for path itself. This was overdue and I apologize if it breaks anything, but I felt that the gain in consistency was worth it. It now works like this:
+
+ path --the current path of the file (including the filename)
+ relative_path --the current path of the file relative to the root_path option
+
+ dir --the directory where the file is currently stored
+ relative_dir --like dir but relative to root_path
+
+ store_dir --The directory where files are permanently stored
+ relative_store_dir --the same but relative to root_dir
+
+ tmp_dir --The directory where tempfiles are stored
+ relative_tmp_dir --you can work this out yourself
+
+As you can see, this is now actually consistent, with all the relative paths relative to the same directory (err... wow?) and a consistent naming convention.
+
+* In related news: you can now pass a Proc to the :store_dir and :tmp_dir options. The default options are now also procs, instead of being some kind of arcane super-exception like before. The procs will be passed to arguments, first the current model instance and the name of the upload column as the second.
+
+* The :accumulate option was removed from :old_files. I really liked it, but it doesn't make sense with th new Proc-based system (it would wipe out data without thinking, thus potentially getting rid of files you want to keep). Use :keep instead or implement some kind of versioning. The new default is :delete. So beware, if you need to keep those files, make sure to change it!
+
+* You can now specify individual versions that should be cropped in image_columns, simply add a 'c' before the string that specifies the size, so you can do:
+
+ image_column :picture, :versions => { :thumb => "100x100", :banner => "c400x200" }
+
+Where thumb will be no larger than 100x100 (but might be smaller) and banner will be cropped to 400x200 exactly!
+
+* Furthermore you can pass a Proc instead of a string to an image_column version:
+
+ image_column :picture, :versions => { :thumb => "100x100", :solarized => proc{|img| img.solarize} }
+
+The Proc will be passed an RMagick object, just like process!
+
+* render_image now uses send_file if no block is given for faster performance.
+
+* FIXED #6955 store_dir callback called when the file is assigned
+
+* FIXED #7697 Editing with old-files :delete / :replace erases the original file
+
+* FIXED #7686 Problem uploading files with spaces in name
+
+============================
+
+1.1.2 (unreleased)
+
+* new :validates_integrity option replaces the old validates_integrity_of. The latter was more elegant, but posed a security risk, since files would be stored on the server in a remotely accessible location without having been validated. I tried to fix the bug, but couldn't make it work, so I opted for the less elegant, but safe solution instead.
+
+* readded the :file_exec option, it's now possible to set this manually again. I cut it originally, because I felt that it was unneccessary and that there were too many options already, I readded it mainly to make it possible to test the validation better.
+
+* assign, save, delete_temporary_files, delete, filename= and dir= are now all private, I see no reason why they should be public, and since they aren't really useful out of context I think it makes for a cleaner API to make them private, if you still need to use them, you can use .send(:save), etc.. instead.
+
+* Added magic columns, see the readme for detaills.
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2006 Sebastian Kanthak, Jonas Nicklas
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
187 README
@@ -0,0 +1,187 @@
+=UploadColumn
+
+Upload_column is a plugin for the Ruby on Rails framework that enables easy uploading of files, especially images.
+
+Suppose you have a list of users, and you would like to associate a picture to each of them. You could upload the image to a database, or you could use upload_column for simple storage to the file system.
+
+Let's create our database first, generate a migration (if you're not using migrations yet you are missing out!) and add
+
+ create_table "users" do |t|
+ t.column "name", :string
+ t.column "picture", :string
+ end
+
+Now generate a scaffold for your user class
+
+Create your model class and add the upload_column method call:
+
+ class User < ActiveRecord::Base
+ upload_column :picture
+ end
+
+Have a look at UploadColumn::ClassMethods.upload_column to find out more about specialized options. Note that the picture column in the database will not store the actual picture, it will only store the filename.
+
+Now just open up your _form.rhtml partial, and edit it so it looks like the following:
+
+ <p><label for="user_picture">Picture</label><br/>
+ <%= upload_column_field 'user', 'picture' %></p>
+
+Here we're making a call to UploadColumnHelper.upload_column_field, this will create an input field of the file type.
+
+We'll need to set the form to multipart, so that the picture will actually be sent as well. You can use the upload_form_tag helper:
+
+ <%= upload_form_tag( :action => 'create' ) %>
+
+And that's it! Your uploads are up and running (hopefully) and you should now be able to add pictures to your users. The madness doesn't stop there of course!
+
+== Storage Path
+
+You won't always want to store the pictures in the directory that upload_column selects for you, but that's not a problem, because changing that directory is trivial. You can pass a <tt>:store_dir</tt> key to the upload_column declaration, this will override the default mechanism and always use that directory as the basis.
+
+ upload_column :picture, :store_dir => "pictures"
+
+might be sensible in our case. Note that this way, all files will be stored in the same directory.
+
+If you need more refined control over the storage path (maybe you need to store it by the id of an association?) then you can use a callback method. In our case the method would be called +picture_store_dir+. Just append +_store_dir+ to your upload_column field.
+
+ def picture_store_dir
+ "images/#{self.category.name}/#{self.id}"
+ end
+
+A shorter way to do something like this is to pass a Proc to :store_dir, like so:
+
+ upload_column :picture, :store_dir => proc{|inst, attr| "images/#{inst.category.name}/#{inst.id}"}
+
+The proc will be passed two parameters, the first is the current instance of your model class, the second is the name of the attribute that is being uploaded to (in our case attr would be :picture).
+
+You can change the :tmp_dir in the same way. For reference: the default for :store_dir is the following proc:
+
+ proc{|inst, attr| File.join(Inflector.underscore(inst.class).to_s, attr.to_s, inst.id.to_s) }
+
+Note how it uses File.join, if you plan to use your code on Windows systems you should use File.join, since Windows uses backslashes instead of forwardslashes. File.join takes care of that automatically.
+
+== Filename
+
+By default, UploadColumn will keep the name of the original file, however this might be inconvenient in some cases. You can pass a :filename directive to your upload_column declaration:
+
+ upload_column :picture, :filename => "donkey.png"
+
+In which case all files will be named <tt>donkey.png</tt>. This is not desirable if the file in question is a jpeg file of course. Usually it is more sensible to pass a Proc to :filename.
+
+ upload_column :picture, :filename => proc{|inst, orig, ext| "avatar#{inst.id}.#{ext}"}
+
+The Proc will be passed three parameters, the current instance, the basename of the original file (without the extension) and the properly corrected extension.
+
+You can also use the +picture_filename+ callback, which must take two arguments, the original basename and the corrected extension.
+
+== Manipulating Images with RMagick
+
+Say you would want (for whatever reason) to have a funky solarize effect on your users' images. Manipulating images with upload_column can be done either at runtime or after the image is saved, let's look at some possibilities:
+
+ class User < ActiveRecord::Base
+ upload_column :picture
+
+ def picture_after_assign
+ picture.process! do |img|
+ img.solarize
+ end
+ end
+ end
+
+Or maybe we want different versions of our image, then we could simply specify:
+
+ class User < ActiveRecord::Base
+ upload_column :picture, :versions => [ :solarized, :sepiatoned ]
+
+ def picture_after_assign
+ picture.solarized.process! do |img|
+ img.solarize
+ end
+ picture.sepiatoned.process! do |img|
+ img.sepiatone
+ end
+ end
+ end
+
+== Image column
+
+If you only want to upload images, then UploadColumn comes with a convenient method, +image_column+ works basically the same way as +upload_column+ but it is especially trimmed for work with images.
+
+The mime_extensions and extensions parameters are restricted to those used for images, so +validates_integrity_of+ can be used to easily restrict uploads to images only.
+
+Most importantly if you use image_column you can resize the images automagickaly (sorry) when they are assigned, just pass a Hash, like in the following example:
+
+ class User < ActiveRecord::Base
+ image_column :picture, :versions => { :thumb => "100x100", :large => "200x300" }
+ end
+
+If you need the image to be cropped to the exact dimensions, you can pass <tt>:crop => true</tt>.
+
+== Runtime rendering
+
+You can manipulate images at runtime (it's a huge performance hit though!). In your controller add an action and use UploadColumnRenderHelper.render_image.
+
+ def sepiatone
+ @user = User.find(parms[:id])
+ render_image @user.picture do |img|
+ img.sepiatone
+ end
+ end
+
+And that's it!
+
+In your view, you can use UploadColumnHelper.image to easily create an image tag for your action:
+
+ <%= image :action => "sepiatone", :id => 5 %>
+
+== Views
+
+If your uploaded file is an image you would most likely want to display it in your view, if it's another kind of file you'll want to link to it. Both of these are easy using UploadColumn::BaseUploadedFile.url.
+
+ <%= link_to "Guitar Tablature", @song.tab.url %>
+
+ <%= image_tag @user.picture.url %>
+
+== Magic Columns
+
+UploadColumn allows you to add 'magic' columns to your model, which will be automatically filled with the appropriate data. Just add the column, for example via migrations:
+
+ add_column :users, :picture_mime_type
+
+And if our model looks like this:
+
+ class User < ActiveRecord::Base
+ upload_column :picture
+ end
+
+The column <tt>picture_mime_type</tt> will now automatically be filled with the file's mime-type (or at least with UploadColumn's best guess ;).
+
+The following are available for upload_column fields:
+
+[+_mime_type+] Will contain the file's mime_type
+[+_filesize+] Will contain the file's size in bytes
+
+If you use image_column you can even add:
+
+[+_width+] Will contain the image's width
+[+_height+] Will contain the image's height
+
+You can also do <tt>picture_exif_date_time</tt> or <tt>picture_exif_model</tt>, etc. if picture is an image_column and the uploaded image i a JPEG file. This requires EXIFR, which you can get by installing the gem via <tt>gem install exifr</tt>.
+
+== Validations
+
+You can use SOME of Rails validations with UploadColumn
+
+validates_presence_of and validates_size_of have been verified to work.
+
+ validates_size_of :image, :maximum => 200000, :message => "is too big, must be smaller than 200kB!"
+
+Remember to change the error message, the default one sounds a bit stupid with UploadColumn.
+
+validates_uniqueness_of does NOT work, this is because validates_uniqueness_of will send(:your_upload_column) instead of asking for the instance variable, thus it will get an UploadedFile object, which it can't really compare to other values in the database, this is rather difficult to work around without messing with Rails internals (if you manage, please let me know!). Meanwhile you could do
+
+ validates_each :your_upload_column do |record, attr, value|
+ record.errors.add attr, 'already exists!' if YourModel.find( :first, :conditions => ["#{attr.to_s} = ?", value ] )
+ end
+
+It's not elegant I know, but it should work.
33 Rakefile
@@ -0,0 +1,33 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the UploadColumn plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the UploadColumn plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'UploadColumn'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+desc 'Generate documentation for the UploadColumn plugin using the allison template.'
+Rake::RDocTask.new(:rdoc_allison) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'UploadColumn'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ rdoc.main = "README" # page to start on
+ rdoc.template = "~/projects/allison/allison.rb"
+end
11 init.rb
@@ -0,0 +1,11 @@
+# plugin init file for rails
+# this file will be picked up by rails automatically and
+# add the file_column extensions to rails
+
+ActiveRecord::Base.send(:include, UploadColumn)
+ActionView::Base.send(:include, UploadColumnHelper)
+ActionController::Base.send(:include, UploadColumnRenderHelper)
+
+Mime::Type.register "image/png", :png
+Mime::Type.register "image/jpeg", :jpg
+Mime::Type.register "image/gif", :gif
882 lib/upload_column.rb
@@ -0,0 +1,882 @@
+require 'fileutils'
+require 'tempfile'
+require 'RMagick'
+
+module UploadColumn
+ def self.append_features(base) #:nodoc:
+ super
+ base.extend(ClassMethods)
+ end
+
+ # default mapping of mime-types to file extensions. FileColumn will try to
+ # rename a file to the correct extension if it detects a known mime-type
+ MIME_EXTENSIONS = {
+ "image/gif" => "gif",
+ "image/jpeg" => "jpg",
+ "image/pjpeg" => "jpg",
+ "image/x-png" => "png",
+ "image/jpg" => "jpg",
+ "image/png" => "png",
+ "application/x-shockwave-flash" => "swf",
+ "application/pdf" => "pdf",
+ "application/pgp-signature" => "sig",
+ "application/futuresplash" => "spl",
+ "application/msword" => "doc",
+ "application/postscript" => "ps",
+ "application/x-bittorrent" => "torrent",
+ "application/x-dvi" => "dvi",
+ "application/x-gzip" => "gz",
+ "application/x-ns-proxy-autoconfig" => "pac",
+ "application/x-shockwave-flash" => "swf",
+ "application/x-tgz" => "tar.gz",
+ "application/x-tar" => "tar",
+ "application/zip" => "zip",
+ "audio/mpeg" => "mp3",
+ "audio/x-mpegurl" => "m3u",
+ "audio/x-ms-wma" => "wma",
+ "audio/x-ms-wax" => "wax",
+ "audio/x-wav" => "wav",
+ "image/x-xbitmap" => "xbm",
+ "image/x-xpixmap" => "xpm",
+ "image/x-xwindowdump" => "xwd",
+ "text/css" => "css",
+ "text/html" => "html",
+ "text/javascript" => "js",
+ "text/plain" => "txt",
+ "text/xml" => "xml",
+ "video/mpeg" => "mpeg",
+ "video/quicktime" => "mov",
+ "video/x-msvideo" => "avi",
+ "video/x-ms-asf" => "asf",
+ "video/x-ms-wmv" => "wmv",
+ "video/x-flv" => "flv"
+ }
+
+ IMAGE_MIME_EXTENSIONS = {
+ "image/gif" => "gif",
+ "image/jpeg" => "jpg",
+ "image/pjpeg" => "jpg",
+ "image/x-png" => "png",
+ "image/jpg" => "jpg",
+ "image/png" => "png",
+ }
+
+ IMAGE_EXTENSIONS = Set.new( [ "jpg", "jpeg", "png", "gif" ] )
+
+ EXTENSIONS = Set.new MIME_EXTENSIONS.values
+ EXTENSIONS.merge %w(jpeg)
+
+ # default options. You can override these with +upload_column+'s +options+ parameter
+ DEFAULT_OPTIONS = {
+ :root_path => File.join(RAILS_ROOT, "public"),
+ :web_root => "",
+ :mime_extensions => MIME_EXTENSIONS,
+ :extensions => EXTENSIONS,
+ :fix_file_extensions => true,
+ :store_dir => proc{|inst, attr| File.join(Inflector.underscore(inst.class).to_s, attr.to_s, inst.id.to_s) },
+ :tmp_dir => proc{|inst, attr| File.join(Inflector.underscore(inst.class).to_s, attr.to_s, "tmp") },
+ :old_files => :delete,
+ :validate_integrity => true,
+ :file_exec => 'file',
+ :filename => proc{|inst, original, ext| original + ( ext.blank? ? '' : ".#{ext}" )},
+ :permissions => 0644
+ }.freeze
+
+ # = Basics
+ # When you call an upload_column field, an instance of this class will be returned.
+ #
+ # Suppose a +User+ model has a +picture+ upload_column, like so:
+ # class User < ActiveRecord::Base
+ # upload_column :picture
+ # end
+ # Now in our controller we did:
+ # @user = User.find(params[:id])
+ # We could then access the file:
+ # @user.picture.url
+ # Which would output the url to the file (assuming it is stored in /public/)
+ # = Versions
+ # If we had instead added different versions in our model
+ # upload_column :picture, :versions => [:thumb, :large]
+ # Then we could access them like so:
+ # @user.picture.thumb.url
+ class UploadedFile
+
+ attr_accessor :options, :mime_type, :ext, :original_basename
+ attr_reader :instance, :attribute, :versions, :suffix, :options, :relative_dir
+
+ private :mime_type=, :ext=, :original_basename=
+
+ def initialize(options, instance, attribute, dir = nil, filename = nil, suffix = nil)
+ options = DEFAULT_OPTIONS.merge(options)
+
+ @options = options
+ @instance = instance
+ @attribute = attribute
+ @filename = filename || instance[attribute]
+ @suffix = suffix
+
+ @relative_dir = dir
+ @relative_dir ||= self.relative_store_dir
+
+ unless options[:web_root].blank?
+ options[:web_root] = '/' << options[:web_root] unless options[:web_root] =~ %r{^/}
+ end
+
+ unless options[:tmp_dir].blank?
+ options[:tmp_dir][0] = '' if options[:tmp_dir] =~ %r{^/}
+ end
+
+ if suffix.nil? and options[:versions]
+ @versions = {}
+ for version in options[:versions]
+ version = version[0] if version.is_a?( Array )
+ @versions[version.to_sym] = self.class.new(options, instance, attribute, dir, filename, version.to_s )
+ end
+ end
+ end
+
+ def to_s #:nodoc:
+ filename
+ end
+
+ # Returns the file's size
+ def size
+ File.size(self.path)
+ end
+
+ # checks whether the file exists
+ def exists?
+ File.exists?(self.path)
+ end
+
+ # Processes the file with RMagick. This works only if the file is an image that
+ # RMagick can understand. The image is loaded using +Image::read+ and then passed
+ # to a block, +process+ then returns the result of the block, like so:
+ # new_image = @user.picture.process do |img|
+ # img = img.thumbnail( 0.1 )
+ # img.solarize
+ # end
+ # Resulting in an image shrunk to 10% of the original size and solarized. For more information
+ # on what you can do inside a +process+ block, see the RMagick doumentation at:
+ # http://www.simplesystems.org/RMagick/doc/index.html.
+ #
+ # Note that you will need to 'carry' the image since most Rmagick methods do not modify
+ # the image itself but rather return the result of the transformation.
+ #
+ # Note also that unlike RMagicks's read, this method will return nil if the image cannot
+ # be opened, it will not throw an Error, so you can happily
+ # apply this, even if you aren't sure that the file is an image.
+ #
+ # +process!+ is usually more useful, don't use +process+ unless there is a good reason to!
+ #
+ # Remember to call GC.start after you are done processing the image, to avoid memory leaks.
+ def process
+ # Load the file as a ImageMagick object, pass to block, return the yielded result
+ begin
+ img = ::Magick::Image::read(self.path).first
+ rescue Magick::ImageMagickError
+ return nil
+ end
+ yield( img )
+ end
+
+ # Like +process+, but instead of returning the new image, it will replace this one.
+ # Use +process!+ in an _after_assign callback, like so:
+ # class User < ActiveRecord::Base
+ # upload_column :picture
+ #
+ # def picture_after_assign
+ # picture.process! do |img|
+ # img.solarize
+ # end
+ # end
+ #
+ # end
+ # This is an easy way to apply some RMagick effect to an image right after it's uploaded
+ #
+ # Note that this method will silently fail if the image cannot be opened, so you can happily
+ # apply this, even if you aren't sure that the file is an images.
+ #
+ # To prevent memory leaks, process! will call GC.start manually.
+ def process!
+ img = process do |img|
+ img = yield( img )
+ end
+ return false if img.nil?
+ img.write self.path
+ img = nil
+ GC.start
+ true
+ end
+
+ # Returns the (absolute) directory where the file is currently stored.
+ def dir
+ File.expand_path(self.relative_dir, options[:root_path])
+ end
+
+ # Returns the (absolute) path of the file
+ def path
+ File.expand_path(self.relative_path, options[:root_path])
+ end
+
+ # Returns the path of the file relative to :root_path
+ def relative_path()
+ join_path(self.relative_dir, self.filename)
+ end
+
+ # Returns the URL of the file, you can use this in your views to easily create links to
+ # your files:
+ # <%= link_to "#{@song.title} Tab", @song.guitar_tab.url %>
+ # Or if your file is an image, you can use +url+ like so:
+ # <%= image_tag @user.picture.url %>
+ def url
+ options[:web_root] + ( "/" << self.relative_path.gsub("\\", "/") )
+ end
+
+ # Returns the directory where the file is (or will be) permanently stored
+ def store_dir
+ File.expand_path(self.relative_store_dir, options[:root_path])
+ end
+
+ # Like +store_dir+ but will return the directory relative to the :root_path option
+ def relative_store_dir
+ sd = self.instance.send("#{self.attribute}_store_dir")
+ if options[:store_dir].is_a?( Proc )
+ sd ||= options[:store_dir].call(self.instance, self.attribute)
+ else
+ sd ||= options[:store_dir]
+ end
+ return sd
+ end
+
+ # Returns the directory where the file will be temporarily stored between form redisplays
+ def tmp_dir
+ File.expand_path(self.relative_tmp_dir, options[:root_path])
+ end
+
+ # Like +tmp_dir+ but will return the directory relative to the :root_path option
+ def relative_tmp_dir
+ sd = self.instance.send("#{self.attribute}_tmp_dir")
+ if options[:tmp_dir].is_a?( Proc )
+ sd ||= options[:tmp_dir].call(self.instance, self.attribute)
+ else
+ sd ||= options[:tmp_dir]
+ end
+ return sd
+ end
+
+ # Returns the filename without the extension
+ def filename_base
+ split_extension(@filename)[0]
+ end
+
+ # Returns the file's extension
+ def filename_extension
+ split_extension(@filename)[1]
+ end
+
+ # Returns the mime-type of the file.
+ def mime_type
+ return @mime_type if @mime_type
+ case filename_extension
+ when "jpg":
+ return "image/jpeg"
+ when "gif":
+ return "image/gif"
+ when "png":
+ return "image/png"
+ else
+ @mime_type = MIME_EXTENSIONS.invert[filename_extension]
+ end
+ end
+
+ # returns the file's name
+ def filename
+ expand_filename(@filename)
+ end
+
+ private
+
+ # Set the filename, use at your own risk!
+ def filename=(name)
+ @filename = sanitize_filename( name )
+ end
+
+ def relative_dir=(dir)
+ @relative_dir = dir
+ end
+
+ # Assigns a file to this upload column and stores it in a temporary file,
+ # Note: does not check the validity of the file!
+ def assign(file, directory = nil )
+
+ unless directory
+ directory = join_path( self.relative_tmp_dir, generate_temp_name )
+ end
+
+ #
+ #self.filename = fetch_filename( self.instance, basename, ext )
+ self.relative_dir = directory
+
+ FileUtils.mkpath(self.dir)
+
+ # stored uploaded file into self.path
+ # If it was a Tempfile object, the temporary file will be
+ # cleaned up automatically, so we do not have to care for this
+ # Large files will be passed as tempfiles, whereas small ones
+ # will be passed as StringIO
+ if temp_path = ( file.respond_to?(:local_path) ? file.local_path : file.path ) and temp_path != ""
+ if File.exists?(temp_path)
+ temp_filename = file.original_filename if file.respond_to?(:original_filename)
+ temp_filename ||= File.basename(file.path)
+ basename, self.ext = split_extension(temp_filename)
+ self.ext, self.mime_type = fetch_file_extension( file, temp_path, self.ext )
+ self.filename = fetch_filename( self.instance, basename, self.ext )
+ return false unless check_integrity( self.filename_extension )
+ FileUtils.copy_file( temp_path, self.path )
+ else
+ raise ArgumentError.new("File #{file.inspect} at #{temp_path.inspect} does not exist")
+ end
+ elsif file.respond_to?(:read)
+ basename, self.ext = split_extension(file.original_filename)
+ self.ext, self.mime_type = fetch_file_extension( file, nil, self.ext )
+ self.filename = fetch_filename( self.instance, basename, self.ext )
+ return false unless check_integrity( self.filename_extension )
+ file.rewind # Make sure we are at the beginning of the buffer
+ File.open(self.path, "wb") { |f| f.write(file.read) }
+ else
+ raise ArgumentError.new("Do not know how to handle #{file.inspect}")
+ end
+
+ self.original_basename = basename
+
+ versions.each { |k, v| v.send(:assign_version, self.path, self.relative_dir, self.filename, self.original_basename, self.ext) } if versions
+
+ File.chmod(options[:permissions], self.path)
+
+ set_magic_columns
+
+ return true
+ end
+
+ def assign_version( path, directory, filename, basename, ext )
+ self.relative_dir = directory
+ self.filename = filename
+ self.ext = ext
+ self.original_basename = basename
+ FileUtils.copy_file( path, self.path )
+ end
+
+ def save
+
+ new_dir = self.store_dir
+ new_filename = sanitize_filename(fetch_filename(self.instance, self.original_basename, self.ext))
+ new_path = join_path( new_dir, expand_filename(new_filename) )
+
+ # create the directory first
+ FileUtils.mkpath(new_dir) #unless File.exists?(new_di)
+
+ # move the temporary file over
+ FileUtils.cp( self.path, new_path )
+
+ self.relative_dir = self.relative_store_dir
+ self.filename = new_filename
+
+ versions.each { |k, v| v.send(:save) } if versions
+ end
+
+ def fetch_filename(inst, original, ext)
+ fn = self.instance.send("#{self.attribute}_filename", original, ext)
+ if options[:filename].is_a?( Proc )
+ fn ||= options[:filename].call(inst, original, ext)
+ else
+ fn ||= options[:filename]
+ end
+ fn
+ end
+
+ def set_magic_columns
+ self.instance.class.column_names.each do |column|
+ if column =~ /^#{self.attribute}_(.*)$/
+ case $1
+ when "mime_type"
+ self.instance.send("#{self.attribute}_mime_type=".to_sym, self.mime_type)
+ when "filesize"
+ self.instance.send("#{self.attribute}_filesize=".to_sym, self.size)
+ end
+ end
+ end
+ end
+
+ def delete_temporary_files
+ Dir.glob(join_path(self.tmp_dir, "*")).each do |file|
+ # Check if the file was created more than an hour ago
+ if file =~ %r{(\d+)\.[\d]+\.[\d]+$} and $1.to_i < ( Time.now - 3600 ).to_i
+ FileUtils.rm_rf(file)
+ end
+ end
+ end
+
+ # Delete this file, note that it will only delete the FILE, not the value in the
+ # database
+ def delete
+ FileUtils.rm( self.path ) if File.exists?( self.path )
+ versions.each { |k, v| v.send(:delete) } if versions
+ if Dir.glob(join_path(self.dir, '*')).empty?
+ FileUtils.rm_rf( self.dir )
+ end
+ end
+
+ def set_path(temp_path)
+ return if temp_path == self.relative_path # We do not need to set this path
+ raise ArgumentError.new("invalid format of '#{temp_path}'") unless temp_path =~ %r{^((\d+\.)+\d+)/([^/;]+)(;([^/;]+))?$}
+ self.relative_dir = join_path( self.relative_tmp_dir, $1 )
+ self.original_basename, self.ext = split_extension($5 || $3)
+ self.filename = $3
+ if versions
+ versions.each do |k, v|
+ v.send(:filename=, self.filename)
+ v.send(:relative_dir=, self.relative_dir)
+ v.send(:original_basename=, self.original_basename)
+ v.send(:ext=, self.ext)
+ end
+ end
+ end
+
+ def check_integrity( extension )
+ if self.options[:validate_integrity]
+ unless self.options[:extensions].include?( extension )
+ return false
+ end
+ end
+ true
+ end
+
+ def generate_temp_name
+ now = Time.now
+ "#{now.to_i}.#{now.usec}.#{Process.pid}"
+ end
+
+ def join_path( *paths )
+ # remove paths that are nil
+ paths.delete( nil )
+ File.join( paths )
+ end
+
+ # Split the filename into base and extension
+ def split_extension(fn)
+ # regular expressions to try for identifying extensions
+ ext_regexps = [
+ /^(.+)\.([^.]+\.[^.]+)$/, # matches "something.tar.gz"
+ /^(.+)\.([^.]+)$/ # matches "something.jpg"
+ ]
+ ext_regexps.each do |regexp|
+ if fn =~ regexp
+ base, ext = $1, $2
+ return [base, ext] if options[:extensions].include?(ext.downcase)
+ end
+ end
+ [fn, ""]
+ end
+
+ def sanitize_filename(name)
+ # Sanitize the filename, to prevent hacking
+ name = File.basename(name.gsub("\\", "/")) # work-around for IE
+ name.gsub!(/[^a-zA-Z0-9\.\-\+_]/,"_")
+ name = "_#{name}" if name =~ /^\.+$/ # huh? some specific browser fix?
+ name = "unnamed" if name.size == 0
+ name
+ end
+
+ def expand_filename(fn)
+ if suffix.nil?
+ fn
+ else
+ base, ext = split_extension(fn)
+ "#{base}-#{self.suffix}.#{ext}"
+ end
+ end
+
+ # tries to identify the mime-type of file and correct self's extension
+ # based on the found mime-type
+ def fetch_file_extension( file, local_path, ext )
+ # try to fetch the filename via the 'file' Unix exec
+ content_type = get_content_type( local_path )
+ # Fetch the content type that was passed from the users browser
+ content_type ||= file.content_type.chomp if file.respond_to?(:content_type) and file.content_type
+
+ # Is this one of our known content types?
+ if content_type and options[:fix_file_extensions] and options[:mime_extensions][content_type]
+ # If so, correct the extension
+ return options[:mime_extensions][content_type], content_type
+ else
+ return ext, content_type
+ end
+ end
+
+ # Try to use *nix exec to fetch content type
+ def get_content_type( local_path )
+ if options[:file_exec] and local_path
+ begin
+ content_type = `file -bi "#{local_path}"`.chomp
+ return nil unless $?.success?
+ return nil if content_type =~ /cannot_open/
+ # Cut off ;xyz from the result
+ content_type.gsub!(/;.+$/,"") if content_type =~ /;.+$/
+ return content_type
+ rescue
+ nil
+ end
+ end
+ end
+
+ # Catch when different versions are requested... e.g. upload_column.thumb
+ def method_missing(method_name, *args)
+ if versions and versions.include?(method_name)
+ return versions[method_name.to_sym]
+ end
+ raise NoMethodError.new( "Method #{method_name} not found in UploadColumn::UploadedFile")
+ end
+ end
+
+ # = Basics
+ # When you call an +image_column+ field, an instance of this class will be returned.
+ #
+ # See +image_column+ and the +README+ for more info
+ class UploadedImage < UploadedFile
+
+ attr_reader :width, :height
+ # Resize the image so that it will not exceed the dimensions passed
+ # via geometry, geometry should be a string, formatted like '200x100' where
+ # the first number is the height and the second is the width
+ def resize!( geometry )
+ process! do |img|
+ img.change_geometry( geometry ) do |c, r, i|
+ i.resize(c,r)
+ end
+ end
+ end
+
+ # Resize and crop the image so that it will have the exact dimensions passed
+ # via geometry, geometry should be a string, formatted like '200x100' where
+ # the first number is the height and the second is the width
+ def crop_resized!( geometry )
+ process! do |img|
+ h, w = geometry.split('x')
+ img.crop_resized(h.to_i,w.to_i)
+ end
+ end
+
+ private
+
+ # Convert the image to format
+ def convert!(format)
+ process! do |img|
+ img.format = format.to_s.upcase
+ img
+ end
+ end
+
+ # I eat your memory for breakfast, don't use me!
+ def width
+ unless @width
+ img = process do |img|
+ @width = img.columns
+ @height = img.rows
+ end
+ img = nil
+ GC.start
+ end
+ @width
+ end
+
+ def height
+ unless @height
+ img = process do |img|
+ @width = img.columns
+ @height = img.rows
+ end
+ img = nil
+ GC.start
+ end
+ @height
+ end
+
+ def set_magic_columns
+ super
+ self.instance.class.column_names.each do |column|
+ if column =~ /^#{self.attribute}_(.*)$/
+ case $1
+ when "width"
+ self.instance.send("#{self.attribute}_width=".to_sym, width)
+ when "height"
+ self.instance.send("#{self.attribute}_height=".to_sym, height)
+ when /^exif_(.*)$/
+ if self.mime_type == "image/jpeg"
+ require_gem 'exifr'
+ i = EXIFR::JPEG.new(self.path)
+ self.instance.send("#{self.attribute}_exif_#{$1}=".to_sym, i.exif[$1.to_sym]) if i and i.exif
+ end
+ end
+ end
+ end
+
+ end
+
+ def assign(file, directory = nil )
+ # Call superclass method and check for success (not if this actually IS a version!)
+ if super(file, directory)
+
+ if options[:force_format] and options[:extensions].include?(options[:force_format].to_s)
+ convert!(options[:force_format])
+ @mime_type = options[:mime_extensions][options[:force_format]]
+ self.instance.send("#{self.attribute}_mime_type=".to_sym, self.mime_type) if self.instance.class.column_names.include?("#{self.attribute}_mime_type")
+ self.versions.each { |k, v| v.send(:convert!, options[:force_format]) } if self.versions
+ end
+ if suffix.nil? and options[:versions].respond_to?( :to_hash )
+
+ options[:versions].to_hash.each do |name, size|
+ # Check if size is a string, and if so resize the respective version
+ if size.is_a?( String )
+ if options[:crop]
+ return false unless self.versions[name].crop_resized!( size )
+ elsif size[0,1] == "c"
+ return false unless self.versions[name].crop_resized!( size[1,30] )
+ else
+ return false unless self.versions[name].resize!( size )
+ end
+ elsif size.is_a?( Proc )
+ self.versions[name].process! do |img|
+ img = size.call(img)
+ end
+ elsif size != :none
+ raise TypeError.new( "#{size.inspect} is not a valid option, must be of format '123x123' or a Proc or :none.")
+ end
+ end
+ end
+ return true
+ else
+ return false
+ end
+ end
+
+ def fetch_file_extension( file, local_path, ext )
+ ext, content_type = super(file, local_path, ext)
+ ext = options[:force_format].to_s if options[:force_format] and options[:extensions].include?(options[:force_format].to_s)
+ return ext, content_type
+ end
+
+ end
+
+ 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+ accepts the following common options:
+ # [+versions+] Creates different versions of the file, must be an Array, +image_column+ allows a Hash of dimensions to be passed.
+ # [+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 <tt>:keep</tt>, <tt>:delete</tt> and <tt>:replace</tt>. If set to <tt>:keep</tt> 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.
+ #
+ # and even the following less common ones
+ # [+permissions+] Specify the Unix permissions to be used with UploadColumn. Defaults to 0644.
+ # [+root_path+] The root path where image will be stored, it will be prepended to store_dir and tmp_dir
+ # [+web_root+] Prepended to all addresses returned by UploadColumn::BaseUploadedFile.url
+ # [+mime_extensions+] Overwrite UploadColumns default list of mime-type to extension mappings
+ # [+extensions+] Overwirte UploadColumns default list of extensions that may be uploaded
+ # [+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, set :validate_integrity to true (it is by default). Defaults to true
+ # [+validate_integrity] If set to true, no files with an extension not included in :extensions will be uploaded, defaults to true.
+ # [+file_exec+] Path to an executable used to find out a files mime_type, works only on *nix based systems. Defaults to 'file'
+ def upload_column(attr, options={})
+ register_functions( attr, UploadedFile, options )
+ end
+
+ # Creates a column specifically designed for images, see +upload_column+ for options
+ # Additinally you may specify:
+ # [+crop+] Specifies whether the image will be cropped to fit the dimensions passed
+ # via versions, that way the image will always be exactly the specified size (otherwise
+ # that size would be a maximum), however some areas of the image may be cut off. Default to false.
+ # [+force_format+] Allows you to specify an image format, all images will automatically be converter to that format. (Defaults to false)
+ def image_column( attr, options={} )
+ options[:crop] ||= false
+ options[:web_root] ||= "/images"
+ options[:root_path] ||= File.join(RAILS_ROOT, "public", "images")
+ options[:mime_extensions] ||= IMAGE_MIME_EXTENSIONS
+ options[:extensions] ||= IMAGE_EXTENSIONS
+
+ register_functions( attr, UploadedImage, options )
+ 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...).
+ #
+ # WARNING: validates_integrity_of does NOT work with :validates_integrity => true (which is the default)!
+ #
+ # EVEN STRONGER WARNING: Even if you use validates_integrity_of, potentially harmful files may still be uploaded to your
+ # tmp dir, make sure that these are not in your public directory, otherwise a hacker might seriously damage
+ # your system (by uploading .rb files or similar), if you want to avoid this problem, use :validate_integrity => true instead!
+ 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)
+
+ validates_each(attr_names, configuration) do |record, attr, column|
+ if column and not column.options[:extensions].include?( column.filename_extension )
+ record.errors.add(attr, configuration[:message])
+ end
+ end
+ end
+
+ private
+
+ def register_functions(attr, column_class, options={})
+ upload_column_attr = "@#{attr}_file".to_sym
+ upload_column_method = "#{attr}".to_sym
+
+ define_method upload_column_method do
+ result = instance_variable_get( upload_column_attr )
+ if result.nil?
+ filename = self[attr]
+ if filename.nil? or filename.empty?
+ nil
+ else
+ result = column_class.new(options, self, attr)
+ end
+ instance_variable_set upload_column_attr, result
+ end
+ result
+ end
+
+ define_method "#{attr}=" do |file|
+ if file.nil?
+ self[attr] = nil
+ instance_variable_set upload_column_attr, nil
+ else
+ uploaded_file = instance_variable_get( upload_column_attr )
+ old_file = uploaded_file.dup if uploaded_file
+ uploaded_file ||= column_class.new(options, self, attr)
+ # We simply write over the temp version if it exists
+
+
+ if file and not file.blank? and file.is_a?(String)
+ # if file is a non-empty string it is most probably
+ # the filename and the user forgot to set the encoding
+ # to multipart/form-data. Since we would raise an exception
+ # because of the missing "original_filename" method anyways,
+ # we raise a more meaningful exception rightaway.
+ raise TypeError.new("Do not know how to handle a string with value '#{file}' that was passed to an upload_column. Check if the form's encoding has been set to 'multipart/form-data'.")
+ end
+
+ filesize = file.size if file.respond_to?(:size)
+ filesize = file.stat.size if not file and file.respond_to?(:stat)
+ if file and not file.blank? and filesize != 0 and uploaded_file.send(:assign, file)
+ instance_variable_set upload_column_attr, uploaded_file
+ self.send("#{attr}_after_assign")
+ self[attr] = uploaded_file.to_s
+ #uploaded_file.send(:set_magic_columns)
+ if old_file and [ :replace, :delete ].include?(options[:old_files])
+ old_file.send(:delete)
+ end
+ else
+ # Reset if something's gone wrong
+ instance_variable_set upload_column_attr, old_file
+ end
+ end
+ end
+
+ define_method "#{attr}_temp" do
+ uploaded_file = send(upload_column_method)
+ # Return the real path and the original(!) filename, we need that to fetch the filename later ;)
+ if uploaded_file and uploaded_file.original_basename
+ return uploaded_file.relative_path.sub( "#{uploaded_file.relative_tmp_dir}/", '' ) + ";#{uploaded_file.original_basename}.#{uploaded_file.ext}"
+ elsif uploaded_file
+ return uploaded_file.relative_path.sub( "#{uploaded_file.relative_tmp_dir}/", '' )
+ else
+ return ""
+ end
+ end
+
+ define_method "#{attr}_temp=" do |temp_path|
+ if temp_path and temp_path != ""
+ uploaded_file = instance_variable_get( upload_column_attr )
+ # The actual upload should always have preference over the temp upload
+ unless uploaded_file
+ uploaded_file = column_class.new(options, self, attr)
+ uploaded_file.send(:set_path, temp_path)
+ uploaded_file.send(:set_magic_columns)
+ instance_variable_set upload_column_attr, uploaded_file
+ self[attr] = uploaded_file.to_s
+ end
+ end
+ end
+
+ # Callbacks the user can use to hook into uploadcolumn
+ define_method "#{attr}_filename" do |original, ext|
+ end
+
+ define_method "#{attr}_store_dir" do
+ end
+
+ define_method "#{attr}_tmp_dir" do
+ end
+
+ define_method "#{attr}_after_assign" do
+ end
+
+ # Hook UploadColumn into Rails via before_save, after_save and after_destroy
+ after_save_method = "#{attr}_after_save".to_sym
+
+ define_method after_save_method do
+ uploaded_file = send(upload_column_method)
+ # Check if the filename is blank, is this a tmp file?
+ if uploaded_file and uploaded_file.filename and not uploaded_file.filename.blank? and uploaded_file.dir != uploaded_file.store_dir
+ old_dir = uploaded_file.dir
+ uploaded_file.send(:save)
+ uploaded_file.send(:delete_temporary_files)
+ connection.update(
+ "UPDATE #{self.class.table_name} " +
+ "SET #{quoted_comma_pair_list(connection, {attr => quote_value(uploaded_file.to_s)})} " +
+ "WHERE #{self.class.primary_key} = #{quote_value(self.id)}",
+ "#{self.class.name} Update"
+ )
+ FileUtils.rm_rf(old_dir)
+ end
+ end
+
+ after_save after_save_method
+
+# before_save_method = "#{attr}_before_save".to_sym
+#
+# define_method before_save_method do
+# uploaded_file = send(upload_column_method)
+# if uploaded_file and uploaded_file.dir != uploaded_file.store_dir
+# uploaded_file.send(:filename=, uploaded_file.send(:fetch_filename, self, uploaded_file.send(:original_basename), uploaded_file.send(:ext)))
+# end
+# end
+
+# before_save before_save_method
+
+ # After destroy
+ after_destroy_method = "#{attr}_after_destroy".to_sym
+
+ define_method after_destroy_method do
+ uploaded_file = send(upload_column_method)
+ uploaded_file.send(:delete) if uploaded_file and not [ :keep, :replace ].include?(options[:old_files])
+ end
+ after_destroy after_destroy_method
+
+ private after_save_method, after_destroy_method
+
+
+ end
+
+ end
+
+
+
+end
100 lib/upload_column_helper.rb
@@ -0,0 +1,100 @@
+module UploadColumnHelper
+
+ # Returns an input tag of the "file" type tailored for accessing an upload_column field
+ # (identified by method) on an object assigned to the template (identified by object).
+ # Additional options on the input tag can be passed as a hash with options.
+ #
+ # Example (call, result)
+ # upload_column_field( :user, :picture )
+ # <input id="user_picture_temp" name="user[picture_temp]" type="hidden" />
+ # <input id="user_picture" name="user[picture]" size="30" type="file" />
+ #
+ # Note: if you use file_field instead of upload_column_field, the file will not be
+ # stored across form redisplays.
+ def upload_column_field(object, method, options={})
+ result = ActionView::Helpers::InstanceTag.new(object, method, self).to_input_field_tag("file", options)
+ result << ActionView::Helpers::InstanceTag.new(object, method.to_s+"_temp", self).to_input_field_tag("hidden", {})
+ end
+
+ # A helper method for creating a form tag to use with uploadng files,
+ # it works exactly like Rails' start_form_tag, except that :multipart is always true
+ def upload_form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc)
+ options[:multipart] = true
+ form_tag( url_for_options, options, *parameters_for_url, &proc )
+ end
+
+ # What? You cry, files cannot be uploaded using JavaScript! Well,
+ # you're right. But you see, this method will use an iframe, clever no? What this means
+ # for you is that you'll probably want to fetch the respond_to_parent plugin, that will
+ # make handling this a breeze.
+ # You can pass the following keys to options
+ # [+url+] The target URL
+ # [+fallback+] If JavaScript is disabled, the fallback address will be used, use Rails' ActionController::Base.url_for syntax.
+ # [+force_html+] This will set the target attribute via HTML instead of JS, so if JS is disabled, it will submit to the iframe anyway (defaults to false)
+ # [+html+] HTML options for the form tag
+ # [+iframe+] HTML options for the iframe tag
+ # [+before+] JavaScript called before the form is sent (via onsubmit)
+ # Note: You can NOT use the normal prototype callbacks in this function, since it does not use
+ # Ajax to upload the form.
+ def remote_upload_form_tag( options = {}, &block )
+ framename = "uf#{Time.now.usec}#{rand(1000)}"
+ iframe_options = {
+ "style" => "position: absolute; width: 0; height: 0; border: 0;",
+ "id" => framename,
+ "name" => framename,
+ "src" => ''
+ }
+ iframe_options = iframe_options.merge(options[:iframe].stringify_keys) if options[:iframe]
+
+ form_options = { "method" => "post" }
+ form_options = form_options.merge(options[:html].stringify_keys) if options[:html]
+
+ form_options["enctype"] = "multipart/form-data"
+
+ url = url_for(options[:url])
+
+ if options[:force_html]
+ form_options["action"] = url_for(options[:url])
+ form_options["target"] = framename
+ else
+ form_options["action"] = if options[:fallback] then url_for(options[:fallback]) else url end
+ form_options["onsubmit"] = %(this.action = '#{escape_javascript( url )}'; this.target = '#{escape_javascript( framename )}';)
+ form_options["onsubmit"] << options[:before] if options[:before]
+ end
+ if block_given?
+ content = capture(&block)
+ concat(tag( :iframe, iframe_options, true ) + '</iframe>', block.binding)
+ form_tag( url, form_options, &block )
+ else
+ tag( :iframe, iframe_options, true ) + '</iframe>' + form_tag( form_options[:action], form_options, &block )
+ end
+ end
+
+ # Returns an image tag using a URL created by the set of +options+. Accepts the same options
+ # as ActionController::Base#url_for. It's also possible to pass a string instead of an options
+ # hash.
+ #
+ # Example
+ # image( :action => "solarize_picture", :id => @user )
+ # Use this in conjunction with UploadColumnRenderHelper.render_image to output dynamically
+ # rendered version of your RMagick manipulated images.
+ def image(options = {}, html_options = {})
+ html_options[:src] = if options.is_a?(String) then options else self.url_for(options) end
+ html_options[:alt] ||= File.basename(html_options[:src], '.*').split('.').first.capitalize
+
+ if html_options[:size]
+ html_options[:width], html_options[:height] = html_options[:size].split("x")
+ html_options.delete :size
+ end
+
+ tag("img", html_options)
+ end
+
+end
+
+class ActionView::Helpers::FormBuilder
+ self.field_helpers += ['upload_column_field']
+ def upload_column_field(method, options = {})
+ @template.send(:upload_column_field, @object_name, method, options.merge(:object => @object))
+ end
+end
40 lib/upload_column_render_helper.rb
@@ -0,0 +1,40 @@
+module UploadColumnRenderHelper
+
+ # You can use +render_image+ in your controllers to render an image
+ # def picture
+ # @user = User.find(params[:id])
+ # render_image @user.picture
+ # end
+ # This of course, is not very useful at all (you could simply have linked to the image itself),
+ # However it is even possible to pass a block to render_image that allows manipulation using
+ # RMagick, here the fun begins:
+ # def solarize_picture
+ # @user = User.find(params[:id])
+ # render_image @user.picture do |img|
+ # img = img.segment
+ # img.solarize
+ # end
+ # end
+ # Note that like in UploadColumn::BaseUploadedFile.process you will need to 'carry' the image
+ # since most Rmagick methods do not modify the image itself but rather return the result of the
+ # transformation.
+ #
+ # Instead of passing an upload_column object to +render_image+ you can even pass a path String,
+ # if you do you will have to pass a :mime-type option as well though.
+ def render_image( file, options = {} )
+ format = if options.is_a?(Hash) then options[:force_format] else nil end
+ mime_type = if options.is_a?(String) then options else options[:mime_type] end
+ mime_type ||= file.mime_type
+ path = if file.is_a?( String ) then file else file.path end
+ headers["Content-Type"] = mime_type unless format
+
+ if block_given? or format
+ img = ::Magick::Image::read(path).first
+ img = yield( img ) if block_given?
+ img.format = format.to_s.upcase if format
+ render :text => img.to_blob, :layout => false
+ else
+ send_file( path )
+ end
+ end
+end
136 test/abstract_unit.rb
@@ -0,0 +1,136 @@
+RAILS_ROOT = File.dirname(__FILE__)
+
+require 'test/unit'
+require 'rubygems'
+require_gem 'activesupport'
+require_gem 'activerecord'
+require_gem 'actionpack'
+require 'stringio'
+require 'breakpoint'
+require_gem 'mocha'
+#require 'test_help'
+require File.expand_path(File.join(RAILS_ROOT, '..', 'lib', 'upload_column'))
+require File.expand_path(File.join(RAILS_ROOT, '..', 'lib', 'upload_column_helper'))
+require File.expand_path(File.join(RAILS_ROOT, '..', 'lib', 'upload_column_render_helper'))
+
+#:nodoc:
+
+# Bootstrap the database
+require 'logger'
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+config = YAML::load(File.open("#{RAILS_ROOT}/../../../../config/database.yml"))
+ActiveRecord::Base.establish_connection( config["test"] )
+
+$: << "../lib"
+
+ActiveRecord::Base.send(:include, UploadColumn)
+
+
+#Mock Class for Uploads
+
+class TestUploadedFile
+ # The filename, *not* including the path, of the "uploaded" file
+ attr_accessor :original_filename
+
+ # The content type of the "uploaded" file
+ attr_accessor :content_type
+
+ def initialize(path, content_type = 'text/plain')
+ raise "#{path} file does not exist" unless File.exist?(path)
+ @content_type = content_type
+ @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 }
+ @tempfile = Tempfile.new(@original_filename)
+ FileUtils.copy_file(path, @tempfile.path)
+ end
+
+ def path #:nodoc:
+ @tempfile.path
+ end
+
+ alias local_path path
+
+ def method_missing(method_name, *args, &block) #:nodoc:
+ @tempfile.send(method_name, *args, &block)
+ end
+end
+
+class Test::Unit::TestCase
+ private
+
+ def uploaded_file(basename, mime_type = nil, filename = nil)
+ #ActionController::TestProcess # This is a hack, do not remove :P
+ file = TestUploadedFile.new(
+ file_path( basename ),
+ mime_type.to_s
+ )
+ file.original_filename = filename if filename
+ return file
+ end
+
+ def uploaded_stringio( basename, mime_type = nil, filename = nil)
+ filename ||= basename
+ if basename
+ t = StringIO.new( IO.read( file_path( basename ) ) )
+ else
+ t = StringIO.new
+ end
+ (class << t; self; end).class_eval do
+ define_method(:local_path) { "" }
+ define_method(:original_filename) {filename}
+ define_method(:content_type) {mime_type}
+ end
+ return t
+ end
+
+ def assert_identical( actual, expected, message = nil )
+ message ||= "files #{actual} and #{expected} are not identical."
+ assert FileUtils.identical?(actual, expected), message
+ end
+
+ def assert_not_identical( actual, expected, message = nil )
+ message ||= "files #{actual} and #{expected} are identical, expected to be not identical."
+ assert !FileUtils.identical?(actual, expected), message
+ end
+
+ def file_path( basename )
+ File.join(RAILS_ROOT, 'fixtures', basename)
+ end
+
+ def read_image(path)
+ Magick::Image::read(path).first
+ end
+
+ def assert_max_image_size(img, cols, rows)
+ assert img.columns <= cols, "img has #{img.columns} columns, expected: #{cols}"
+ assert img.rows <= rows, "img has #{img.rows} rows, expected: #{rows}"
+ end
+
+ def assert_image_size(img, cols, rows)
+ assert img.columns == cols, "img has #{img.columns} columns, expected: #{cols}"
+ assert img.rows == rows, "img has #{img.rows} rows, expected: #{rows}"
+ end
+
+end
+
+class TestMigration < ActiveRecord::Migration
+ def self.up
+ create_table :entries do |t|
+ t.column :image, :string
+ t.column :textfile, :string
+ end
+
+ create_table :movies do |t|
+ t.column :movie, :string
+ t.column :name, :string
+ t.column :description, :text
+ end
+ end
+
+ def self.down
+ drop_table :entries
+ drop_table :movies
+ end
+end
+
+ActiveRecord::Migration.verbose = false
0  test/db/.gitignore
No changes.
6 test/fixtures/entries.yaml
@@ -0,0 +1,6 @@
+valid_entry:
+ image: kerb.jpg
+ textfile: pict.png
+blank_entry:
+ image: ""
+ textfile: ""
1  test/fixtures/invalid-image.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  test/fixtures/kerb.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  test/fixtures/skanthak.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
320 test/image_column_test.rb
@@ -0,0 +1,320 @@
+require File.join(File.dirname(__FILE__), 'abstract_unit')
+
+#:nodoc:
+
+Image1 = "kerb.jpg"
+Image2 = "skanthak.png"
+Mime1 = "image/jpeg"
+Mime2 = "image/png"
+ImageInvalid = "invalid-image.jpg"
+MimeInvalid = "image/jpeg"
+
+class Entry < ActiveRecord::Base
+end
+
+class UploadColumnProcessTest < Test::Unit::TestCase
+ def setup
+ TestMigration.up
+ Entry.upload_column :image
+ end
+
+ def teardown
+ TestMigration.down
+ FileUtils.rm_rf File.dirname(__FILE__)+"/public/images"
+ end
+
+ def test_process_before_save
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+ img = e.image.process do |img|
+ img.crop_resized(50, 50)
+ end
+ assert_image_size(img, 50, 50)
+ img = nil
+ GC.start
+ end
+
+ def test_process_after_save
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+ assert e.save
+ img = e.image.process do |img|
+ img.crop_resized(50, 50)
+ end
+ assert_image_size(img, 50, 50)
+ img = nil
+ GC.start
+ end
+
+ def test_process_exclamation_before_save
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+ e.image.process! do |img|
+ img.crop_resized(50, 50)
+ end
+ img = read_image(e.image.path)
+ assert_image_size(img, 50, 50)
+ img = nil
+ GC.start
+ end
+
+ def test_process_exclamation_after_save
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+ assert e.save
+ e.image.process! do |img|
+ img.crop_resized(50, 50)
+ end
+ img = read_image(e.image.path)
+ assert_image_size(img, 50, 50)
+ img = nil
+ GC.start
+ end
+end
+
+class ImageColumnSimpleTest < Test::Unit::TestCase
+ def setup
+ TestMigration.up
+ Entry.image_column :image, :versions => { :thumb => "100x100", :flat => "200x100" }
+ end
+
+ def teardown
+ TestMigration.down
+ FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+ end
+
+ def test_assign
+ e = Entry.new
+ e.image = uploaded_file(Image1, Mime1)
+ assert_not_nil e.image
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ do_test_assign e.image
+ do_test_assign e.image.thumb
+ do_test_assign e.image.flat
+ assert_identical e.image.path, file_path(Image1)
+ assert_not_identical e.image.thumb.path, file_path(Image1)
+ assert_not_identical e.image.flat.path, file_path(Image1)
+ end
+
+ def do_test_assign( file )
+ assert file.is_a?(UploadColumn::UploadedFile), "#{file.inspect} is not an UploadedFile"
+ assert file.respond_to?(:path), "{file.inspect} did not respond to 'path'"
+ assert File.exists?(file.path)
+ end
+
+ def test_resize_without_save
+ e = Entry.new
+ e.image = uploaded_file(Image1, Mime1)
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_max_image_size thumb, 100, 100
+ assert_max_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+ def test_simple_resize_with_save
+ e = Entry.new
+ e.image = uploaded_file(Image1, Mime1)
+ e.save
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_max_image_size thumb, 100, 100
+ assert_max_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+ def test_resize_on_saved_image
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime1)
+ assert e.save
+ e.reload
+ old_path = e.image.path
+
+ e.image = uploaded_file(Image1, Mime1)
+ assert e.save
+ assert_not_equal e.image.path, old_path
+ assert Image1, e.image.filename
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_max_image_size thumb, 100, 100
+ assert_max_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+ def test_manipulate_with_proc
+ Entry.image_column :image, :versions => { :thumb => "100x100", :solarized => proc{|img| img.solarize} }
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+
+ thumb = read_image(e.image.thumb.path)
+ assert_max_image_size thumb, 100, 100
+
+ assert_not_identical e.image.solarized.path, e.image.path
+
+ thumb = nil
+ GC.start
+ end
+
+ def test_invalid_image
+ e = Entry.new
+ assert_nothing_raised do
+ e.image = uploaded_file(ImageInvalid, MimeInvalid)
+ end
+ assert_nil e.image
+ assert e.valid?
+ end
+
+ def test_force_format
+ Entry.image_column :image, :force_format => :png, :versions => { :thumb => "100x100", :flat => "200x100" }
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ assert_equal('png', e.image.filename_extension)
+ assert_equal("kerb.png", e.image.filename)
+ assert_equal("image/png", e.image.mime_type)
+ assert e.save
+ assert_equal('png', e.image.filename_extension)
+ assert_equal("kerb.png", e.image.filename)
+ assert_not_identical( file_path("kerb.jpg"), e.image.path )
+ assert_equal("image/png", e.image.mime_type)
+ end
+end
+
+class ImageColumnCropTest < Test::Unit::TestCase
+
+ def setup
+ TestMigration.up
+ Entry.image_column :image, :crop => true, :versions => { :thumb => "100x100", :flat => "200x100" }
+ end
+
+ def teardown
+ TestMigration.down
+ FileUtils.rm_rf File.dirname(__FILE__)+"/public/entry/"
+ end
+
+ def test_assign
+ e = Entry.new
+ e.image = uploaded_file(Image1, Mime1)
+ assert_not_nil e.image
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ do_test_assign e.image
+ do_test_assign e.image.thumb
+ do_test_assign e.image.flat
+ assert_identical e.image.path, file_path(Image1)
+ assert_not_identical e.image.thumb.path, file_path(Image1)
+ assert_not_identical e.image.flat.path, file_path(Image1)
+ end
+
+ def do_test_assign( file )
+ assert file.is_a?(UploadColumn::UploadedFile), "#{file.inspect} is not an UploadedFile"
+ assert file.respond_to?(:path), "{file.inspect} did not respond to 'path'"
+ assert File.exists?(file.path)
+ end
+
+ def test_resize_without_save
+ e = Entry.new
+ e.image = uploaded_file(Image1, Mime1)
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_image_size thumb, 100, 100
+ assert_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+ def test_simple_resize_with_save
+ e = Entry.new
+ e.image = uploaded_file(Image1, Mime1)
+ e.save
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_image_size thumb, 100, 100
+ assert_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+ def test_resize_on_saved_image
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+ assert e.save
+ e.reload
+ old_path = e.image
+
+ e.image = uploaded_file(Image1, Mime1)
+ assert e.save
+ assert Image1, e.image.filename
+ assert_not_nil e.image.thumb
+ assert_not_nil e.image.flat
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_image_size thumb, 100, 100
+ assert_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+ def test_crop_selected_images_only
+ Entry.image_column :image, :versions => { :thumb => "100x100", :flat => "c200x100" }
+ e = Entry.new
+ e.image = uploaded_file(Image2, Mime2)
+
+ thumb = read_image(e.image.thumb.path)
+ flat = read_image(e.image.flat.path)
+ assert_max_image_size thumb, 100, 100
+ # Thumb is not cropped
+ assert_not_equal(100, thumb.columns)
+ # Flat IS cropped
+ assert_image_size flat, 200, 100
+ flat = nil
+ thumb = nil
+ GC.start
+ end
+
+
+ def test_do_nothing_with_versions
+ Entry.image_column :image, :versions => { :thumb => "100x100", :flat => :none }
+ e = Entry.new
+
+ assert_nothing_raised(TypeError) { e.image = uploaded_file(Image2, Mime2) }
+
+ assert_not_identical( e.image.thumb.path, file_path(Image2) )
+ assert_identical( e.image.flat.path, file_path(Image2) )
+ end
+
+ def test_do_stupid_stuff_with_versions
+ Entry.image_column :image, :versions => { :thumb => "100x100", :flat => 654 }
+ e = Entry.new
+ assert_raise(TypeError) { e.image = uploaded_file(Image2, Mime2) }
+ end
+
+
+ def test_invalid_image
+ e = Entry.new
+ assert_nothing_raised do
+ e.image = uploaded_file(ImageInvalid, MimeInvalid)
+ end
+ assert_nil e.image
+ assert e.valid?
+ end
+end
83 test/upload_column_callback_test.rb
@@ -0,0 +1,83 @@
+require File.join(File.dirname(__FILE__), 'abstract_unit')
+
+#:nodoc:
+
+Entry = Class.new( ActiveRecord::Base )
+Movie = Class.new( ActiveRecord::Base )
+
+class Entry < ActiveRecord::Base
+ attr_accessor :validation_should_fail, :iaac
+
+ upload_column :image
+
+ def validate
+ errors.add("image","some stupid error") if @validation_should_fail
+ end
+
+ def image_store_dir
+ "entries"
+ end
+
+ def image_after_assign
+ iaac = true
+ end
+
+ def after_assign_called?
+ return true if iaac
+ end
+end
+
+class Movie < ActiveRecord::Base
+
+ upload_column :movie
+
+ def movie_store_dir
+ # Beware in this test case you'll HAVE to pass a name... otherwise stupid errors...
+ File.join("files", name)
+ end
+end
+
+
+class UploadColumnTest < Test::Unit::TestCase
+
+ def setup
+ TestMigration.up
+ end
+
+ def teardown
+ TestMigration.down
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/entry/" )
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/movie/" )
+ end
+
+ # A convenience helper, since we'll be doing this a lot
+ def upload_entry
+ e = Entry.new
+ e.image = uploaded_file("skanthak.png", "image/png")
+ return e
+ end
+
+ def test_store_dir
+ e = upload_entry
+ assert e.save
+ assert_equal File.expand_path(File.join(RAILS_ROOT, 'public', 'entries', e.id.to_s, "skanthak.png")), e.image.path
+ end
+
+ def test_complex_store_dir
+ e = Movie.new
+ e.name = "aroo"
+ e.movie = uploaded_file("skanthak.png", "image/png")
+ assert e.save
+ #assert_equal File.expand_path(File.join(RAILS_ROOT, 'public', 'files', 'aroo', e.id.to_s, "skanthak.png")), e.movie.path
+ end
+
+ def test_after_assign
+ e = Entry.new
+ e.image = uploaded_file("skanthak.png", "image/png")
+ assert_not_nil e.image
+ # This DOES work in dev, yet I can't get the assertion to pass, help? please?
+ #assert e.after_assign_called?
+ end
+
+
+end
60 test/upload_column_helper_test.rb
@@ -0,0 +1,60 @@
+require File.dirname(__FILE__) + '/abstract_unit'
+#require 'test_help'
+
+class Entry < ActiveRecord::Base
+ upload_column :image
+end
+
+
+class UploadColumnHelperTest < Test::Unit::TestCase
+ include UploadColumnHelper
+ #include ActionView::Helpers::AssetTagHelper
+ #include ActionView::Helpers::TagHelper
+ #include ActionView::Helpers::UrlHelper
+
+ attr_accessor :entries
+
+ def setup
+ # Can't get fixtures to work, so I'll make them myself :)
+ entries = YAML::load(File.open(File.join(RAILS_ROOT, 'fixtures', 'entries.yaml')))
+ TestMigration.up
+ Entry.upload_column :image
+ Entry.upload_column :textfile
+ for entry in entries
+ e = Entry.new
+ e["image"] = entry[1]["image"]
+ e["textfile"] = entry[1]["textfile"]
+ e.save
+ end
+ end
+
+ def teardown
+ TestMigration.down
+ end
+
+ def test_fixtures
+ e = Entry.find(1)
+ assert_nil e.image
+ assert_nil e.textfile
+ e = Entry.find(2)
+ assert e.image.is_a?( UploadColumn::UploadedFile )
+ assert e.textfile.is_a?( UploadColumn::UploadedFile )
+ end
+
+ def test_upload_column_field
+ @entry = Entry.new
+ assert_not_nil upload_column_field('entry', 'image')
+ assert_equal upload_column_field('entry', 'image'), %(<input id="entry_image" name="entry[image]" size="30" type="file" /><input id="entry_image_temp" name="entry[image_temp]" type="hidden" value="" />)
+ @entry = Entry.find(1)
+ assert_not_nil upload_column_field('entry', 'image')
+ assert_equal upload_column_field('entry', 'image'), %(<input id="entry_image" name="entry[image]" size="30" type="file" /><input id="entry_image_temp" name="entry[image_temp]" type="hidden" value="" />)
+ @entry = Entry.find(2)
+ assert_not_nil upload_column_field('entry', 'image')
+ assert_equal upload_column_field('entry', 'image'), %(<input id="entry_image" name="entry[image]" size="30" type="file" /><input id="entry_image_temp" name="entry[image_temp]" type="hidden" value="#{@entry.image_temp}" />)
+ @entry = Entry.find(2)
+ @entry.image_temp = "1234.56789.1234/kerb.jpg;donkey.png"
+ assert_not_nil upload_column_field('entry', 'image')
+ assert_equal upload_column_field('entry', 'image'), %(<input id="entry_image" name="entry[image]" size="30" type="file" /><input id="entry_image_temp" name="entry[image_temp]" type="hidden" value="1234.56789.1234/kerb.jpg;donkey.png" />)
+ end
+
+end
143 test/upload_column_magic_columns_test.rb
@@ -0,0 +1,143 @@
+require File.join(File.dirname(__FILE__), 'abstract_unit')
+
+#:nodoc:
+
+class Entry < ActiveRecord::Base
+ attr_accessor :validation_should_fail, :iaac
+
+ def validate
+ errors.add("image","some stupid error") if @validation_should_fail
+ end
+end
+
+class Movie < ActiveRecord::Base
+end
+
+class MimeMigration < ActiveRecord::Migration
+ def self.up
+ add_column :entries, :image_mime_type, :string
+ end
+end
+
+class WidthMigration < ActiveRecord::Migration
+ def self.up
+ add_column :entries, :image_width, :integer
+ end
+end
+
+class HeightMigration < ActiveRecord::Migration
+ def self.up
+ add_column :entries, :image_height, :integer
+ end
+end
+
+class SizeMigration < ActiveRecord::Migration
+ def self.up
+ add_column :entries, :image_filesize, :integer
+ end
+end
+
+class ExifMigration < ActiveRecord::Migration
+ def self.up
+ add_column :entries, :image_exif_date_time, :datetime
+ add_column :entries, :image_exif_model, :string
+ end
+end
+
+class UploadColumnMagicColumnTest < Test::Unit::TestCase
+
+ def setup
+ TestMigration.up
+ # we define the upload_columns here so that we can change
+ # settings easily in a single tes
+ Entry.upload_column :image, :file_exec => nil, :validate_integrity => false, :fix_file_extensions => false
+ end
+
+ def teardown
+ TestMigration.down
+ Entry.reset_column_information
+ Movie.reset_column_information
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/entry/" )
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/donkey/" )
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/movie/" )
+ end
+
+ def test_mime_type
+ MimeMigration.up
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "text/html", "index.html")
+ assert_equal "text/html", e.image.mime_type
+ assert_equal "text/html", e.image_mime_type
+ assert_equal "text/html", e['image_mime_type']
+ assert e.save
+ assert_equal "text/html", e.image.mime_type
+ assert_equal "text/html", e.image_mime_type
+ assert_equal "text/html", e['image_mime_type']
+ end
+
+ def test_filesize
+ SizeMigration.up
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ assert_equal 87582, e.image.size
+ assert_equal 87582, e.image_filesize
+ assert_equal 87582, e['image_filesize']
+ assert e.save
+ assert_equal 87582, e.image.size
+ assert_equal 87582, e.image_filesize
+ assert_equal 87582, e['image_filesize']
+ end
+
+ def test_magic_columns_from_tmp
+ SizeMigration.up
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ f = Entry.new
+ f.image_temp = e.image_temp
+ assert_equal 87582, f.image.size
+ assert_equal 87582, f.image_filesize
+ assert_equal 87582, f['image_filesize']
+ assert f.save
+ assert_equal 87582, f.image.size
+ assert_equal 87582, f.image_filesize
+ assert_equal 87582, f['image_filesize']
+
+ end
+
+ def test_width
+ WidthMigration.up
+ Entry.image_column :image
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/png")
+ assert_equal 640, e.image_width
+ assert_equal 640, e['image_width']
+ assert e.save
+ assert_equal 640, e.image_width
+ assert_equal 640, e['image_width']
+ end
+
+ def test_height
+ HeightMigration.up
+ Entry.image_column :image
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/png")
+ assert_equal 480, e.image_height
+ assert_equal 480, e['image_height']
+ assert e.save
+ assert_equal 480, e.image_height
+ assert_equal 480, e['image_height']
+ end
+
+ def test_exif
+ ExifMigration.up
+ Entry.reset_column_information
+ Entry.image_column :image
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ assert_equal Time.at(1061063810), e.image_exif_date_time
+ assert_equal "Canon PowerShot A70", e.image_exif_model
+ assert e.save
+ assert_equal Time.at(1061063810), e['image_exif_date_time']
+ assert_equal "Canon PowerShot A70", e['image_exif_model']
+ end
+end
941 test/upload_column_test.rb
@@ -0,0 +1,941 @@
+require File.join(File.dirname(__FILE__), 'abstract_unit')
+
+#:nodoc:
+
+class Entry < ActiveRecord::Base
+ attr_accessor :validation_should_fail, :iaac
+
+ def validate
+ errors.add("image","some stupid error") if @validation_should_fail
+ end
+end
+
+class Movie < ActiveRecord::Base
+end
+
+
+class UploadColumnTest < Test::Unit::TestCase
+
+ def setup
+ TestMigration.up
+ # we define the upload_columns here so that we can change
+ # settings easily in a single tes
+ Entry.upload_column :image
+ Entry.upload_column :textfile
+ Movie.upload_column :movie
+ end
+
+ def teardown
+ TestMigration.down
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/entry/" )
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/donkey/" )
+ FileUtils.rm_rf( File.dirname(__FILE__)+"/public/movie/" )
+ end
+
+ # A convenience helper, since we'll be doing this a lot
+ def upload_entry
+ e = Entry.new
+ e.image = uploaded_file("skanthak.png", "image/png")
+ return e
+ end
+
+ def test_column_write_method
+ assert Entry.new.respond_to?("image=")
+ end
+
+ def test_column_read_method
+ assert Entry.new.respond_to?("image")
+ end
+
+ def test_sanitize_filename
+ e = upload_entry
+ e.image.send(:filename=, "test.jpg")
+ assert_equal "test.jpg", e.image.filename
+
+ e.image.send(:filename=, "test-s,%&m#st?.jpg")
+ assert_equal "test-s___m_st_.jpg", e.image.filename, "weird signs not escaped"
+
+ e.image.send(:filename=, "../../very_tricky/foo.bar")
+ assert e.image.filename !~ /[\\\/]/, "slashes not removed"
+
+ e.image.send(:filename=, '`*foo')
+ assert_equal "__foo", e.image.filename
+
+ e.image.send(:filename=, 'c:\temp\foo.txt')
+ assert_equal "foo.txt", e.image.filename
+
+ e.image.send(:filename=, ".")
+ assert_equal "_.", e.image.filename
+ end
+
+ def test_default_options
+ e = upload_entry
+ e.id = 10
+ assert_equal File.join(RAILS_ROOT, "public"), e.image.options[:root_path]
+ assert_equal "", e.image.options[:web_root]
+ assert_equal UploadColumn::MIME_EXTENSIONS, e.image.options[:mime_extensions]
+ assert_equal UploadColumn::EXTENSIONS, e.image.options[:extensions]
+ assert_equal true, e.image.options[:fix_file_extensions]
+ assert_equal File.join('entry', 'b', '10'), e.image.options[:store_dir].call(e,'b')
+ assert_equal File.join('entry', 'b', 'tmp'), e.image.options[:tmp_dir].call(e,'b')
+ assert_equal :delete, e.image.options[:old_files]
+ assert_equal true, e.image.options[:validate_integrity]
+ assert_equal 'file', e.image.options[:file_exec]
+ assert_equal "duck.png", e.image.options[:filename].call(e,'duck','png')
+ assert_equal 0644, e.image.options[:permissions]
+ end
+
+ def test_assign_without_save_with_tempfile
+ e = upload_entry
+ do_test_assign(e)
+ end
+
+ def test_assign_without_save_with_stringio
+ e = Entry.new
+ e.image = uploaded_stringio("skanthak.png", "image/png")
+ do_test_assign(e)
+ end
+
+ def test_assign_without_save_with_file
+ e = Entry.new
+ f = File.open(file_path('skanthak.png'))
+ e.image = f
+ f.close
+ do_test_assign(e)
+ end
+
+ def test_assign_twice
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ do_test_assign(e, "kerb.jpg")
+ e.image = uploaded_file("skanthak.png", "image/png")
+ do_test_assign(e)
+ end
+
+ def do_test_assign(e, basename="skanthak.png")
+ assert e.image.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert e.image.respond_to?(:path), "{e.image.inspect} did not respond to 'path'"
+ assert File.exists?(e.image.path)
+ assert_identical e.image.path, file_path(basename)
+ assert_match %r{^((\d+\.)+\d+)/([^/].+)$}, e.image_temp
+ end
+
+ def test_filename_preserved
+ e = upload_entry
+ assert_equal "skanthak.png", e.image.to_s
+ assert_equal "skanthak.png", e.image.filename
+ assert_equal "skanthak", e.image.filename_base
+ assert_equal "png", e.image.filename_extension
+ assert_equal "skanthak", e.image.original_basename
+ assert_equal "png", e.image.ext
+ end
+
+ def test_filename_stored_in_attribute
+ e = Entry.new("image" => uploaded_file("kerb.jpg", "image/jpeg"))
+ assert e.image.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert e.image.respond_to?(:path), "{e.image.inspect} did not respond to 'path'"
+ assert File.exists?(e.image.path)
+ assert_identical e.image.path, file_path("kerb.jpg")
+ end
+
+ def test_with_string
+ e = Entry.new
+ assert_raise(TypeError) do
+ e.image = "duck"
+ end
+ end
+
+ def test_extension_added
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg", "kerb")
+ assert_equal "kerb.jpg", e.image.filename
+ assert_equal "kerb.jpg", e["image"]
+ end
+
+ def test_extension_unknown_type
+ Entry.upload_column :image, :file_exec => false
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "not/known", "kerb")
+ assert_nil e.image
+ Entry.upload_column :image, :validate_integrity => false, :file_exec => false
+ e.image = uploaded_file("kerb.jpg", "not/known", "kerb")
+ assert_equal "kerb", e.image.filename
+ assert_equal "kerb", e["image"]
+ end
+
+ def test_extension_unknown_type_with_extension
+ Entry.upload_column :image, :file_exec => false
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "not/known", "kerb.abc")
+ assert_nil e.image
+ Entry.upload_column :image, :validate_integrity => false, :file_exec => false
+ e.image = uploaded_file("kerb.jpg", "not/known", "kerb.abc")
+ assert_equal "kerb.abc", e.image.filename
+ assert_equal "kerb.abc", e["image"]
+ end
+
+ def test_extension_corrected
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg", "kerb.jpeg")
+ assert_equal "kerb.jpg", e.image.filename
+ assert_equal "kerb.jpg", e["image"]
+ end
+
+ def test_double_extension
+ Entry.upload_column :image, :file_exec => false
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "application/x-tgz", "kerb.tar.gz")
+ assert_equal "kerb.tar.gz", e.image.filename
+ assert_equal "kerb.tar.gz", e["image"]
+ end
+
+ def test_get_content_type_with_file
+
+ # run this test only if the machine we are running on
+ # has the file utility installed
+ if File.executable?("/usr/bin/file")
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", nil, "kerb") # no content type passed
+ assert_not_nil e.image
+ assert_equal "kerb.jpg", e.image.filename
+ assert_equal "kerb.jpg", e["image"]
+ else
+ puts "Warning: Skipping test_get_content_type_with_file test as '/usr/bin/file' does not exist"
+ end
+ end
+
+ def test_do_not_fix_file_extensions
+ Entry.upload_column :image, :fix_file_extensions => false
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg", "kerb.jpeg")
+ assert_equal "kerb.jpeg", e.image.filename
+ assert_equal "kerb.jpeg", e["image"]
+ # Assign an invalid file
+ e.image = uploaded_file("skanthak.png", "image/png", "skanthak")
+ assert_equal "kerb.jpeg", e.image.filename
+ assert_equal "kerb.jpeg", e["image"]
+ end
+
+ def test_do_not_fix_file_extensions_without_validating_integrity
+ Entry.upload_column :image, :fix_file_extensions => false, :validate_integrity => false
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg", "kerb.jpeg")
+ assert_equal "kerb.jpeg", e.image.filename
+ assert_equal "kerb.jpeg", e["image"]
+ e.image = uploaded_file("kerb.jpg", "image/jpeg", "kerb")
+ assert_equal "kerb", e.image.filename
+ assert_equal "kerb", e["image"]
+ end
+
+ def test_validate_integrity
+ Entry.upload_column :image, :fix_file_extensions => false
+ e = Entry.new
+ # invalid file
+ e.image = uploaded_file("kerb.jpg", "image/jpeg", "kerb")
+ assert_nil e.image
+ end
+
+ def test_assign_with_save
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ tmp_file_path = e.image.path
+ assert e.save
+ assert File.exists?(e.image.path)
+ assert FileUtils.identical?(e.image.path, file_path("kerb.jpg"))
+ assert_equal File.expand_path(File.join(RAILS_ROOT, 'public', 'entry', 'image', e.id.to_s, "kerb.jpg")), e.image.path
+ assert_equal "entry/image/#{e.id}/kerb.jpg", e.image.relative_path
+ assert !File.exists?(tmp_file_path), "temporary file '#{tmp_file_path}' not removed"
+ assert !File.exists?(File.dirname(tmp_file_path)), "temporary directory '#{File.dirname(tmp_file_path)}' not removed"
+
+ local_path = e.image.path
+ e = Entry.find(e.id)
+ assert e.image.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert e.image.respond_to?(:path), "{e.image.inspect} did not respond to 'path'"
+ assert File.exists?(e.image.path)
+ assert_equal local_path, e.image.path
+ assert_identical e.image.path, file_path("kerb.jpg")
+ end
+
+ # Tests store_dir, relative_store_dir, tmp_dir, relative_tmp_dir, dir and relative_dir
+ def test_dir_methods
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+
+ assert_equal File.join("entry", "image", e.id.to_s), e.image.relative_store_dir
+ assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "entry", "image", e.id.to_s)), e.image.store_dir
+
+ assert_equal File.join("entry", "image", "tmp"), e.image.relative_tmp_dir
+ assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "entry", "image", "tmp")), e.image.tmp_dir
+
+ assert_match %r{^#{File.join('entry', 'image', 'tmp', '(\d+\.)+\d+')}$}, e.image.relative_dir
+ assert_match %r{^#{File.expand_path(File.join(RAILS_ROOT, 'public', 'entry', 'image', 'tmp', '(\d+\.)+\d+'))}$}, e.image.dir
+
+ e.save
+
+ assert_equal File.join("entry", "image", e.id.to_s), e.image.relative_store_dir
+ assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "entry", "image", e.id.to_s)), e.image.store_dir
+
+ assert_equal File.join("entry", "image", "tmp"), e.image.relative_tmp_dir
+ assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "entry", "image", "tmp")), e.image.tmp_dir
+
+ assert_equal File.join("entry", "image", e.id.to_s), e.image.relative_dir
+ assert_equal File.expand_path(File.join(RAILS_ROOT, "public", "entry", "image", e.id.to_s)), e.image.dir
+ end
+
+ def test_assign_with_save_and_multiple_versions
+ Entry.upload_column :image, :versions => [ :thumb, :large ]
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+
+ tmp_file_path = e.image.path
+
+ assert e.save
+ assert File.exists?(e.image.path)
+
+ assert e.image.thumb.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert File.exists?(e.image.thumb.path)
+ assert_identical e.image.thumb.path, file_path("kerb.jpg")
+ assert_equal File.expand_path(File.join(RAILS_ROOT, 'public', 'entry', 'image', e.id.to_s, "kerb-thumb.jpg")), e.image.thumb.path
+ assert_equal "entry/image/#{e.id}/kerb-thumb.jpg", e.image.thumb.relative_path
+
+ assert e.image.large.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert File.exists?(e.image.large.path)
+ assert_identical e.image.large.path, file_path("kerb.jpg")
+ assert_equal File.expand_path(File.join(RAILS_ROOT, 'public', 'entry', 'image', e.id.to_s, "kerb-large.jpg")), e.image.large.path
+ assert_equal "entry/image/#{e.id}/kerb-large.jpg", e.image.large.relative_path
+
+ assert !File.exists?(File.dirname(tmp_file_path)), "temporary directory '#{File.dirname(tmp_file_path)}' not removed"
+
+ local_path = e.image.thumb.path
+ e = Entry.find(e.id)
+ assert e.image.thumb.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert File.exists?(e.image.thumb.path)
+ assert_equal local_path, e.image.thumb.path
+ assert_identical e.image.thumb.path, file_path("kerb.jpg")
+
+ end
+
+ def test_absolute_path_is_simple
+ # we make :root_path more complicated to test that it is normalized in absolute paths
+ Entry.upload_column :image, {:root_path => File.join(RAILS_ROOT, "public") + "/../public" }
+
+ e = Entry.new
+ e.image = uploaded_file("kerb.jpg", "image/jpeg")
+ assert File.exists?(e.image.path)
+ assert e.image.path !~ /\.\./, "#{e.image.path} is not a simple path"
+ end
+
+
+ def test_cleanup_after_destroy
+ e = Entry.new("image" => uploaded_file("kerb.jpg", "image/jpeg"))
+ assert e.save
+ local_path = e.image.path
+ assert File.exists?(local_path)
+ assert e.destroy
+ assert !File.exists?(local_path), "'#{local_path}' still exists although entry was destroyed"
+ assert !File.exists?(File.dirname(local_path))
+ end
+
+ def test_assign_tmp_image
+ e = Entry.new("image" => uploaded_file("kerb.jpg", "image/jpeg") )
+ e.validation_should_fail = true
+ assert !e.save, "e should not save due to validation errors"
+
+ assert_match %r{^((\d+\.)+\d+)/([^/].+)$}, e.image_temp
+ assert File.exists?(local_path = e.image.path)
+
+ image_temp = e.image_temp
+
+ e = Entry.new("image_temp" => image_temp)
+ assert_equal local_path, e.image.path
+ assert e.save
+
+ assert e.image.is_a?(UploadColumn::UploadedFile), "#{e.image.inspect} is not an UploadedFile"
+ assert File.exists?(e.image.path)
+ assert_equal File.expand_path(File.join(RAILS_ROOT, 'public', 'entry', 'image', e.id.to_s, "kerb.jpg")), e.image.path
+ assert_equal "entry/image/#{e.id}/kerb.jpg", e.image.relative_path
+ assert_identical e.image.path, file_path("kerb.jpg")
+ end
+
+ def test_assign_tmp_image_with_existing_image
+ e = Entry.new("image" => uploaded_file("kerb.jpg", "image/jpeg") )
+ assert e.save
+ assert File.exists?(local_path = e.image.path)
+
+ e = Entry.find(e.id)
+ e.image = uploaded_file("skanthak.png", "image/png")
+ e.validation_should_fail = true
+
+ assert !e.save
+ temp_path = e.image_temp
+
+ e = Entry.find(e.id)
+ e.image_temp = temp_path
+ assert e.save
+
+ assert_equal "skanthak.png", e.image.filename
+ assert_identical e.image.path, file_path("skanthak.png")
+ #assert !File.exists?(local_path), "old image has not been deleted"
+ end
+
+ def test_replace_tmp_image_temp_first