Adding Uploadcare to Rails the Easy Way
Give your users the ability to create, read, and delete images
Coding by Matthew (WMF) is licensed under CC BY-SA 3.0
Uploadcare is a really handy service that provides a combination of a CDN and a convenient, configurable JavaScript upload dialog that lets your app’s users easily put images onto your CDN. Each Uploadcare image has a unique UUID, which means that if your Rails app is also using UUIDs as database identifiers, then Uploadcare will fit right in.
This post will show you how to easily integrate Uploadcare into an existing Rails app, in a way that gives your users the ability to create, read, and delete the images that they’ve uploaded to your account on the CDN. In this app, we’ll look specifically at the case of a user with a profile photo, but you can easily generalize this to any type of model with a user-uploaded image, or even an array of user-uploaded images.
Here’s the user table we’ll be working with:
create_table :user, id: :uuid do |t|
t.string :name
t.uuid :profile_photo_id
end
This post assumes that you’re already familiar with how to set up a Rails app. We’ll be using Rails 4.2.5 in this example, but this post should work for earlier versions, as well. We’re also using the postgres UUID extension, so you’ll need that enabled before beginning.
Just use REST, and Bring Your Own Magic
We’re going to skip the uploadcare-rails
gem, because it hasn’t been updated in a long time and I wasn’t really able to do anything useful with it (or with the similarly stale uploadcare-ruby
gem). Fortunately, it’s really easy for us to do what we need to do without either of the Uploadcare gems.
Indeed, I often find that when I’m using an external service with a well-documented REST API, I can write less code with better performance and fewer headaches by skipping the official gem entirely. This is especially true in cases where the gem implements a half-baked ORM that attempts to map some subset of ActiveRecord’s functionality onto the REST API. By the time you’ve learned how that mapping works and which parts of ActiveRecord the gem has kind-of-sort-of implemented, you usually know enough about the REST API to put together your own half-baked ActiveRecord-like implementation that contains only the parts you need and works exactly like you want it to. So that’s what we’ll do in this case.
Once Uploadcare is set up in the app, we’ll create what is basically a fake ActiveRecord model using the virtus
gem. Using this model, which contains all of the Uploadcare-related code and functionality that we need (and nothing that we don’t need), we can do the kinds of things that we tend to do with rails models in our controllers and views.
Setting up Uploadcare
To get started, create a free Uploadcare account and project, and then take note of location of the public and private keys for that project because we’ll need them in order to configure the app.
First, we’ll add some code to our app/views/layouts/application.html.erb
file that will put the correct Uploadcare code in the <head>
tags of our app:
UPLOADCARE_LOCALE = "en";
UPLOADCARE_TABS = "file url facebook gdrive box skydrive instagram evernote";
UPLOADCARE_AUTOSTORE = true;
UPLOADCARE_PUBLIC_KEY = "<%= ENV['UPLOADCARE_PUBLIC_KEY'] %>";
UPLOADCARE_CDN_BASE = "https://ucarecdn.com/";
UPLOADCARE_PATH_VALUE = false;
<%= javascript_include_tag "https://ucarecdn.com/widget/2.5.5/uploadcare/uploadcare-2.5.5.min.js" %>
You can set those ENV variables in your app’s environment, but we strongly recommend using the figaro
gem and adding them to config/application.yml
.
As for the bit of ERB below the script
tags, this is where we include the minified version of the latest Uploadcare widget. Or, if you like you can place this in your vendor
directory and bundle it with your app.
At this point, you should be able to create a Rails form
object for a model that stores an Uploadcare UUID using the standard widget, and it should work. But we want to take it further than just adding an Uploadcare image UUID to a single model.
Creating an Image model
The virtus
gem is super handy for dealing with REST APIs, and Uploadcare’s API is no exception. First, I’ll show you the code for my Image
model, and then we’ll walk through what it does:
class Image
include Virtus.model
include ActiveModel::Model
include HTTParty
base_uri "api.uploadcare.com"
debug_output $stdout unless Rails.env.production?
class Info
include Virtus.model
attribute :height, Integer
attribute :width, Integer
attribute :geo_location
attribute :datetime_original
attribute :format, String
end
attribute :uuid, UUID
attribute :mime_type, String
attribute :is_ready, Boolean
attribute :original_filename, String
attribute :original_file_url, String
attribute :datetime_uploaded, String
attribute :size, Integer
attribute :datetime_stored, String
attribute :source, Hash
attribute :image_info, Image::Info
def ==(img)
uuid = img.uuid
end
alias_method :eql?, :==
def url
return unless uuid
"https://ucarecdn.com/#{uuid}/"
end
def formatted_url(url_operations_str=nil)
return unless url.present?
"#{url}#{url_operations_str}"
end
def destroy
self.class.delete("/files/#{uuid}/", headers: self.class.headers)
end
def self.find(uuid)
return unless uuid.present?
response = self.get("/files/#{uuid}/", headers: headers)
Image.new(
JSON.parse(response.body).
with_indifferent_access.
slice(*Image.attributes.map(&:name))
)
end
def self.uuid_from_url(url)
return unless url.present?
url.split("/")[3]
end
def self.formatted_url_from_uuid(uuid, url_operations_str=nil)
self.new(uuid: uuid).formatted_url(url_operations_str)
end
def self.headers
{
"Accept" => "application/vnd.uploadcare-v0.4+json",
"Authorization" => "Uploadcare.Simple #{ENV['UPLOADCARE_PUBLIC_KEY']}:#{ENV['UPLOADCARE_PRIVATE_KEY']}",
"Date" => "#{Time.current.rfc2822}"
}
end
end
Let’s go through the above code from top to bottom.
First, I include ActiveModel::Model
so that I get model_name
and the other methods that my controllers and views look for by default.
Next, you’ll notice that I’m using the httparty
gem. I do this because in the specific app that I built this model for I have other gems that depend on httparty
, so I went ahead and used it here instead of introducing a new dependency. Feel free to substitute your gem of choice.
In the next two lines, I set HTTParty’s base_uri
to the domain of the Uploadcare REST API, and I configure the model to send debug output to stdout
so that I can watch what it’s doing in dev and testing.
Let’s skip the inner class
declaration for the moment and look at the Image
model’s attribute declarations. The attributes interface is one of my favorite aspects of virtus, because it handles coercion and therefore makes deserializing JSON objects a breeze.
I can get
a bit of JSON from the Uploadcare REST API, convert it to a hash with JSON.parse
, and then use that hash to initialize an Image
object with a call to Image.new
; thanks to the built-in type coercion, the attributes will each be the proper type.
Now let’s take a quick look at that inner class, Image::Info
. Uploadcare’s JSON response has an image_info
field that contains some sub-fields, and with the Image::Info
class and Virtus’s automagic coercion I can easily turn that into an object.
Moving down the file, I went ahead and defined ==
for the models so that I can check for equality in my specs.
I also go ahead and derive the url
every time from the UUID, instead of using what Uploadcare gives me back via the REST API. I do this because I often want to call Image#url
on persisted image objects where I have the UUID stored in my database but I don’t need to make an API call to fill out the entire object. Two examples will clarify what I mean.
First, consider the case of the User#profile_photo
method, which is basically a fake ActiveRecord belongs_to :profile_photo
association as I’ve set it up below:
def profile_photo
Image.find(uuid: profile_photo_id) }
end
The above code will make a REST API call for every profile_photo
method call, which in some cases may be exactly what we want. But for cases where we don’t need that, one option would be to memoize it with an instance variable:
def profile_photo
@profile_photo ||= Image.find(uuid: profile_photo_id) }
end
But what if all we really need to do is display the user’s profile photo in a view, and we don’t need any of the metadata (size, dimensions, date uploaded, etc.) about the image from Uploadcare. In this case, we can avoid an API call entirely by just doing:
def profile_photo
Image.new(uuid: profile_photo_id) }
end
Now we’ve got an image
object, and a call to url
or formatted_url
will give us the correct URL for the image on the CDN.
This small optimization works even better when we’re dealing with multiple images. Let’s say I want my users to have multiple profile photos, so I have my users
table configured as follows:
create_table :user do |t|
t.string :name
t.uuid :profile_photo_ids, array: true
end
Now let’s say that in my users/show
view I want to show all of the user’s profile photos with a bit of code like this:
<% @user.profile_photos.each do |photo| %>
<%= image_tag photo.url %>
<% end %>
If I only need to show the full-sized image and not any of the other data associated with the image, then I can define User#profile_photos
as follows and have the Image
objects I need without making any REST API calls:
def profile_photos
profile_photo_ids.map { |uuid| Image.new(uuid: uuid) }
end
Moving on, Image#formatted_url
provides an easy way for me to take advantage of Uploadcare’s image resize options, and Image#destroy
lets me delete the image from the service.
Finally, there are the three class methods: find
is for retrieving a fully populated Image
from the service, uuid_from_url
provides the inverse of Image.new(uuid: my_uuid).url
for use in tests and other situations where I want take in a URL and derive a UUID, and headers
is a convenience method that populates handles the authorization, API versioning, and timestamp requirements for the REST API.
Wiring up the controller and view
At this point, we’ve got what amounts to a fake ActiveRecord belongs_to :profile_photo
association on our User
model, so we can actually let users CRUD their images using the standard rails machinery of views and controllers. Let’s take a look at how that might work on a simple users/edit
view.
<%= f.label :profile_photo_id, "Profile Photo:" %>
<%= f.hidden_field :profile_photo_id, role: "uploadcare-uploader" %>
Assuming that your controller’s strong parameters are configured to permit(:profile_photo_id)
, the above bit of code will launch the Uploadcare widget and let you save the resulting UUID to your User
model.
If you want to remove an image from the CDN when it’s no longer associated with a User
, then you have a number of options. I suggest a before_save
hook in the User
model, like the following:
before_save :remove_orphaned_profile_photo
def remove_orphaned_profile_photo
return unless profile_photo_id.changed?
image = Image.new(uuid: profile_photo_id_was)
image.destroy
end
So there you have it: with very little code you can get Uploadcare working in your app, and you don’t need a bunch of stale dependencies. And don’t get me wrong: faking bits of ActiveRecord functionality in order to provide a clean interface between your rails app and a REST API is a strategy that I heartily endorse, but only if you’re implementing the functionality yourself. Doing it yourself means that all of the knowledge that your app needs to talk to the API lives in your repo under your control, and any newcomer to the app can read that code and related specs and know everything they need to know about the interface.
Comments
I’d like to read your article but something is wrong with it’s markup. Can I ask you to fix it?
@vizvamitra Sorry about that, we’ve cleaned up the post and it should be much more readable again!