Today, people buy electronic products online very often such as images, audio, software, …. And of course, not everyone wants to upload something, others can freely download the file without any obligation. For example, websites that buy and sell images, sounds, etc.
For example, we have a website that sells paintings, for example, with the following basic requirements:
- Users can upload photos for sale
- Users can purchase photos from others
- Users can review and download photos they have purchased
We will build the database with the following relationship:
In this example we will use paperclip
for file upload:
1 2 3 | <span class="token comment"># Gemfile</span> gem <span class="token string">'paperclip'</span> <span class="token punctuation">,</span> <span class="token string">'~> 5.0.0'</span> |
Then run bundle install
.
We will take a quick look at migration creation parts, and model:
Migration
1 2 3 4 5 6 7 8 9 10 | <span class="token comment"># Migration for create_users.rb </span> <span class="token keyword">class</span> <span class="token class-name">CreateUsers</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token punctuation">[</span> <span class="token number">5.1</span> <span class="token punctuation">]</span> <span class="token keyword">def</span> change create_table <span class="token symbol">:users</span> <span class="token keyword">do</span> <span class="token operator">|</span> t <span class="token operator">|</span> t <span class="token punctuation">.</span> string <span class="token symbol">:email</span> <span class="token punctuation">,</span> <span class="token symbol">:name</span> t <span class="token punctuation">.</span> timestamps <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 | <span class="token comment"># Migration for create_images.rb</span> <span class="token keyword">class</span> <span class="token class-name">CreateImages</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token punctuation">[</span> <span class="token number">5.1</span> <span class="token punctuation">]</span> <span class="token keyword">def</span> change create_table <span class="token symbol">:images</span> <span class="token keyword">do</span> <span class="token operator">|</span> t <span class="token operator">|</span> t <span class="token punctuation">.</span> integer <span class="token symbol">:user_id</span> t <span class="token punctuation">.</span> timestamps <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 | <span class="token comment"># Migration for Paperclip attachments</span> <span class="token keyword">class</span> <span class="token class-name">AddAttachmentToImages</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token punctuation">[</span> <span class="token number">5.1</span> <span class="token punctuation">]</span> <span class="token keyword">def</span> up add_attachment <span class="token symbol">:images</span> <span class="token punctuation">,</span> <span class="token symbol">:asset</span> <span class="token keyword">end</span> <span class="token keyword">def</span> down remove_attachment <span class="token symbol">:images</span> <span class="token punctuation">,</span> <span class="token symbol">:asset</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 | <span class="token comment"># Migration for create_purchased_images.rb</span> <span class="token keyword">class</span> <span class="token class-name">CreatePurchasedImages</span> <span class="token operator"><</span> <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Migration</span> <span class="token punctuation">[</span> <span class="token number">5.1</span> <span class="token punctuation">]</span> <span class="token keyword">def</span> change create_table <span class="token symbol">:purchased_images</span> <span class="token keyword">do</span> <span class="token operator">|</span> t <span class="token operator">|</span> t <span class="token punctuation">.</span> integer <span class="token symbol">:user_id</span> <span class="token punctuation">,</span> <span class="token symbol">:image_id</span> t <span class="token punctuation">.</span> timestamps <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Don’t forget to run the rails db:migrate
command
Model
1 2 3 4 5 6 7 8 | <span class="token comment"># app/models/user.rb</span> <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> has_many <span class="token symbol">:images</span> has_many <span class="token symbol">:purchased_images</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 | <span class="token comment"># app/models/image.rb</span> <span class="token keyword">class</span> <span class="token class-name">Image</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:user</span> has_attached_file <span class="token symbol">:asset</span> <span class="token punctuation">,</span> styles <span class="token punctuation">:</span> <span class="token punctuation">{</span> thumb <span class="token punctuation">:</span> <span class="token string">"200x200>"</span> <span class="token punctuation">}</span> validates_attachment_content_type <span class="token symbol">:asset</span> <span class="token punctuation">,</span> content_type <span class="token punctuation">:</span> <span class="token regex">/Aimage/.*z/</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 | <span class="token comment"># app/models/purchased_image.rb</span> <span class="token keyword">class</span> <span class="token class-name">PurchasedImage</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:user</span> belongs_to <span class="token symbol">:image</span> <span class="token keyword">end</span> |
Upload Photos
Before users can sell, of course they must upload photos
1 2 3 4 5 | <span class="token comment"># config/routes.rb</span> resources <span class="token symbol">:users</span> <span class="token keyword">do</span> resources <span class="token symbol">:images</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 | <span class="token comment"># app/controllers/images_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">ImagesController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token keyword">new</span> <span class="token variable">@image</span> <span class="token operator">=</span> <span class="token constant">Image</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token class-name">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 | <span class="token comment"># app/views/images/new.html.erb</span> <span class="token operator"><</span> h1 <span class="token operator">></span> <span class="token constant">New</span> <span class="token class-name">Image</span> <span class="token keyword">for</span> <span class="token operator"><</span> <span class="token string">%= current_user.name %></h1> <%=</span> form_for <span class="token punctuation">[</span> current_user <span class="token punctuation">,</span> <span class="token variable">@image</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> html <span class="token punctuation">:</span> <span class="token punctuation">{</span> multipart <span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token punctuation">}</span> <span class="token keyword">do</span> <span class="token operator">|</span> f <span class="token operator">|</span> <span class="token string">%> <p></span> <span class="token operator"><</span> <span class="token string">%= f.file_field :asset %></p> <p><%=</span> f <span class="token punctuation">.</span> submit <span class="token string">%></p></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token keyword">end</span> <span class="token operator">%</span> <span class="token operator">></span> |
List of photos uploaded by the user
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token comment"># app/controllers/users_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> index <span class="token variable">@users</span> <span class="token operator">=</span> <span class="token constant">User</span> <span class="token punctuation">.</span> all <span class="token keyword">end</span> <span class="token keyword">def</span> show <span class="token variable">@user</span> <span class="token operator">=</span> <span class="token constant">User</span> <span class="token punctuation">.</span> <span class="token function">find</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:id</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token comment"># app/views/users/index.html.erb</span> <span class="token operator"><</span> h1 <span class="token operator">></span> <span class="token constant">Users</span> <span class="token operator"><</span> <span class="token operator">/</span> h1 <span class="token operator">></span> <span class="token operator"><</span> ul <span class="token operator">></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token variable">@users</span> <span class="token punctuation">.</span> <span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span> user <span class="token operator">|</span> <span class="token string">%> <li></span> <span class="token operator"><</span> <span class="token string">%= link_to " <span class="token interpolation"><span class="token delimiter tag">#{</span> user <span class="token punctuation">.</span> name <span class="token delimiter tag">}</span></span> , <span class="token interpolation"><span class="token delimiter tag">#{</span> user <span class="token punctuation">.</span> images <span class="token punctuation">.</span> size <span class="token delimiter tag">}</span></span> images", user_path(user) %> <%=</span> link_to <span class="token string">'Upload Image'</span> <span class="token punctuation">,</span> <span class="token function">new_user_image_path</span> <span class="token punctuation">(</span> user <span class="token punctuation">)</span> <span class="token keyword">if</span> current_user <span class="token operator">==</span> user <span class="token string">%> </li></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token keyword">end</span> <span class="token string">%> </ul></span> |
1 2 3 4 5 6 7 | <span class="token comment"># app/views/users/show.html.erb</span> <span class="token operator"><</span> h1 <span class="token operator">></span> <span class="token constant">Images</span> offered by <span class="token operator"><</span> <span class="token string">%= @user.name %></h1> <% @user.images.each do |image| %> <%=</span> image_tag image <span class="token punctuation">.</span> asset <span class="token punctuation">.</span> <span class="token function">url</span> <span class="token punctuation">(</span> <span class="token symbol">:thumb</span> <span class="token punctuation">)</span> <span class="token string">%> <% end %></span> |
Purchase
1 2 3 4 5 6 7 | <span class="token comment"># config/routes.rb</span> resources <span class="token symbol">:users</span> <span class="token keyword">do</span> resources <span class="token symbol">:images</span> <span class="token keyword">do</span> post <span class="token symbol">:purchase</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
We need to create a new PurchasedImage
record
1 2 3 4 5 6 7 8 9 10 11 | <span class="token comment"># app/views/images/show.html.erb</span> <span class="token operator"><</span> h1 <span class="token operator">></span> <span class="token operator"><</span> <span class="token string">%= @image.asset_file_name %> offered by <%=</span> <span class="token variable">@image</span> <span class="token punctuation">.</span> user <span class="token punctuation">.</span> name <span class="token string">%></h1></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token keyword">unless</span> <span class="token variable">@image</span> <span class="token punctuation">.</span> user <span class="token operator">==</span> current_user <span class="token string">%> <%= form_for [current_user, @image], url: user_image_purchase_path, method: :post do |f| %></span> <span class="token operator"><</span> <span class="token string">%= f.submit "Purchase" %> <% end %> <% end %> <%=</span> image_tag <span class="token variable">@image</span> <span class="token punctuation">.</span> asset <span class="token punctuation">.</span> <span class="token function">url</span> <span class="token punctuation">(</span> <span class="token symbol">:thumb</span> <span class="token punctuation">)</span> <span class="token operator">%</span> <span class="token operator">></span> |
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token comment"># app/controllers/images_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">ImagesController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token comment"># code omitted</span> <span class="token keyword">def</span> purchase image <span class="token operator">=</span> <span class="token constant">Image</span> <span class="token punctuation">.</span> <span class="token function">find</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:image_id</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token constant">PurchasedImage</span> <span class="token punctuation">.</span> <span class="token function">create</span> <span class="token punctuation">(</span> user <span class="token punctuation">:</span> current_user <span class="token punctuation">,</span> image <span class="token punctuation">:</span> image <span class="token punctuation">)</span> redirect_to users_path <span class="token keyword">end</span> <span class="token keyword">end</span> |
Purchases Link
We want to see which purchases have been made by users
1 2 3 4 5 6 7 8 9 10 | <span class="token comment"># config/routes.rb</span> <span class="token constant">Rails</span> <span class="token punctuation">.</span> application <span class="token punctuation">.</span> routes <span class="token punctuation">.</span> draw <span class="token keyword">do</span> resources <span class="token symbol">:users</span> <span class="token keyword">do</span> get <span class="token symbol">:purchases</span> resources <span class="token symbol">:images</span> <span class="token keyword">do</span> post <span class="token symbol">:purchase</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token comment"># app/views/users/index.html.erb</span> <span class="token operator"><</span> h1 <span class="token operator">></span> <span class="token constant">Users</span> <span class="token operator"><</span> <span class="token operator">/</span> h1 <span class="token operator">></span> <span class="token operator"><</span> ul <span class="token operator">></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token variable">@users</span> <span class="token punctuation">.</span> <span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span> user <span class="token operator">|</span> <span class="token string">%> <li></span> <span class="token operator"><</span> <span class="token string">%= link_to " <span class="token interpolation"><span class="token delimiter tag">#{</span> user <span class="token punctuation">.</span> name <span class="token delimiter tag">}</span></span> , <span class="token interpolation"><span class="token delimiter tag">#{</span> user <span class="token punctuation">.</span> images <span class="token punctuation">.</span> size <span class="token delimiter tag">}</span></span> images", user_path(user) %> <%=</span> link_to <span class="token string">'Upload Image'</span> <span class="token punctuation">,</span> <span class="token function">new_user_image_path</span> <span class="token punctuation">(</span> user <span class="token punctuation">)</span> <span class="token keyword">if</span> current_user <span class="token operator">==</span> user <span class="token string">%> <%= link_to " <span class="token interpolation"><span class="token delimiter tag">#{</span> user <span class="token punctuation">.</span> purchased_images <span class="token punctuation">.</span> size <span class="token delimiter tag">}</span></span> Purchased Images", user_purchases_path(user) %></span> <span class="token operator"><</span> <span class="token operator">/</span> li <span class="token operator">></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token keyword">end</span> <span class="token string">%> </ul></span> |
1 2 3 4 5 6 7 8 9 10 11 | <span class="token comment"># config/routes.rb</span> <span class="token constant">Rails</span> <span class="token punctuation">.</span> application <span class="token punctuation">.</span> routes <span class="token punctuation">.</span> draw <span class="token keyword">do</span> resources <span class="token symbol">:users</span> <span class="token keyword">do</span> get <span class="token symbol">:purchases</span> resources <span class="token symbol">:images</span> <span class="token keyword">do</span> post <span class="token symbol">:purchase</span> get <span class="token symbol">:download</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 | <span class="token comment"># app/controllers/users_controllers.rb</span> <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token comment"># code omitted</span> <span class="token keyword">def</span> purchases <span class="token variable">@user</span> <span class="token operator">=</span> current_user <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | <span class="token operator"><</span> <span class="token operator">%</span> <span class="token comment"># app/view/users/purchases.html.erb %></span> <span class="token operator"><</span> h1 <span class="token operator">></span> <span class="token constant">Images</span> purchased by <span class="token operator"><</span> <span class="token string">%= current_user.name %></h1> <% current_user.purchased_images.each do |purchase| %> <%=</span> link_to <span class="token function">image_tag</span> <span class="token punctuation">(</span> purchase <span class="token punctuation">.</span> image <span class="token punctuation">.</span> asset <span class="token punctuation">.</span> <span class="token function">url</span> <span class="token punctuation">(</span> <span class="token symbol">:thumb</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token function">user_image_download_path</span> <span class="token punctuation">(</span> current_user <span class="token punctuation">,</span> purchase <span class="token punctuation">.</span> image <span class="token punctuation">)</span> <span class="token string">%> <% end %></span> |
So we have built the frame of the website. And now users can access the images they have purchased. It’s time we add the download function enabled by that link. By default, Paperclip will store your attachment in the public/system
directory in the application’s file structure. That means just clicking the download link. Of course, we want to secure our files, so they can only be downloaded by people who have access to them after purchase.
Confidential and download
With paperclip
, when declaring attachments, we do the following:
1 2 3 4 5 6 7 8 9 | <span class="token comment"># app/models/image.rb</span> <span class="token keyword">class</span> <span class="token class-name">Image</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:user</span> has_attached_file <span class="token symbol">:asset</span> <span class="token punctuation">,</span> styles <span class="token punctuation">:</span> <span class="token punctuation">{</span> thumb <span class="token punctuation">:</span> <span class="token string">"200x200>"</span> <span class="token punctuation">}</span> validates_attachment_content_type <span class="token symbol">:asset</span> <span class="token punctuation">,</span> content_type <span class="token punctuation">:</span> <span class="token regex">/Aimage/.*z/</span> <span class="token keyword">end</span> |
The default path for saving files is as follows:: :rails_root/public/system/:class/:attachment/:id_partition/:style/:filename
. The public folder we usually share and everyone can use, of course we don’t want that, so we need a little configuration in the model:
1 2 3 4 5 6 7 8 9 10 | <span class="token comment"># app/models/image.rb</span> <span class="token keyword">class</span> <span class="token class-name">Image</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:user</span> has_attached_file <span class="token symbol">:asset</span> <span class="token punctuation">,</span> styles <span class="token punctuation">:</span> <span class="token punctuation">{</span> thumb <span class="token punctuation">:</span> <span class="token string">"200x200>"</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> path <span class="token punctuation">:</span> <span class="token string">":rails_root/secure_files/:class/:attachment/:id_partition/:style/:filename."</span> <span class="token punctuation">,</span> validates_attachment_content_type <span class="token symbol">:asset</span> <span class="token punctuation">,</span> content_type <span class="token punctuation">:</span> <span class="token regex">/Aimage/.*z/</span> <span class="token keyword">end</span> |
Now we have a new address to save the uploaded file
Serving the secure images
The problem is that we will show thumbnails instead of full size images
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token comment"># config/routes.rb</span> <span class="token constant">Rails</span> <span class="token punctuation">.</span> application <span class="token punctuation">.</span> routes <span class="token punctuation">.</span> draw <span class="token keyword">do</span> resources <span class="token symbol">:users</span> <span class="token keyword">do</span> get <span class="token symbol">:purchases</span> resources <span class="token symbol">:images</span> <span class="token keyword">do</span> post <span class="token symbol">:purchase</span> get <span class="token symbol">:download</span> <span class="token keyword">end</span> <span class="token keyword">end</span> get <span class="token string">'/images/:id/display'</span> <span class="token punctuation">,</span> to <span class="token punctuation">:</span> <span class="token string">"images#display"</span> <span class="token punctuation">,</span> as <span class="token punctuation">:</span> <span class="token string">"secure_image_display"</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 | <span class="token comment"># app/models/image.rb</span> <span class="token keyword">class</span> <span class="token class-name">Image</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> belongs_to <span class="token symbol">:user</span> has_attached_file <span class="token symbol">:asset</span> <span class="token punctuation">,</span> styles <span class="token punctuation">:</span> <span class="token punctuation">{</span> thumb <span class="token punctuation">:</span> <span class="token string">"200x200>"</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> path <span class="token punctuation">:</span> <span class="token string">":rails_root/secure_files/:class/:attachment/:id_partition/:style/:filename."</span> <span class="token punctuation">,</span> url <span class="token punctuation">:</span> <span class="token string">"/images/:id/display"</span> validates_attachment_content_type <span class="token symbol">:asset</span> <span class="token punctuation">,</span> content_type <span class="token punctuation">:</span> <span class="token regex">/Aimage/.*z/</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 | <span class="token comment"># app/controllers/images_controller.rb</span> <span class="token keyword">def</span> display <span class="token variable">@image</span> <span class="token operator">=</span> <span class="token constant">Image</span> <span class="token punctuation">.</span> <span class="token function">find</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:id</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> send_file <span class="token variable">@image</span> <span class="token punctuation">.</span> asset <span class="token punctuation">.</span> <span class="token function">path</span> <span class="token punctuation">(</span> <span class="token symbol">:thumb</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> |
Downloading Purchases
Last but not least is the download
1 2 3 4 5 6 | <span class="token comment"># app/controllers/images_controller.rb</span> <span class="token keyword">def</span> download image <span class="token operator">=</span> <span class="token constant">Image</span> <span class="token punctuation">.</span> <span class="token function">find</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:id</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> send_file image <span class="token punctuation">.</span> asset <span class="token punctuation">.</span> path <span class="token keyword">end</span> |
Quite similar to the above display function except that the downloaded image will be full size as it was originally uploaded
References
https://chrisherring.co/posts/how-can-i-protect-a-user-s-file-uploads-in-rails