Ruby
Article

Easily Allow File Uploads with Rails and Refile

By Kingsley Silas

Enabling image uploading as a feature in your web application cannot be underestimated. Especially when it is an application that has to do with users having a profile. There are lots of gems available out there to help you handle this in a few lines of code, and most of them have been covered here on SitePoint Ruby.

They include:

In this tutorial, we will be learning how to enable image uploading in Rails using Refile. As written on Refile’s github page, it is a modern file upload library for Ruby applications. It is simple, yet powerful.

For the curious ones reading this you might want to know what makes Refile better….did I just say it is? Anyway, that is left for you to decide. :)

Refile features includes:

  • Configurable backends, file system, S3 etc.
  • Convenient integration with ORMs.
  • On fly manipulation of images and other files.
  • Streaming IO for fast and memory friendly uploads.
  • Works across form redisplays, i.e, when validations fail, even on S3.
  • Effortless direct uploads, even to S3.
  • Support for multiple uploads.

So what do you think? Do you want to give it a shot?

if "answer" = "yes"
  hop_on_board_with_me
else
  still_tag_along
end

Let’s roll!

Rails Application Setup

We are going to craft an app to basically save the images of our family members and friends. We will be the only user of this app, so it will be as simple as possible.

Create a new Rails app called photobook:

rails new photobook

Let us generate a scaffolding for our application:

rails g scaffold Photo name:string

Next we apply our migration:

rake db:migrate

Integrating Refile

Open your Gemfile and add the gem:

#Gemfile

gem 'refile', require: 'refile/rails'
gem 'refile-mini_magick'

Install the gems you just added:

bundle install

The refile-mini_magick is for image processing. You need to have ImageMagick installed on your system.

Depending on your system, you can install ImageMagick following this:

brew install imagemagick # OS X

sudo apt-get install imagemagick # Ubuntu

After doing that, add the attachment method to use Refile in your model:

#app/models/photo.rb

class Photo < ActiveRecord::Base
  attachment :profile_image
end

Let’s generate a new migration to add a new column for profile_image to our photos table:

rails generate migration add_profile_image_to_photos profile_image_id:string
rake db:migrate

We need to set up strong parameters to allow profile_image in our photos_controller:

#app/controllers/photos_controller.rb

def photo_params
  params.require(:photo).permit(:name, :profile_image)
end

Navigate to your views to add the attachment field in your form:

#app/views/_form.html.erb

<%= form_for(@photo) do |f| %>
  <% if @photo.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@photo.errors.count, "error") %> prohibited this photo from being saved:</h2>

      <ul>
      <% @photo.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.attachment_field :profile_image %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

To show the file in your views, edit your index and show page to look like this:

#app/views/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Listing Photos</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @photos.each do |photo| %>
      <tr>
        <td><%= photo.name %></td>
        <td><%= image_tag(attachment_url(photo, :profile_image, :fill, 100, 100)) %></td>
        <td><%= link_to 'Show', photo %></td>
        <td><%= link_to 'Edit', edit_photo_path(photo) %></td>
        <td><%= link_to 'Destroy', photo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Photo', new_photo_path %>


#app/views/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @photo.name %>
  <%= image_tag attachment_url(@photo, :profile_image, :fill, 100, 100, format: "jpg") %>
</p>

<%= link_to 'Edit', edit_photo_path(@photo) %> |
<%= link_to 'Back', photos_path %>

Removing Attachments

You may want to remove an old image and add a new one. Refile has a functionality to help in this scenario. It adds an attribute to your model when you use the attachment method, which is designed to be used with a checkbox in a form.

Here is how to do it:

#app/views/photo/_form.html.erb

<%= form_for(@photo) do |f| %>
  <% if @photo.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@photo.errors.count, "error") %> prohibited this photo from being saved:</h2>

      <ul>
      <% @photo.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.label :profile_image %>
    <%= f.attachment_field :profile_image %>

    <% if @photo.profile_image_id? %>
      <%= image_tag attachment_url(@photo, :profile_image, :fill, 100, 100, format: "jpg") %>

      <%= f.checkbox :remove_profile_image %>
      <%= f.label :remove_profile_image %>
    <% end %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

In the above code we use a checkbox to select the option of removing the image. We do not want the checkbox showing when no image has been uploaded so we use an if statement to check for there is a profile_image_id. If there is, the profile image and the option to remove it are displayed. This gives our application a professional look. :)

For the remove option to work, we need to whilelist :remove_profile_image in strong params. So navigate to your photos_controller and add :remove_profile_image.

Here is how your controller should look:

#app/controllers/photos_controller.rb

class PhotosController < ApplicationController
  before_action :set_photo, only: [:show, :edit, :update, :destroy]

  # GET /photos
  # GET /photos.json
  def index
    @photos = Photo.all
  end

  # GET /photos/1
  # GET /photos/1.json
  def show
  end

  # GET /photos/new
  def new
    @photo = Photo.new
  end

  # GET /photos/1/edit
  def edit
  end

  # POST /photos
  # POST /photos.json
  def create
    @photo = Photo.new(photo_params)

    respond_to do |format|
      if @photo.save
        format.html { redirect_to @photo, notice: 'Photo was successfully created.' }
        format.json { render :show, status: :created, location: @photo }
      else
        format.html { render :new }
        format.json { render json: @photo.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /photos/1
  # PATCH/PUT /photos/1.json
  def update
    respond_to do |format|
      if @photo.update(photo_params)
        format.html { redirect_to @photo, notice: 'Photo was successfully updated.' }
        format.json { render :show, status: :ok, location: @photo }
      else
        format.html { render :edit }
        format.json { render json: @photo.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /photos/1
  # DELETE /photos/1.json
  def destroy
    @photo.destroy
    respond_to do |format|
      format.html { redirect_to photos_url, notice: 'Photo was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_photo
      @photo = Photo.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def photo_params
      params.require(:photo).permit(:name, :profile_image, :remove_profile_image)
    end
end

File Type Validations

With Refile you can restrict the file extensions and content type users are allowed to upload. This is more of a convenient feature for your users, and not a security feature. With this feature you can warn users if they try to upload an invalid file.

To enable this, open your model and add edit the attachment method line:

#app/models/photo.rb

attachment :profile_image, content_type: "image/jpeg"

That will restrict the allowed files to images with a JPEG extension. If you want to use a combination of different extension, it looks like this:

#app/models/photo.rb

attachment :profile_image, content_type: ["image/jpeg", "image/png", "image/gif"]

Here is a better way of doing this, especially when it has to do with images:

#app/models/photo.rb

attachment :profile_image, type: image

That will work!

Conclusion

Now you know how to enable image uploading in your Rails application using Refile. You might want to give this a shot in your next project. Check out Refile’s documentation to see how you can add other exciting capabilities Refile has to offer. Have a great time learning!

Meet the author
Kingsley Silas is a web developer from Nigeria. He has a hunger for acquiring new knowledge in every aspect he finds interesting.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.