您的位置:首页 > 大数据 > 人工智能

AJAX file uploads in Rails using attachment_fu and responds_to_parent2

2014-03-10 20:50 375 查看
In this walkthrough, I go through the available options and an example using attachment_fu to handle file uploads and image thumbnailing, and responds_to_parent to implement the

iframe remoting pattern to work around javascript’s security restrictions on file system access.

You can also
download the complete example.

This is an outdated article. I will be updating with a new article soon.

Step 1. Choose a file upload plugin

Sure, you can write one yourself (or bake the code directly into your app), but unless you have specific requirements you should take a look at what’s available. Even if you do have a good excuse, you can learn from the existing plugins or extend them. The
three that I’ve used over the past two years are:

file_column – the first file upload plugin available for Rails that I know of, it handles saving files to the filesystem, resizing of images, creation of thumbnails, and integration
with rmagick; however it doesn’t seem to be in active development.
acts_as_attachment – written by
Rick Olson, it does everything that file_column can, but with a cleaner and extensible code base.

attachment_fu – is a rewrite of acts_as_attachment adding a plugin architecture to extend it to add different image processors (image_science, mini_magick and rmagick
are provided) and storage backends (database, file system, and Amazon S3). The only problem is you need Rails 1.2+

Recommendation: attachment_fu if you are using Rails 1.2+, otherwise acts_as_attachment.

Step 2. Determine which Image Processor you want to use.

attachment_fu supports three processors out of the box:

image_science – a light ruby wrapper around the FreeImage library, it can only be used to resize images. It used to have problems with image quality of thumbnails and
PNG color profiles but these have recently been fixed.
RMagick – a ruby wrapper around the ImageMagick/GraphicsMagick libraries, it provides a lot of advanced image processing features. It’s memory hungry though, and can max resource limits on some
shared hosts causing your app to fail; it’s happened to me a few times on large images.

minimagick – another wrapper around ImageMagick, however it resizes images using imagemagick’s
mogrify command. If you are hitting resource limits on your host, minimagick is preferred over rmagick.

Recommendation: image_science if you only need image resizing and can handle the slightly inferior thumbnail quality, minimagick otherwise.

Step 3. Install image processors and attachment_fu

The installation process is quite long for the image processors, so I’ve just linked to them here:

RMagick/ImageMagick:
Mac OS X,
Linux, and
Windows
FreeImage/image_science:
Mac OS X, Linux
To install minimagick:
sudo gem install mini_magick


To install attachment_fu:
script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/[/code] 

Step 4. Add uploading to your code

I’ll use a restful model for our file uploads since it’s all the rage (here’s a good

introduction). You can create a restful scaffold using the following command:

ruby script/generate scaffold_resource asset filename:string content_type:string size:integer width:integer height:integer parent_id:integer thumbnail:string created_at:datetime

This will create the controllers, models, views and a migration. I’ve included support for saving image properties (
width
and
height
attributes) and thumbnailing (
parent_id
and
thumbnail
attributes).

Here is the resulting migration if you want to do it manually:

class CreateAssets < ActiveRecord::Migration
def self.up
create_table :assets do |t|
t.column :filename,     :string
t.column :content_type, :string
t.column :size,         :integer
t.column :width,        :integer
t.column :height,       :integer
t.column :parent_id,    :integer
t.column :thumbnail,    :string
t.column :created_at,   :datetime
end
end  def self.down
drop_table :assets
end
end

In the model, it’s really a one liner to add file upload features.

class Asset < ActiveRecord::Base  has_attachment  :storage => :file_system,
:max_size => 1.megabytes,
:thumbnails => { :thumb => '80x80>', :tiny => '40x40>' },
:processor => :MiniMagick # attachment_fu looks in this order: ImageScience, Rmagick, MiniMagick  validates_as_attachment # ok two lines if you want to do validation, and why wouldn't you?
end

The
has_attachment
(or
acts_as_attachment
method for those not using attachment_fu) adds a lot of useful methods such as
image?
to determine if the file is an image, and
public_filename(thumbnail=nil)
to retrieve the filename for the original or thumbnail. I usually add methods to determine other file types such as movies, music, and documents.

The options available are:

content_type
– Allowed content types. Allows all by default. Use
:image
to allow all standard image types.
min_size
– Minimum size allowed. 1 byte is the default.
max_size
– Maximum size allowed. 1.megabyte is the default.
size
– Range of sizes allowed. (
1..1.megabyte
) is the default. This overrides the
:min_size
and
:max_size
options.
resize_to
– Used by RMagick to resize images. Pass either an array of width/height, or a geometry string.

thumbnails
– Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options.

thumbnail_class
– Set what class to use for thumbnails. This attachment class is used by default.

path_prefix
– path to store the uploaded files. Uses
public/#{table_name}
by default for the filesystem, and just
#{table_name}
for the S3 backend. Setting this sets the
:storage
to
:file_system
.
storage
– Use
:file_system
to specify the attachment data is stored with the file system. Defaults to
:db_system
.

In the above we’re storing the files in the file system and are adding two thumbnails if it’s an image: one called ‘thumb’ no bigger than 80×80 pixels, and the other called ‘tiny’. By default, these will be stored in the same directory as the original: /public/assets/nnnn/mmmm/
with their thumbnail name as a suffix. To show them in the view, we just do the following:
<%= image_tag(image.public_filename(:thumb)) %>


validates_as_attachment
ensures that
size
,
content_type
and
filename
are present and checks against the options given to
has_attachment
; in our case the original should be no larger than 1 megabyte.

To enable multipart file uploads, we need to set
multipart => true
as a form option in
new.rhtml
. The
uploaded_data
file input field is used by attachment_fu to store the file contents in an attribute so that attachment_fu can do its magic when the
uploaded_data=
method is called.

<%= error_messages_for :asset %>
<% form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |form| %>
<p>
<label for="uploaded_data">Upload a file:</label>
<%= form.file_field :uploaded_data %>
</p>
<p>
<%= submit_tag "Create" %>
</p>
<% end %>

We’ll also pretty up the index code. We want to show a thumbnail if the file is an image, otherwise just the name:

<h1>Listing assets</h1>
<ul id="assets">
<% @assets.each do |asset| %>
<li id="asset_<%= asset.id %>">
<% if asset.image? %>
<%= link_to(image_tag(asset.public_filename(:thumb))) %><br />
<% end %>
<%= link_to(asset.filename, asset_path(asset)) %> (<%= link_to "Delete", asset_path(asset), :method => :delete, :confirm => "are you sure?"%>)
</li>
<% end %>
</ul><br /><%= link_to 'New asset', new_asset_path %>

Don’t forget to do a
rake db:migrate
to add the assets table. At this stage you can start your server and go to
http://localhost:3000/assets/new to add a new file. After being redirected back to the index page you’ll notice that thumbnails are showing in our index with the originals. To get rid of this, we
can modify
assets_controller
to only display originals by checking if the
parent_id
attribute is
nil
. attachment_fu also allows you to store thumbnails into a different model, which would make this step unnecessary.

def index
@assets = Asset.find(:all, :conditions => {:parent_id => nil}, :order => 'created_at DESC')
respond_to do |format|
format.html # index.rhtml
format.xml  { render :xml => @assets.to_xml }
end
end

Step 5. AJAX it

Let’s try and AJAX our file uploads. The current user flow is:

go to index page
click on “new file” link
choose a file and submit the form
get redirected to index.

What we want to happen is to have all that occur on the index page, with no page refreshes. Normally you would do the following:

Add the Javascript prototype/scriptaculous libraries into your layout.

<%= javascript_include_tag :defaults %>

Change the
form_for
tag to a
remote_form_for


<% remote_form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |f| %>

Add
format.js
to the
create
action in the controller to handle
AJAX requests:

def create
@asset = Asset.new(params[:asset])
respond_to do |format|
if @asset.save
flash[:notice] = 'Asset was successfully created.'
format.html { redirect_to asset_url(@asset) }
format.xml  { head :created, :location => asset_url(@asset) }
format.js
else
format.html { render :action => "new" }
format.xml  { render :xml => @asset.errors.to_xml }
format.js
end
end
end

Make a
create.rjs
file to insert the asset at the bottom of your list:

page.insert_html :bottom, "assets", :partial => 'assets/list_item', :object => @asset
page.visual_effect :highlight, "asset_#{@asset.id}"

Create a partial to show the image in the list

<li id="asset_<%= list_item.id %>">
<% if list_item.image? %>
<%= link_to(image_tag(list_item.public_filename(:thumb))) %><br />
<% end %>
<%= link_to(list_item.filename, asset_path(list_item))%> (<%= link_to_remote("Delete", {:url => asset_path(list_item), :method => :delete, :confirm => "are you sure?"}) %>)
</li>

Add AJAX deletion (optional)

If you’ve noticed the changes in the previous code, I’ve added
AJAX deletion of files as well. To enable this on the server we add a
destroy.rjs
file to remove the deleted file form the list.

page.remove "asset_#{@asset.id}"

In the controller you also need to add
format.js
to the
delete
action.

Keep our form views DRY (optional)

We should also make the file upload form contents into a partial and use it in
new.rhtml
as well as
index.rhtml
.

_form.rhtml

<p>
<label for="uploaded_data">Upload a file:</label>
<%= form.file_field :uploaded_data %>
</p>
<p>
<%= submit_tag "Create" %>
</p>

new.rhtml

<% form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |form| %>
<%= render(:partial => '/assets/form', :object => form)%>
<% end %>

Add the form to
index.rhtml


<% remote_form_for(:asset, :url => assets_path, :html => { :multipart => true }) do |form| %>
<%= render(:partial => '/assets/form', :object => form) %>
<% end %>

Now that we have all our code in place, go back to the index page where you should be able to upload a new file using
AJAX.

Unfortunately there is one problem. A security restriction with javascript prevents access to the filesystem. If you used validations for your asset model you would have gotten an error complaining about missing attributes. This is because only the filename
is sent to the server, not the file itself. How can we solve this issue?

Step 6. Using iframes and responds_to_parent

To get around the AJAX/file upload problem we make use of the

iframe remoting pattern. We need a hidden iframe and target our form’s action to that iframe. First, we change the
index.rhtml
to use a
form_for
tag. To get rails to process our action like an
AJAX request we simply add a ”.js” extension to the form’s action. We then set the iframe to a 1×1 sized pixel so it doesn’t get shown. Don’t use
display:none
or your iframe will be hidden from your form and depending on your browser you will end up opening a new window, load the response in the main window, or download the server response.

<% form_for(:asset, :url =>formatted_assets_path(:format => 'js'), :html => { :multipart => true, :target => 'upload_frame'}) do |form| %>
<%= render(:partial => '/assets/form', :object => form) %>
<% end %>
<iframe id='upload_frame' name="upload_frame" style="width:1px;height:1px;border:0px" src="about:blank"></iframe>

To handle the form on the server, we can use Sean Treadway’s
responds_to_parent plugin.

script/plugin install http://responds-to-parent.googlecode.com/svn/trunk/

This plugin makes it dead simple to send javascript back to the parent window, not the iframe itself. Add the following to your
create
action:

def create
@asset = Asset.new(params[:asset])
respond_to do |format|
if @asset.save
flash[:notice] = 'Asset was successfully created.'
format.html { redirect_to asset_url(@asset) }
format.xml { head :created, :location => asset_url(@asset) }
format.js do
responds_to_parent do
render :update do |page|
page.insert_html :bottom, "assets", :partial => 'assets/list_item', :object => @asset page.visual_effect :highlight, "asset_#{@asset.id}"end
end
end
else
format.html { render :action => "new" }
format.xml { render :xml => @asset.errors.to_xml }
format.js do
responds_to_parent do
render :update do |page|
# update the page with an error message
end
end
end
end
end
end

At this point you no longer need the
create.rjs
file.

NOW you should be able to get your index page and upload a file the
AJAX way!

Step 7. Make it production ready

There are some more changes you need to make it production ready:

handling errors,
displaying error messages when uploading fails,
showing some feedback to the user while the file is uploading or being deleted

Step 8. Bonus: making a file download by clicking on a link

Just add the following action to your assets controller; don’t forget to add the route to your
routes.rb
file.

def download
@asset = Asset.find(params[:id])
send_file("#{RAILS_ROOT}/public"+@asset.public_filename,
:disposition => 'attachment',
:encoding => 'utf8',
:type => @asset.content_type,
:filename => URI.encode(@asset.filename))
end

Update: 2007/05/23 Thanks to Geoff Buesing for pointing out that we can use formatted_routes.

Update: 2007/05/26 Updated a bug in the initial index.html example (thanks Benedikt!) and added a download link to the final example (see the first paragraph).
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: