How can I protect a user's file uploads in Rails?
The world is becoming much more digital. As a result, a lot more of the things that we use and buy are digital items, ones that we download from websites after we pay for them. There will very likely be a time in your career when you will have to build a website that is offering things for sale, digital things. Ebooks, software downloads, these kinds of things. These things will very likely be stored on the server, waiting for people to purchase them and then download them. But how do you stop people from just being able to download them without paying for them?
Out of the box, static files for download implementation in a web application will likely involve storing files in the /public
section of your site/application which means that browsers can just grab what they need for your application to run for your users. This works great for the application's assets, it's icons, stylesheets and JavaScripts, but of course, it's not what you want if people, or yourself, are storing digital assets in the application that are for sale. You will only want these to be available to those who have purchased them. Luckily with Ruby on Rails and Paperclip, this can be achieved without too much difficulty. Let's take a look
Install Paperclip
First up, you want to install Paperclip in your Rails application, so let's add it to a Gemfile.
# Gemfile
gem 'paperclip', '~> 5.0.0
And run bundle
for you application.
Now that we have Paperclip in, let's look at what we need to do. For an application like this, we need a few things.
- A User class, this will be people who have things for sale, as well as others who want to purchase things
- An Image class, this can be any files of any type, you may want it to be more generic, but image works fine for purposes of example
- A PurchasedImage class, this will link purchases with the users that have bought them in a many to many relationship, as users can buy more than one thing, and images can be bought by more than one person.
In a production application where you sell digital goods, you will also need to have a way to take payment. That is beyond the scope of what I'm talking about here, but I have written several things on how to accept payments with Stripe, so you can refer to those if you need.
Storing our data
Let's quickly run through those ActiveRecord classes. These will just be a bare minimum for you to see how this all works, your classes in your application will undoubtedly store other information as well.
# Migration for create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :email, :name
t.timestamps
end
end
end
This User
class will be for all of our users, both those that want to upload images for sale and those that wish to purchase images. One User would be able to both upload images for sale and purchase images from other users.
# Migration for create_images.rb
class CreateImages < ActiveRecord::Migration[5.1]
def change
create_table :images do |t|
t.integer :user_id
t.timestamps
end
end
end
The Image
class will store the information for images, and the user_id
is the User
that owns the image.
# Migration for Paperclip attachments
class AddAttachmentToImages < ActiveRecord::Migration[5.1]
def up
add_attachment :images, :asset
end
def down
remove_attachment :images, :asset
end
end
This migration is made available to us by using Paperclip. It will add all the fields to store all the information we need to know about the image files themselves.
# Migration for create_purchased_images.rb
class CreatePurchasedImages < ActiveRecord::Migration[5.1]
def change
create_table :purchased_images do |t|
t.integer :user_id, :image_id
t.timestamps
end
end
end
Our PurchasedImage
class will allow us to join users and the images that they have purchased.
Now that we have all of our migrations for the database, we can setup a database capable of doing what we need:
> bundle exec rake db:migrate
And now we have our database tables ready to go.
Setup our Models
Now that we have tables in the database, we need to set up our ActiveRecord
models that will use them
# app/models/user.rb
class User < ApplicationRecord
has_many :images
has_many :purchased_images
end
As mentioned, our User
will be able to have both images (ones they own for sale) and purchased images associated with their account.
# app/models/image.rb
class Image < ApplicationRecord
belongs_to :user
has_attached_file :asset, styles: { thumb: "200x200>" }
validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/
end
Our Image
class belongs to a User
, the person who owns it and has it for sale. It has an attached file, this is the information that Paperclip stores about a file, it's location, content type and the like.
# app/models/purchased_image.rb
class PurchasedImage < ApplicationRecord
belongs_to :user
belongs_to :image
end
For when a purchase is made, we need to store which image was purchased by which user.
How does it all work together
So what do we need to get this thing working for users? What does it need to do?
- A user needs to be able to upload images that they can offer for sale
- We need a way of listing users and their images
- We need to be able to view an image
- We need to be able to purchase an image from the screen that displays it
- We need to able to list, view and download images that we have purchased
- We need to make sure that images are not accessible for download if they have not been purchased
Uploading an Image
Before a User
can have an Image
for sale, they need to be able to get it up onto the site. So let's add a way for them to be able to do this. First we need a controller for the feature, a route for it, and a form.
# config/routes.rb
resources :users do
resources :images
end
So we have a route for our user's pages, and inside them for a user will be a way to upload an Image
, so we have the routes for images nested under users, as it will be done in the context of a particular User
.
# app/controllers/images_controller.rb
class ImagesController < ApplicationController
def new
@image = Image.new
end
end
Pretty simple, we just need to create an @image
object to get used to render our new
form:
<% # app/views/images/new.html.erb %>
<h1>New Image for <%= current_user.name %></h1>
<%= form_for [current_user, @image], html: { multipart: true } do |f| %>
<p><%= f.file_field :asset %></p>
<p><%= f.submit %></p>
<% end %>
A very simple for, just a file field to upload an Image, the form_for
passes both the current_user
and the @image
, this way the route that it builds will be new_user_image_path
which will route to our images nested under users.
Now if a User
goes to this form they can upload an image to the site and have it for sale.
A User and their Images
To be able to purchase an Image
that has been uploaded, we need to have a section that will display our all of our users, and in turn, their images that they are offering. We already have our routes for these, so let's add the controller and the views.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
@users = User.all
end
def show
@user = User.find(params[:id])
end
end
We will need two views. An index, to see all of our users, and a show, to see details about what a particular User
has for sale. So let's add those pages now.
<% # app/views/users/index.html.erb %>
<h1>Users</h1>
<ul>
<% @users.each do |user| %>
<li>
<%= link_to "#{user.name}, #{user.images.size} images", user_path(user) %>
<%= link_to 'Upload Image', new_user_image_path(user) if current_user == user %>
</li>
<% end %>
</ul>
Here our index page will list all of our users and the number of images that they have available for purchase. Clicking on a user's name will take the User
through to the show page:
<h1>Images offered by <%= @user.name %></h1>
<% @user.images.each do |image| %>
<%= image_tag image.asset.url(:thumb) %>
<% end %>
Here we are on the page for a User
and presented with a list of their images for purchase shown in a smaller thumbnail form.
How about Purchasing
Now that we have all of the pieces that we need together, we need to build purchasing an image. To do that, we need to add a button to each image so that the current logged in user can purchase it and an action to be triggered when it is pressed. First, we need a route for the purchase button:
# config/routes.rb
resources :users do
resources :images do
post :purchase
end
end
This will give us the route that we need to create a PurchasedImage
record, so let's add our button:
<% # app/views/images/show.html.erb %>
<h1><%= @image.asset_file_name %> offered by <%= @image.user.name %></h1>
<% unless @image.user == current_user %>
<%= form_for [current_user, @image], url: user_image_purchase_path, method: :post do |f| %>
<%= f.submit "Purchase" %>
<% end %>
<% end %>
<%= image_tag @image.asset.url(:thumb) %>
And we need a controller action hooked up to that button:
# app/controllers/images_controller.rb
class ImagesController < ApplicationController
# code omitted
def purchase
image = Image.find(params[:image_id])
PurchasedImage.create(user: current_user, image: image)
redirect_to users_path
end
end
So now when a User
goes to an Image page and clicks on the purchase button, then this action in the images controller will be fired, creating a PurchasedImage
for the current_user
and redirects back to the users_path
page. In your own application, you will want to choose a page that makes sense for you, likely some kind of purchases page.
Purchases Link
Now that we are back on our user's page, we will want to be able to see what purchases have been made. Let's add a route for a user's purchases and a link to the page:
# config/routes.rb
Rails.application.routes.draw do
resources :users do
get :purchases
resources :images do
post :purchase
end
end
end
# app/views/users/index.html.erb
<h1>Users</h1>
<ul>
<% @users.each do |user| %>
<li>
<%= link_to "#{user.name}, #{user.images.size} images", user_path(user) %>
<%= link_to 'Upload Image', new_user_image_path(user) if current_user == user %>
<%= link_to "#{user.purchased_images.size} Purchased Images", user_purchases_path(user) %>
</li>
<% end %>
</ul>
Now we have a Purchased images link, so a user can go and see their images that they have, let's add the controller action and page to view them:
# app/controllers/users_controllers.rb
class UsersController < ApplicationController
# code omitted
def purchases
@user = current_user
end
end
On this page, we are going to need a link for a User
to be able to download their images, so let's add a route and make the page:
# config/routes.rb
Rails.application.routes.draw do
resources :users do
get :purchases
resources :images do
post :purchase
get :download
end
end
end
Securing the files and downloads
With paperclip, when we declare an attached file, like the following:
# app/models/image.rb
class Image < ApplicationRecord
belongs_to :user
has_attached_file :asset, styles: { thumb: "200x200>" }
validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/
end
Now our file will be stored in a section of the server that is not open to the outside world. This does present us with a new problem though, how do we display a preview of it in the browser if we want? A quick way to do this to add a route to our app so Rails can serve the image for us. This is not normally recommended as Rails serving static files is not what you would normally want in production, but you should be able to either cache it or with some extra configuration have nginx or Apache still serve the file for you. To get this working in development though, let's just have Rails serve. A good way to get things built is to get them working first, then make them better. This way you can always have a fallback point of a working feature, even if the code isn't as optimal as you'd like.
Downloading Purchases
Last but not least, letting our users download any images that they have purchased. We have our link and our route for them to download their file, we just need to add a controller action to get it to them. So we need to add our action to the controller:
# app/controllers/images_controller.rb
def download
image = Image.find(params[:image_id])
send_file image.asset.path
end
Nice and simple, we just need to look up our Image
and then use send_file
to send it to their browser, the same as our display except that we are sending the original full size Image
.
Considerations and Caveats
I've used Images in this example, but that is just because it was easier to put together. If you are going to put together this for images, I would say watermark them and have those available as public images that your website can use to show what's on offer, and have the originals stored in the secure section for download after purchase. Of course, this can be used for any digital assets for download, ebooks, applications, code templates, etc.
When you take this to production, do look into having your web server serve your static assets rather your rails app. There will be some extra configuration, but having the web server serve the static files means each part of your infrastructure is doing what it does best.
I would also not have any state on your application server and store the files on S3 or something similar. This allows you to have more than one application server running for your app, and any of them will be able to handle any request, which will help you reduce complexity and keep things simple.