Ruby on Rails
HowtoUploadFiles

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>&nbsp;</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.

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

NOTE: A much better way to handle this is to use the Ruby File library. Use File.basename(your_file) instead of reinventing the wheel.

CGI class documentation

From the CGI class documentation

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.

How to upload files directly into the database

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.com/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>

Do

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”

How to download files from the database

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.

File save variation

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)

Using caching to reduce database load and bandwidth usage

See HowtoUseHTTPCaching

Checking that the user provided a file:

This can be done via picture.size == 0

or like this:
if picture.kind_of? StringIO or picture.kind_of? Tempfile

Uploading files to a SQLite database

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.

Testing a file upload

Use fixture_file_upload to simulate an uploaded file.

Other approaches

Mime-types

No mention of mime-types here. You could try the MIME::Types library for Ruby

Germany!

“Screencast on How to upload images”: http://www.rubyplus.org/episodes/31-How-to-upload-images-in-Rails-2-.html

category:Howto