June 15, 2017

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.

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?

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

On our page we are also going to have a link for them to download their purchases, so we just need to add this to routes as well:

# config/routes.rb
Rails.application.routes.draw do
  resources :users do
    get :purchases
    resources :images do
      post :purchase
      get :download
    end
  end
end
<% # app/view/users/purchases.html.erb %>
<h1>Images purchased by <%= current_user.name %></h1>

<% current_user.purchased_images.each do |purchase| %>
  <%= link_to image_tag(purchase.image.asset.url(:thumb)), user_image_download_path(current_user, purchase.image) %>
<% end %>

Now your users can access their images that they have purchased. It's time for us to add a download function activated by that link. By default, Paperclip will store your attachments in the public/system directory of your application's file structure. That means that they are just available for clicking and downloading. Of course, we want to secure our files so they can only be downloaded by those that have access to them after purchase. So let's look at how we can secure our files, and still have them able to be downloaded.

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

It has a default path for the application to store its file. It is :rails_root/public/system/:class/:attachment/:id_partition/:style/:filename which puts it in the public directory of our application. We don't want our files to be downloadable by the general public, so we need to change this configuration, let's update our Image model:

# app/models/image.rb
class Image < ApplicationRecord

  belongs_to :user

  has_attached_file :asset, styles: { thumb: "200x200>" },
                      path: ":rails_root/secure_files/:class/:attachment/:id_partition/:style/:filename.",
  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.

Serving the secure images

Serving the images isn't hard, we just need a route and a controller action for them.

# config/routes.rb
Rails.application.routes.draw do

  resources :users do
    get :purchases
    resources :images do
      post :purchase
      get :download
    end
  end

  get '/images/:id/display', to: "images#display", as: "secure_image_display"

end

Now that we have a URL that can be used to get an image, we need to map it to our Paperclip attachment:

# app/models/image.rb
class Image < ApplicationRecord

  belongs_to :user

  has_attached_file :asset, styles: { thumb: "200x200>" },
                      path: ":rails_root/secure_files/:class/:attachment/:id_partition/:style/:filename.",
                      url: "/images/:id/display"
  validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/
end

The same parameters are available to URL as path, but I have not supplied a style, we are only showing thumbnails as samples in this app, but you could easily have /images/:style/:id/display or something along those lines to be able to serve different versions of the image. Now we just need a controller action:

# app/controllers/images_controller.rb
  def display
    @image = Image.find(params[:id])
    send_file @image.asset.path(:thumb)
  end

By adding this action into the controller, anywhere that we now call <%= image_tag @image.asset.url %> we will get the thumbnail image instead of a broken link.

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.