Rails 5.2 comes with a new module - Active Storage, which manage file storage in the framework. It supports local storage service, AWS, Azure and GCP by default with the ability to add user defined storage service. In this example, we will use AWS S3.
(1) Create a new rails 5.2 API Project.
(2) Install Active Storage. This will copy "storage.yml" and some DB migration files to your project.
rails active_storage:install
(3) Add S3 gem for connection to AWS S3.
gem 'aws-sdk-s3', require: false
(4) Add mini_magick gem to handle image files.
gem 'mini_magick', '~> 4.8'
bundle install
(5) Install graphicmagick library which is required by mini_magick.
(Mac)
brew uninstall imagemagick graphicsmagick libpng jpeg brew cleanup -s brew install graphicsmagick
(Ubuntu)
sudo apt-get install python-software-properties sudo apt-get install software-properties-common sudo add-apt-repository ppa:rwky/graphicsmagick sudo apt-get update sudo apt-get install graphicsmagick
(6) Open "storage.yml" to configure AWS setting.
amazon: service: S3 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> region: ap-southeast-1 bucket: your-bucket-999
(7) The access key id and secret can be configured in Rails encrypted credential file.
EDITOR="pico" rails credentials:edit
(8) Set your AWS credentials as follow and save the file.
aws: access_key_id: 123 secret_access_key: 456
(9) In your environment file, set the type of storage service you want to use in each environment. E.g. development.rb
config.active_storage.service = :amazon
(10) Create a model with attachment.
class Product < ApplicationRecord has_one_attached :product_image end
(11) Run DB migration. This will migrate Active Storage migrations as well as your own migrations.
rails db:migrate
(12) There are several ways to add attachment:
# From a file or URL product.product_image.attach( io: File.open(Rails.root.join('path', 'to', 'product.png')), filename: 'product.png', content_type: 'image/png' )
# From incoming request product = Product.create(product_params) def product_params params.require(:product).permit(:product_image) end
(13) To check if a file is attached:
product.product_image.attached?
(14) To return a downloadable URL via API endpoint.
# In product.rb we add a method to return a signed URL of the attachment. class Product < ApplicationRecord has_one_attached :product_image def product_image_url Rails.application.routes.url_helpers.url_for(product_image) if product_image.attached? end end
# In environment (e.g. development.rb) we need to speify the default_url_options required by url_for routes.default_url_options = { host: 'localhost', port: 3000, protocol: 'http' }
# We can return the URL in products_controller.rb def index render json: Product.order(id: :asc).to_json(methods: %i[product_image_url]), status: :ok end
(15) The client will get an URL pointing to your app, e.g.:
http://localhost:3000/rails/active_storage/blob/{blob-id}/product.png
(16) When this URL is triggered, it generates and redirects client to a signed S3 public URL that expires in 5 minutes.
(17) The route "/rails/active_storage" is defined in the internal routes.rb of Active Storage. So if you have a catch all route in your application level routes.rb, it will not match the routes defined in the internal version. To overcome this, you need to exclude this specific route. E.g.
match '*path', to: 'application#render_no_route', via: :all, constraints: lambda { |req| req.path.exclude? 'rails/active_storage' # Exclude this }