June 14, 2017
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
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.
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.
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.
So what do we need to get this thing working for users? What does it need to do?
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.
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.
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.
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.
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 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.
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
.
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.
Comments?
I've changed how I run my blog now and have decided to not integrate comments into this new version. I am happy to answer any questions though, feel free to send me an email through the link at the top of the screen. Happy Programming.