Ruby on Rails Quickstart
Learn how to integrate Rabata.io object storage with your Ruby on Rails 7.2+ applications using ActiveStorage.
Introduction
This guide will show you how to use Rabata.io as a storage backend for your Ruby on Rails application using ActiveStorage. ActiveStorage facilitates uploading files to cloud storage services and attaching those files to Active Record objects.
Prerequisites
Before you begin, make sure you have:
- Ruby on Rails 7.2+ installed
- A Rabata.io account with access credentials
- A bucket created in your Rabata.io account
Installation
Create a New Rails Application
If you’re starting from scratch, create a new Rails application:
$ rails new my-app --database=postgresql
$ cd my-app
Add AWS SDK Gem
Add the AWS SDK for S3 to your Gemfile:
# Gemfile
gem 'aws-sdk-s3', require: false
Then install the gem:
$ bundle install
Install ActiveStorage
If you’re using a new Rails 7.2+ application, ActiveStorage is included by default. Otherwise, you can install it:
$ rails active_storage:install
$ rails db:migrate
Configuration
Configure ActiveStorage to Use Rabata.io
Edit your config/storage.yml file to include the Rabata.io configuration:
# config/storage.yml
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:rabata, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:rabata, :secret_access_key) %>
region: eu-west-1
bucket: <%= Rails.application.credentials.dig(:rabata, :bucket) %>
endpoint: https://s3.eu-west-1.rabata.io
force_path_style: true
Store Credentials Securely
Use Rails credentials to securely store your Rabata.io credentials:
$ EDITOR="code --wait" rails credentials:edit
Add your Rabata.io credentials to the opened file:
rabata:
access_key_id: YOUR_ACCESS_KEY
secret_access_key: YOUR_SECRET_KEY
bucket: your-bucket-name
Configure Environment
Set ActiveStorage to use Rabata.io in your environment files:
# config/environments/development.rb
config.active_storage.service = :amazon
# config/environments/production.rb
config.active_storage.service = :amazon
Basic Usage
Setting Up Models
Add ActiveStorage attachments to your models:
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
end
Handling File Uploads in Controllers
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to @user, notice: 'User was successfully updated.'
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:name, :email, :avatar, photos: [])
end
end
Creating File Upload Fields in Forms
<%# app/views/users/_form.html.erb %>
<%= form_with(model: user) do |form| %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="field">
<%= form.label :avatar %>
<%= form.file_field :avatar %>
</div>
<div class="field">
<%= form.label :photos, 'Upload multiple photos' %>
<%= form.file_field :photos, multiple: true %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
Displaying Attached Files
<%# app/views/users/show.html.erb %>
<h1><%= @user.name %></h1>
<% if @user.avatar.attached? %>
<%= image_tag @user.avatar %>
<% end %>
<h2>Photos</h2>
<div class="photos">
<% @user.photos.each do |photo| %>
<%= image_tag photo %>
<% end %>
</div>
Generating URLs for Attachments
# Generate a URL for an attachment
url_for(@user.avatar)
# Generate a URL for a variant (resized image)
url_for(@user.avatar.variant(resize_to_limit: [100, 100]))
Advanced Usage
Direct Uploads
Enable direct uploads to Rabata.io by including the ActiveStorage JavaScript in your application:
// app/javascript/application.js
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
And add the direct upload attribute to your file fields:
<%= form.file_field :avatar, direct_upload: true %>
Image Variants
Process images with variants:
# app/views/users/show.html.erb
<%= image_tag @user.avatar.variant(resize_to_limit: [100, 100]) %>
Make sure you have image processing gems installed:
# Gemfile
gem 'image_processing', '~> 1.2'
Purging Attachments
Remove attachments when no longer needed:
# Remove a single attachment
@user.avatar.purge
# Remove all attachments
@user.photos.purge
Testing
Model Tests
# test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "user can have an avatar" do
user = User.new
# Attach a file
user.avatar.attach(
io: File.open(Rails.root.join('test', 'fixtures', 'files', 'avatar.png')),
filename: 'avatar.png',
content_type: 'image/png'
)
assert user.avatar.attached?
end
end
System Tests
# test/system/users_test.rb
require "application_system_test_case"
class UsersTest < ApplicationSystemTestCase
test "uploading an avatar" do
visit new_user_path
fill_in "Name", with: "John Doe"
attach_file "Avatar", Rails.root.join('test', 'fixtures', 'files', 'avatar.png')
click_on "Create User"
assert_text "User was successfully created"
assert page.has_css?("img[src*='avatar.png']")
end
end
RSpec
If you’re using RSpec, you can test attachments like this:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it "can have an avatar attached" do
user = User.new
user.avatar.attach(
io: File.open(Rails.root.join('spec', 'fixtures', 'files', 'avatar.png')),
filename: 'avatar.png',
content_type: 'image/png'
)
expect(user.avatar).to be_attached
end
end
Troubleshooting
Common Issues
-
Access Denied Errors: Ensure your Rabata.io credentials are correct and the bucket exists.
-
CORS Issues with Direct Uploads: If you’re using direct uploads, you need to configure CORS for your Rabata.io bucket.
CORS Configuration for Direct Uploads
If you’re using direct uploads, you need to configure CORS for your Rabata.io bucket:
[
{
"AllowedHeaders": [
"Authorization",
"Content-Type",
"Content-Length",
"Content-MD5",
"x-amz-content-sha256",
"x-amz-date",
"x-amz-security-token",
"x-amz-user-agent"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"https://your-app-domain.com"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 3600
}
]
Production Considerations
Performance Optimization
-
Use Direct Uploads: For large files, direct uploads can significantly improve performance.
-
Image Processing: Consider using background jobs for image processing to avoid blocking requests.
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
end
Monitoring
Monitor your ActiveStorage usage to avoid unexpected costs:
# Get total storage used
total_bytes = ActiveStorage::Blob.sum(:byte_size)
puts "Total storage used: #{total_bytes / 1.megabyte} MB"
Security Best Practices
- Validate File Types: Always validate uploaded file types to prevent security issues.
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :avatar
validate :acceptable_avatar
private
def acceptable_avatar
return unless avatar.attached?
unless avatar.blob.content_type.in?(%w(image/jpeg image/png))
errors.add(:avatar, 'must be a JPEG or PNG')
end
unless avatar.blob.byte_size <= 1.megabyte
errors.add(:avatar, 'is too big (max is 1MB)')
end
end
end
- Use Signed URLs: For private files, use signed URLs with expiration.
# Generate a signed URL that expires in 30 minutes
@user.avatar.blob.service_url(expires_in: 30.minutes)
Complete Example
Here’s a complete example of a Rails application that uses Rabata.io for file storage:
# app/models/document.rb
class Document < ApplicationRecord
has_one_attached :file
validates :title, presence: true
validate :acceptable_file
private
def acceptable_file
return unless file.attached?
unless file.blob.byte_size <= 10.megabytes
errors.add(:file, 'is too big (max is 10MB)')
end
end
end
# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
def index
@documents = Document.all
end
def show
@document = Document.find(params[:id])
end
def new
@document = Document.new
end
def create
@document = Document.new(document_params)
if @document.save
redirect_to @document, notice: 'Document was successfully created.'
else
render :new
end
end
def destroy
@document = Document.find(params[:id])
@document.destroy
redirect_to documents_path, notice: 'Document was successfully destroyed.'
end
private
def document_params
params.require(:document).permit(:title, :file)
end
end
<%# app/views/documents/_form.html.erb %>
<%= form_with(model: document) do |form| %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :file %>
<%= form.file_field :file, direct_upload: true %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
<%# app/views/documents/show.html.erb %>
<h1><%= @document.title %></h1>
<% if @document.file.attached? %>
<div>
<% if @document.file.content_type.include?('image') %>
<%= image_tag @document.file %>
<% else %>
<p>
<%= link_to "Download #{@document.file.filename}", rails_blob_path(@document.file, disposition: "attachment") %>
</p>
<% end %>
</div>
<% end %>