This page is a complete mess! However, it is updated with a working example, compatible with Rails 2.0.2 below.
See also the Rails cookbook: Sending and receiving files
Example of file saving model (based on Rails Cookbook).
This works with Rails 2.0.2, and requires no special modification to controller code. Just make sure your form in the view has :html => { :multipart => true} and that your file field matches the name of the setter in the model code (“cover” in the below example). Also, it uses the ALBUM_COVER_STORAGE_PATH constant, I recommend setting this in a ruby file in the initializers dir.
It is pretty basic, but shows how to write uploaded files to disk using only the model class. This example uses the model id to create a folder for the uploads, and also names the file with the id and the original extension. The reason for this is to make it simple to save several versions of one upload togheter, like sizes, formats etc.
The model
require "ftools"
class Album < ActiveRecord::Base
belongs_to :profile
validates_presence_of :artist, :title
# run write_file after save to db
after_save :write_file
# run delete_file method after removal from db
after_destroy :delete_file
# setter for form file field "cover"
# grabs the data and sets it to an instance variable.
# we need this so the model is in db before file save,
# so we can use the model id as filename.
def cover=(file_data)
@file_data = file_data
end
# write the @file_data data content to disk,
# using the ALBUM_COVER_STORAGE_PATH constant.
# saves the file with the filename of the model id
# together with the file original extension
def write_file
if @file_data
File.makedirs("#{ALBUM_COVER_STORAGE_PATH}/#{id}")
File.open("#{ALBUM_COVER_STORAGE_PATH}/#{id}/#{id}.#{extension}", "w") { |file| file.write(@file_data.read) }
# put calls to other logic here - resizing, conversion etc.
end
end
# deletes the file(s) by removing the whole dir
def delete_file
FileUtils.rm_rf("#{ALBUM_COVER_STORAGE_PATH}/#{id}")
end
# just gets the extension of uploaded file
def extension
@file_data.original_filename.split(".").last
end
end
The controller (just a snippet showing the new and create actions)
As you can se, nothing unusual here, except maybe the @current_profile.albums.new(). Change it to Album.new() if needed.
def new
@album = Album.new
end
def create
@album = @current_profile.albums.new(params[:album])
if @album.save
flash[:notice] = "Album created"
redirect_to profile_album_path(@current_profile, @album)
else
render :action => "new"
end
end
The view (new.html.erb)
Note the use of a partial to render the form fields, very nice to do, as the edit view can use the same partial, and you save typing. :)
<h1>Create new album</h1>
<%= error_messages_for :album %>
<% form_for [@current_profile, @album], :html => { :multipart => true } do |f| %>
<fieldset>
<ul>
<%= render :partial => "fields", :locals => { :f => f } %>
<li> </li>
<li><%= f.submit "Create" %></li>
</ul>
<% end %>
The partial used in the view (_fields.html.erb)
<li>
<label>Artist name</label>
<%= f.text_field :artist, :value => @current_profile.screen_name, :onclick => ("this.select();" if action?("new")) %>
</li>
<li>
<label>Album title</label>
<%= f.text_field :title %>
</li>
<li>
<label>Release year</label>
<%= f.text_field :year, :value => Time.now.year, :onclick => ("this.select();" if action?("new")) %>
</li>
<li>
<label>Album cover image</label>
<%= f.file_field :cover %>
</li>
<li>
<label>Editorial</label>
<%= f.text_area :editorial %>
</li>
The storage path constant
This is set in the RAILS_ROOT/config/initializers/globals.rb. Just create the file and name it to something nice, and enter the line in there. This file will load when the server starts/restarts. So restart your server after the update.
# sets the upload root, relative to the RAILS_ROOT
ALBUM_COVER_STORAGE_PATH = "#{RAILS_ROOT}/../storage/album_covers"
End of example, the following view and controller code does not belong to the above example
The form part of the “new” template:
<form action="create" method="post" enctype="multipart/form-data">
<p>
<b>Name:</b><br />
<%= text_field "person", "name" %>
</p>
<p>
<b>Picture:</b><br />
<input type="file" name="person[picture]" />
</p>
<p><input type="submit" name="Save" /></p>
</form>
or:
<%= form_tag ({:action => "create"}, {:multipart => true}) %>
<label for="name">Name:</label>
<%= text_field "person", "name" %>
<label for="picture">Picture:</label>
<%= file_field_tag "picture" %>
<%= submit_tag "Save" %>
<%= end_form_tag %>
The controller:
class AddressbookController < ApplicationController
def new
# not really needed since the template doesn't rely on any data
end
def create
post = Post.save(@params["person"])
# Doesn't this mean post is a File object?
# post.id is a bad idea in this case...
redirect_to :action => "show", :id => post.id
end
end
The model:
class Post < ActiveRecord::Base
def self.save(person)
f = File.new("pictures/#{person['name']}/picture.jpg", "wb")
f.write params[:picture].read
f.close
end
end
To get the original name of the uploaded file, use person['picture'].original_filename.
There is also a person['picture'].content_type method. (These are buried somewhere in the depths of cgi.rb) Note: the string returned content_type() seems to have extra whitespace, so if you are needing to parse or compare it, use strip() on it first.
PAY ATTENTION Windows Users: to avoid corrupting binary files, you must call File.open in binary mode. Change the “w” flag to “wb”, like this:
File.open("pictures/#{person['name']}/picture.jpg", "wb") { |f| f.write(person['picture'].read) }
truncated files?
If you have trouble with zero-length files written by this code, try calling
person['picture'].rewind before writing the file.
Files uploaded from Internet Explorer
Internet Explorer prepends the original path of a file to the filename sent, so the original_filename routine will return something like C:\Documents and Files\user_name\Pictures\My File.jpg instead of just My File.jpg. To deal with this, make sure you write a sanitize method, perhaps called via :before_save, to remove the path and any illegal characters from the filename. Note, File.basename will not work correctly on Windows paths.
An example sanitize method (this is not perfect!):
private
def sanitize_filename(value)
# get only the filename, not the whole path
just_filename = value.gsub(/^.*(\\|\/)/, '')
# NOTE: File.basename doesn't work right with Windows paths on Unix
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
# Finally, replace all non alphanumeric, underscore or periods with underscore
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
end
Multipart requests
If a request’s method is POST and its content type is multipart/form-data, then it may contain uploaded files. These are stored by the \QueryExtension module in the parameters of the request. The parameter name is the name attribute of the file input field, as usual. However, the value is not a string, but an IO object, either an IOString for small files, or a Tempfile for larger ones. This object also has the additional singleton methods:
- local_path():
- the path of the uploaded file on the local filesystem
- original_filename():
- the name of the file on the client computer
- content_type():
- the content type of the file
Note There is no such thing as an IOString (they meant to say StringIO), and the object returned does not have a local_path method most of the time. The only methods that do work all of the time are: original_filename, content_type, length, and read.
The view is almost the same as above, but I changed the field’s name to tmp_file since it’s not going to be stored anywhere permanently.
The model for me is just the plain model without any specific methods at the moment. Oh, in the DB you should have a string field (i.e. ‘filename’), and a field ‘picture’ which should be a binary field (for example BLOB in \MySQL).
The controller:
def create
@params['person']['filename'] = @params['person']['tmp_file'].original_filename.gsub(/[^a-zA-Z0-9.]/, '_') # This makes sure filenames are sane
@params['person']['picture'] = @params['person']['tmp_file'].read
@params['person'].delete('tmp_file') # let's remove the field from the hash, because there's no such field in the DB anyway.
@person = Person.new(@params['person'])
# then the basic if @person.save ... like in <a href="http://wiki.rubyonrails.org/rails/pages/TutorialFramingOut" class="existingWikiWord">TutorialFramingOut</a>
In the above, the file contents gets inserted into the hash as a string – ie. it’s read to memory. Is that the best we can do? Can we not pass a file-reference or similar and via that get the file contents streamed from the filesystem to the DB?
Note to Postgresql users If you want to use large objects, you can’t just do obj.connection.lo_import() or anything. You have to edit your activerecord library a little… for me, this meant editing this file
/usr/lib/ruby/gems/1.8/gems/activerecord-1.11.1/lib/active_record/connection_adapters/postgresql_adapter.rb
and adding lines like this
def lo_import(file)
@connection.lo_import(file)
end
under the PostgreSQLAdapter? class… then you can use those methods on the connection.
If when trying to upload you get the following
undefined method `lo_import' for #<PGconn:0xb73dd35c>
gem install postgres
note: this is for outdated ActiveRecord versions only, the newer variants support:
self.connection().execute("BEGIN")
oid = self.connection().raw_connection.lo_import(file)
self.picture = oid.oid()
self.connection().execute("COMMIT")
self.save
Its important to put the lo_import into a transaction. In addition to lo_import and lo_export the postgresql(incl. ruby) interface offer things like lo_read, lo_write for direct block access.
Second Note for Postgresql users : Here is a “solution” that work for me, by using “execute” and SQL statement with lo_import and lo_export. You just have to modify the model describe previously, and create a callback “after_save” in your model. NB, my modelName is not Picture, but Contact, and the field in the postgres db is picture_name, picture_type, and picture_data as an oid. Laurent Buffat @ AltraBio.com .
def picture=(picture_field)
self.picture_name = base_part_of(picture_field.original_filename)
self.picture_type = picture_field.content_type.chomp
# self.picture_data = picture_field.read doesn't work for postgres
@temp_file = picture_field.local_path()
# for some reason that I was not able to understand, local_path sometime, return a empty string
# maybe, when the path for the orignal_filename is not full accecible
# ( the explaination it's not clear, but it's not clear for me what append exactly )
# So to "correct" the bad behaviorh of "local_path", I use read to make the local_copy
@temp_file = "/tmp/local_upload_#{self.picture_name}"
f=File.open(@temp_file,"w")
f.write(picture_field.read)
f.close
@filename = self.picture_name
@contact_id = self.id
end
def picture
@temp_file="/tmp/#{self.picture_name}"
self.connection().execute "SELECT lo_export(picture_data,'#{@temp_file}') FROM contacts WHERE ID=#{self.id};"
f = File.open(@temp_file, "rb")
return f.read
end
def after_save
if @temp_file
FileUtils.chmod 0444, @temp_file
self.connection().execute "UPDATE contacts SET picture_data = lo_import('#{@temp_file}') WHERE ID=#{@contact_id};"
end
end
End of “Second Note”
Here’s a piece of code that’ll help you download the picture from the DB directly (modified from HowtoSendFiles rev=1)
@entry = Person.find(@params['id'])
@response.headers['Pragma'] = ' '
@response.headers['Cache-Control'] = ' '
@response.headers['Content-type'] = 'application/octet-stream'
@response.headers['Content-Disposition'] = "attachment; filename=#{@person.filename}"
@response.headers['Accept-Ranges'] = 'bytes'
@response.headers['Content-Length'] = @person.picture.length
@response.headers['Content-Transfer-Encoding'] = 'binary'
@response.headers['Content-Description'] = 'File Transfer'
render_text @person.picture
Alternately, you can do the much simpler:
@person = Person.find(@params['id'])
send_data @person.picture, :filename => @person.filename, :type => "image/jpeg"
If you want to display the image inline (ie, in a page), use this send_data instead:
send_data @person.picture, :filename => @person.filename, :type => "image/jpeg", :disposition => "inline"
Here’s the api documentation for send_file
and send_data
/>[But I use lighttpd 1.4.11 and the only problem I ever had was bad permissions on the temp directory. If set wrong, large file uploads wont work. Lighttpd works just fine.
The uploaded file will be a TempFile -like object (if over 10kb in size). These can be copied by the filesystem instead of being read and processed through Ruby.
def picture=(picture)
FileUtils.copy picture.local_path, "pictures/#{name}/picture.jpg"
end
In rare cases the file returned can be a String (instead of StringIO or TempFile). (Try it by uploading a thumbs.db file through Safari)
This can be done via picture.size == 0
or like this:
if picture.kind_of? StringIO or picture.kind_of? Tempfile
You can’t store things with null bytes into BLOBs yet. Until the new adapter is out, use Base64 or some similar binary->ascii coding system to make it not contain null bytes.
Use fixture_file_upload to simulate an uploaded file.
No mention of mime-types here. You could try the MIME::Types library for Ruby
Screencast on How to upload images
http://www.rubyplus.org/episodes/31-How-to-upload-images-in-Rails-2-.html
category:Howto