Ngày nay việc mọi người mua những sản phẩm điện tử trên mạng diễn ra rất thường xuyên như hình ảnh, âm thanh, phần mềm, … . Và tất nhiên không phải ai muốn mình upload lên cái gì thì người khác cũng có thể tự do download file đó mà không ràng buộc có gì. Ví dụ như các trang web mua bán hình ảnh, âm thanh, …
Ví dụ chúng ta có 1 website bán tranh chẳng hạn, với các yêu cầu cơ bản như sau:
- Người dùng có thể upload ảnh lên để bán
- Người dùng có thể mua ảnh từ người khác
- Người dùng có thể xem lại và download ảnh mà họ đã đã mua
Chúng ta sẽ xây dựng database với mối quan hệ như sau:
Trong ví dụ này chúng ta sẽ dùng paperclip
cho việc upload file:
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> |
sau đó chạy bundle install
.
Chúng ta sẽ đi nhanh các phần khởi tạo migration, và 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> |
Đừng quên chạy lệnh rails db:migrate
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 Ảnh
Trước khi người dùng có thể bán thì tất nhiên họ phải tải ảnh lên
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> |
Danh sách ảnh đã upload của người dùng
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> |
Mua hàng
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> |
Chúng tra cần tạo bản ghi PurchasedImage
mới
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
Chúng ta muốn xem những giao dịch mua hàng nào đã được thực hiện bởi người dùng
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> |
Vậy là chúng ta đã dựng xong phần khung của website. Và giờ người dùng có thể truy cập vào những hình ảnh họ đã mua. Đã đến lúc chúng ta thêm chức năng tải xuống được kích hoạt bởi liên kết đó. Theo mặc định, Paperclip sẽ lưu trữ tệp đính kèm của bạn trong thư mục public/system
trong cấu trúc tệp của ứng dụng. Điều đó có nghĩa là chỉ cần click vào link là tải xuống. Tất nhiên, chúng ta muốn bảo mật các file, để chúng chỉ có thể được tải xuống bởi những người có quyền truy cập vào chúng sau khi mua.
Bản mật và download
Với paperclip
, khi khai báo file đính kèm ta làm như sau:
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> |
Đường dẫn mặc định để lưu file như sau: :rails_root/public/system/:class/:attachment/:id_partition/:style/:filename
. Folder public chúng ta thường để những gì dùng chung và mọi người đều có thể dùng, tất nhiên là chúng ta không muốn vậy, vì vậy chúng ta cần config 1 xíu trong 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> |
Giờ thì chúng ta đã có địa chỉ mới để lưu file được upload
Serving the secure images
Nôn na là chúng ta sẽ hiển thị thumbnail thay vì ảnh full size
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
Cuối cùng nhưng không kém quan trọng là 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> |
Khá giống với hàm display phía trên ngoại trừ việc ảnh được tải xuống sẽ là full size như ban đầu nó được upload lên
Tài liệu tham khảo
https://chrisherring.co/posts/how-can-i-protect-a-user-s-file-uploads-in-rails