One of the things I've been excited to get to in the Tealeaf Academy course is the section on uploading images and saving to Amazon S3. This post will walk through the steps to get this working in a Rails application.
The first step is to create a form in your application where a user can select a file to upload. In the MyFliX application I need an admin to be able to upload cover images for various videos. I won't layout the whole form below but I have included the snippet for selecting files below:
.form-group
.control-label.col-sm-3
= f.label :large_cover, "Large Cover"
.col-sm-6
= f.file_field :large_cover, class: "btn btn-file form-control"
.form-group
.control-label.col-sm-3
= f.label :small_cover, "Small Cover"
.col-sm-6
= f.file_field :small_cover, class: "btn btn-file form-control"
I don't yet have large_cover
and small_cover
columns in my database so I have to generate a migration (I am adding this to my videos table):
class AddLargeCoverAndSmallCoverToVideos < ActiveRecord::Migration
def change
add_column :videos, :large_cover, :string
add_column :videos, :small_cover, :string
remove_column :videos, :large_cover_url
remove_column :videos, :small_cover_url
end
end
I'm using the CarrierWave gem for file uploading in my application and the files will stored be in my Amazon S3 account. A useful RailsCast video can be found for CarrierWave at this link. Also checkout this post about adding permissions for S3 but I will get to that later in the post.
The next step is to create references to uploader classes in the Video model. The uploader class is used by CarrierWave to hold the logic for uploading and processing and means that this can be kept separate from my Video model class. To do this add the following in the video.rb
file:
mount_uploader :large_cover, LargeCoverUploader
mount_uploader :small_cover, SmallCoverUploader
Obviously I don't have these classes yet so that's the next step. Create files in the following locations, app/uploaders/large_cover_uploader.rb
and app/uploaders/small_cover_uploader.rb
and the basic template for one of them should look as follows:
class LargeCoverUploader < CarrierWave::Uploader::Base
end
One extra piece of functionality I would like to have in my application is to be able to manipulate my images to resize them to fit the layout of my application. If you read the documentation for CarrierWave you will see there are choices for using RMagick or MiniMagick. I have opted to use MiniMagick so firstly I have to include gem 'mini_magick'
and then bundle install
.
It's very easy for me to use this and for example my LargeCoverUploader class will now look as follows:
class LargeCoverUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
process resize_to_fill: [665, 375]
end
I am almost ready to use the form on my application to start uploading images. For file storage I am using Amazon S3. It's a little tricky to set up S3 for uploading files so I suggest reading the post I linked to at the start of the post for adding permissions to S3.
There is helpful documentation in CarrierWave about using Amazon S3 and the first thing they mention is to add gem "fog-aws"
because Fog AWS is used to support S3. I had problems just using this gem and actually also had to add gem 'fog'
to properly read my credentials.
The credentials are stored at config/initializers/carrierwave.rb
and my file looked as follows:
CarrierWave.configure do |config|
config.storage = :fog
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
region: ENV['AWS_REGION']
}
if Rails.env.production?
config.fog_directory = ENV['AWS_S3_BUCKET_PRODUCTION']
elsif Rails.env.staging?
config.fog_directory = ENV['AWS_S3_BUCKET_STAGING']
else
config.fog_directory = ENV['AWS_S3_BUCKET_DEVELOPMENT']
end
end
You will notice that I have setup 3 separate buckets so I can test the functionality with S3 in development and staging as well as having a production bucket. Now, if you don't want to require your development setup to have an internet connection and have files saved to local file storage instead then this is an alternative setup:
CarrierWave.configure do |config|
if Rails.env.staging? || Rails.env.production?
config.storage = :fog
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
region: ENV['AWS_REGION']
}
if Rails.env.production?
config.fog_directory = ENV['AWS_S3_BUCKET_PRODUCTION']
else
config.fog_directory = ENV['AWS_S3_BUCKET_STAGING']
end
else
config.storage = :file
config.enable_processing = Rails.env.development?
end
end
This is what I will do from now on but having a bucket for testing was useful for me when I was learning what to do and made things easier to debug. Obviously change your environment variables to match your credentials and you should now be all set.
This is obviously a fairly basic setup for uploading images but it's a good base to work from to add more complexity to the application. Happy uploading!
As I finished this post I thought I'd write about one other little issue I had when I was writing tests. Here's a quick overview of what I needed to test. When a user clicks on a video, they are taken to a page that displays a large cover image of the video if one is available. However, if there isn't a cover image then I display a dummy image. I have a method in my video.rb
model that tests for the presence of a large cover:
# If large_cover is present then return image url, otherwise disply dummy image
def display_large_video_image
self.large_cover.present? ? self.large_cover_url : "http://dummyimage.com/665x375/000/fff.png&text=No+Preview+Available"
end
self.large_cover_url
then takes advantage of the _url
helper which provides an absolute path for large_cover
, including protocol and server name.
That all works well in my application but I was having trouble getting my test for this method working properly. I won't write out all of the tests but this is the one I was using to test if the method would display the large cover image:
describe :display_large_video_image do
it "should display a large video image if one is available" do
video2 = object_generator(:video, large_cover: './spec/support/uploads/monk_large.jpg')
expect(video2.display_large_video_image).to eq("https://knoxjeffrey-myflix-development.s3.amazonaws.com/uploads/monk_large.jpg")
end
end
I'm using Fabricator to generate my objects and I had been using this to generate my video objects until now:
Fabricator(:video) do
title { Faker::Lorem.characters(5) }
description { Faker::Lorem.sentence }
end
As you can see from the test I was then trying to add an image from a location on file but I was getting the following error:
CarrierWave::FormNotMultipart:
You tried to assign a String or a Pathname to an uploader, for security reasons, this is not allowed.
If this is a file upload, please check that your upload form is multipart encoded.
To solve this I added to the video fabricator:
Fabricator(:video) do
title { Faker::Lorem.characters(5) }
description { Faker::Lorem.sentence }
end
Fabricator(:video_upload, from: :video) do
large_cover { File.open("./spec/support/uploads/monk_large.jpg") }
end
Now my test was as follows:
it "should display a large video image if one is available" do
video2 = object_generator(:video_upload)
expect(video2.display_large_video_image).to eq("https://knoxjeffrey-myflix-development.s3.amazonaws.com/uploads/monk_large.jpg")
end
Now the test is working perfectly, although just make sure to put your test image in the file location specified.