Skip to content

Ruby on Rails - File Uploading

Ruby on Rails - Modern File Uploading with Active Storage

Section titled “Ruby on Rails - Modern File Uploading with Active Storage”

Many web applications require users to upload files (e.g., profile pictures, documents). Rails provides a built-in solution called Active Storage for handling file uploads and attachments to Active Record models. Active Storage supports storing files on local disk for development/testing and various cloud storage services (like Amazon S3, Google Cloud Storage, Microsoft Azure Storage) for production.

Let’s build a simple Rails application to upload resumes.

$ rails new resume_uploader_app
$ cd resume_uploader_app

Active Storage requires database tables to store metadata about the uploaded files. Run the installer:

$ bin/rails active_storage:install

This command generates a migration file. Now, run the migration:

$ bin/rails db:migrate

This creates active_storage_blobs and active_storage_attachments tables.

Open config/storage.yml. By default, it’s configured to use local disk storage for development and testing:

test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>

In config/environments/development.rb (and production.rb), specify which service to use:

# In config/environments/development.rb
Rails.application.configure do
# ... other configurations ...
config.active_storage.service = :local
end

For production, you would configure a cloud service like Amazon S3 here.

Let’s generate a Resume model and a ResumesController:

$ bin/rails generate model Resume name:string
$ bin/rails db:migrate
$ bin/rails generate controller Resumes index new create destroy --no-helper --no-assets --no-test-framework

Modify the Resume model (app/models/resume.rb) to declare an attachment:

class Resume < ApplicationRecord
has_one_attached :attachment
validates :name, presence: true
validates :attachment, presence: true # Ensure a file is uploaded
# For more specific validations (content type, size):
# validates :attachment, content_type: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
# size: { less_than: 5.megabytes , message: 'file is too large (max 5MB)' }
end

has_one_attached :attachment tells Active Storage that a Resume can have one file attached, referred to as attachment.

Update config/routes.rb:

Rails.application.routes.draw do
resources :resumes, only: [:index, :new, :create, :destroy]
root "resumes#index"
end

Edit app/controllers/resumes_controller.rb:

class ResumesController < ApplicationController
def index
@resumes = Resume.all
end
def new
@resume = Resume.new
end
def create
@resume = Resume.new(resume_params)
if @resume.save
redirect_to resumes_path, notice: "The resume '#{@resume.name}' has been uploaded."
else
render :new, status: :unprocessable_entity
end
end
def destroy
@resume = Resume.find(params[:id])
@resume.destroy
redirect_to resumes_path, notice: "The resume '#{@resume.name}' has been deleted.", status: :see_other
end
private
def resume_params
params.require(:resume).permit(:name, :attachment)
end
end

Note that :attachment is permitted in resume_params.

Create app/views/resumes/index.html.erb:

<h1>Resumes</h1>
<% if flash[:notice].present? %>
<p style="color: green;"><%= flash[:notice] %></p>
<% end %>
<%= link_to "Upload New Resume", new_resume_path %>
<hr>
<% if @resumes.any? %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Attachment</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @resumes.each do |resume| %>
<tr>
<td><%= resume.name %></td>
<td>
<% if resume.attachment.attached? %>
<%= link_to resume.attachment.filename, url_for(resume.attachment), target: "_blank" %>
(<%= number_to_human_size(resume.attachment.blob.byte_size) %>)
<% else %>
No attachment
<% end %>
</td>
<td><%= button_to "Delete", resume_path(resume), method: :delete, data: { turbo_confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No resumes uploaded yet.</p>
<% end %>

Create app/views/resumes/new.html.erb:

<h1>Upload New Resume</h1>
<%= form_with(model: @resume, local: true) do |form| %>
<% if @resume.errors.any? %>
<div style="color: red;">
<h2><%= pluralize(@resume.errors.count, "error") %> prohibited this resume from being saved:</h2>
<ul>
<% @resume.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :name %><br>
<%= form.text_field :name %>
</div>
<div>
<%= form.label :attachment %><br>
<%= form.file_field :attachment %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
<%= link_to "Back to resumes", resumes_path %>

The key part for file uploads is form.file_field :attachment. form_with(model: @resume, local: true) is used; for Active Storage with direct uploads (to cloud), local: true might be omitted depending on setup.

  1. Start the server: $ bin/rails server
  2. Go to http://localhost:3000.
  3. Click “Upload New Resume”. The page will show a form with fields for ‘Name’ and ‘Attachment’ (a file input).
  4. Enter a name, choose a file, and click “Create Resume” (or whatever the submit button is named).
  5. If successful, you’ll be redirected to the index page, and your uploaded resume will be listed. The filename will be a link to view/download the file. You’ll also see its size.
  6. If there are validation errors (e.g., no name provided, or file type/size restrictions if you added them in the model), the ‘new’ page will re-render with error messages. For example, if you left the name blank, you’d see “Name can’t be blank”. If you uploaded a very large file (if size validation is active), you might see “Attachment file is too large”.

Active Storage provides a robust and integrated way to handle file uploads in Rails, simplifying a common but complex task.