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:

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

  1. Access Denied Errors: Ensure your Rabata.io credentials are correct and the bucket exists.

  2. 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

  1. Use Direct Uploads: For large files, direct uploads can significantly improve performance.

  2. 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

  1. 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
  1. 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 %>