Milton

Milton is an upload and attachment handling plugin for Rails. It is built for extensibility and has support for resizing images and Amazon S3 built in.

Description

Milton is an upload handling plugin for Rails. It is meant to handle multiple types of file uploads. It includes support for thumbnailing of image uploads, and more processors can easily be added for handling other file types. It also supports multiple file stores including disk storage and Amazon S3. Milton also supports on-demand file processing for prototyping.

Getting Started

You can get started with Milton with the default settings by simply calling is_attachment on your ActiveRecord model you'll be handling files with. It is expected that your underlying table will have a filename (string) column. If you'd like you can also define content_type (string) and size (integer) columns to have that info automatically stored as well.

Example

An Asset model:

class Asset < ActiveRecord::Base
  is_attachment
end

Your new Asset form:

- form_for Asset.new, :html => { :enctype => 'multipart/form-data' } do |f|
  = f.file_field :file
  = f.submit 'Upload'

Note: don't forget to add :html => { :enctype => 'multipart/form-data' }

Your AssetsController:

class AssetsController < ApplicationController
  def create
    @asset = Asset.create params[:asset]
  end
end

Resizing Images

Currently Milton relies on ImageMagick (but not RMagick!). Once you have ImageMagick installed, add the :thumbnail processor to your configuration and define your processing recipes:

class Image < ActiveRecord::Base
  is_attachment :recipes => { :thumb => [{ :thumbnail => { :size => '100x100', :crop => true } }] }
end

@image.path => .../000/000/000/001/milton.jpg
@image.path(:thumb) => .../000/000/000/001/milton.crop_size-100x100.jpg

Thumbnail Options

Note: currently the only options supported are :size and :crop.

:size takes a geometry string similar to other image-manipulation plugins (based off ImageMagick's geometry strings).

:size => '50x50' will resize the larger dimension down to 50px and maintain aspect ratio (you can use :crop for forced zoom/cropping).

:size => '50x' will resize the width to 50px and maintain aspect ratio.

:size => 'x50' will resize the height to 50px and maintain aspect ratio.

Then you can throw in :crop to get zoom/cropping functionality:

@image.path(:thumbnail => { :size => '50x50', :crop => true })

This will create a 50px x 50px version of the image regardless of the source aspect-ratio. It will not distort the source image, rather it will resize the image as close to fitting as possible without distorting, then crop off the remainder.

By default :crop uses a North/Center gravity -- so the remainder will be cropped from the bottom and equally from both sides.

Note: the :size option is required when resizing.

Embedding Images

#path will always return the full path to the image, in your views you probably want to refer to the "public" path -- the portion of the path from your /public folder up for embedding your images. For now there is a helper method that gets attached to your model called #public_path that simply gives you the path from public on.

@image.public_path => '/assets/000/000/001/234/milton.jpg'

As opposed to:

@image.path(:thumb) => '/var/www/site/public/assets/000/000/001/234/milton.jpg'

Note: if you use the :file_system_path option to upload your files to somewhere outside of your public folder this will no longer work. You can pass a different folder to public_path to use as an alternate base.

@image.public_path(:thumb, 'uploads')

Processors

Processors are defined in recipes and are loaded when Milton is loaded. In the above example we're telling Milton to load the thumbnail processor (which comes with Milton) and then telling it to pass { :size => '100x100', :crop => true } to the thumbnail processor in order to create a derivative called :thumb for all uploaded Images.

More processors can be loaded and combined into the recipe, to be run in the order specified (hence the array syntax), i.e.:

class Image < ActiveRecord::Base
  is_attachment :recipes => {
    :watermarked_thumb => [{ :watermark => 'Milton' }, { :thumbnail => { :size => '100x100', :crop => true } }]
  }
end

This recipe would create a watermarked version of the originally uploaded file, then create a 100x100, cropped thumbnail of the watermarked version.

When processors would create the same derivative they use the already created derivative and do not recreate it, so if you had:

class Image < ActiveRecord::Base
  is_attachment :processors => [ :thumbnail, :watermark ], :recipes => { 
    :thumb => [{ :watermark => 'Milton' }, { :thumbnail => { :size => '100x100', :crop => true } }],
    :small => [{ :watermark => 'Milton' }, { :thumbnail => { :size => '250x' } }]
  }
end

The watermarking would only be done once and both thumbnails would be created from the same watermarked version of the original image.

Note: There is no :watermark processor, just an example of how processors can be combined.

Post-processing

Post-processing allows you to create derivatives by running processors on demand instead of when the file is uploaded. This is particularly useful for prototyping or early-on in development when processing options are changing rapidly and you want to play with results immediately.

You can pass :postprocessing => true to is_attachment in order to turn on post-processing of files. This is recommended only for prototyping as it works by checking the existence of the requested derivative every time the derivative is requested to determine if it should be processed or not. With disk storage in development this can be quite fast, but when using S3 or in production mode it is definitely not recommended.

Post-processing allows you to pass recipes to path, i.e.:

@image.path(:thumbnail => { :size => '100x100', :crop => true })

If the particular derivative (size of 100x100 and cropped) doesn't exist it will be created.

Note: Without post-processing turned on the call to path above would still return the same path, it just wouldn't create the underlying file.

Amazon S3

Milton comes with support for Amazon S3. When using S3 uploads and derivatives are stored locally in temp files then sent to S3. To use S3 you need to pass a few options to is_attachment:

class Asset < ActiveRecord::Base
  is_attachment :storage => :s3, :storage_options => { :api_access_key => '123', :secret_access_key => '456', :bucket => 'assets' }
end

Where :api_access_key and :secret_access_key are your API access key and secret access key from your Amazon AWS account and :bucket is the S3 bucket you would like your files to be stored in.

When using S3 files are stored in folders according to the associated model's ID. So in the above example the URL to a stored file might be:

@image.path => http://assets.amazonaws.com/1/milton.jpg

Storage Options

By default Milton uses your local disk for storing files. Additional storage methods can be used by passing the :storage option to is_attachment (as in the S3 example above). Milton comes included with :s3 and :disk storage.

Disk Storage Options

When using disk storage (default) the following options can be passed in :storage_options:

  • :root (default <Rails.root>/public/<table name>) -- is the root path to where files are/will be stored on your file system. The partitioned portion of the path is then added onto this root to generate the full path. You can do some useful stuff with this like pulling your assets out of /public if you want them to be non-web-accessible.
  • :chmod (default 0755) -- is the mode to set on on created folders and uploaded files.

Note: If you're using Capistrano for deployment with disk storage remember to put your :root path in shared and link it up on deploy so you don't lose your uploads between deployments!

General Options

  • :separator (default '.') -- is the character used to separate the options from the filename in cached derivative files (i.e. resized images). It will be stripped from the filename of any incoming file.
  • :replacement (default '-') -- is the character used to replace :separator after stripping it from the incoming filename. :tempfile_path (default <Rails.root>/tmp/milton) -- is the path used for Milton's temporary storage.

Installation

Gem

$ gem install citrusbyte-milton --source=http://gems.github.com

Ruby on Rails gem plugin:

Add to your environment.rb:

config.gem "citrusbyte-milton", :lib => 'milton', :source => 'http://gems.github.com'

Then run:

$ rake gems:install

Ruby on Rails plugin

script/plugin install git://github.com/citrusbyte/milton.git

You will also need to install ImageMagick to use image resizing.

Dependencies

  • Ruby on Rails w/ ActiveRecord
  • A filesystem (hopefully this one is covered...)
  • ImageMagick (only if using :thumbnail processor)
  • mimetype-fu (not required, but if available will be used to recognized mime types of incoming files)
  • right_aws (if using S3 storage option)

Migrating

From 0.2.4

Milton is initialized with only is_attachment as of 0.3.0, so is_resizeable, is_uploadable, etc... are no longer used.

:postprocessing is off by default and should only be used in development now. Recipes should be used instead of post-processing -- see Processors above.

Extended Usage Examples

Basic User Avatar

class User < ActiveRecord::Base
  has_one :avatar, :dependent => :destroy
end

class Avatar < ActiveRecord::Base
  is_attachment :recipes => { :small => [{ :thumbnail => { :size => '100x100', :crop => true } }] }
  belongs_to :user
end

Allow user to upload an avatar when creating

class UsersController < ActiveRecord::Base
  def create
    @user = User.new params[:user]
    @user.avatar = Avatar.new(params[:avatar]) if params[:avatar] && params[:avatar][:file]

    if @user.save
      ...
    else
      ...
    end

    ...
  end
end

Allow user to upload a new avatar, note that we don't care about updating files in this has_one case, we're just gonna set a new relationship (which will destroy the existing one)

class AvatarsController < ActiveRecord::Base
  def create
    @user = User.find params[:user_id]

    # setting a has_one on a saved object saves the new related object
    if @user.avatar = Avatar.new(params[:avatar])
      ...
    else
      ...
    end

    ...
  end
end

User's profile snippet (in Haml)

#profile
  = image_tag(@user.avatar.public_path(:small))
  = @user.name

Contributors

  • Upload and file handling based on AttachmentPow by Ari Lerner (auser)
  • Cropping and thumbnailing calculations based on code by Nicol├ís Sanguinetti (foca)
  • Damian Janowski (djanowski)
  • Michel Martens (soveran)

License

Copyright (c) 2009 Ben Alavi for Citrusbyte

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.