If you are building an eCommerce in Solidus, you can add Files to Products so customers can have them available for download. Find out how to do it by reusing Spree::Assets and how to protect those files using expirable URLs. Keep reading!
Recently, in a Solidus project, we faced the need to add Files to Products so customers could download them.
In general terms, the business logic was:
- A Product can have several Files
- Customers can download Files from Purchased Products
- File links cannot be shared
- Files must be manageable
As the project was using Amazon S3 to store files and we did not want to modify Solidus a lot, our approach was the following:
Models
Generate a File Model
We created a File Model inheriting from Spree::Asset adding a method to obtain a presigned_url which expires in 30 seconds, allowing the browser to init download but not to share.
app/models/my_app_scope/file.rb
module MyAppScope
class File < ::Spree::Asset
has_attached_file :attachment,
url: '/spree/products/:id/:basename.:extension',
path: ':rails_root/public/spree/products/:id/:basename.:extension'
do_not_validate_attachment_file_type :attachment
def download_url
s3_object.presigned_url('get', expires_in: 30)
end
end
end
In this example the paths and validations were not modified, however, you can easily define them as needed.
Link File Model to Product and User
We decorated Product by adding the has_many association to Files. In our case, we did not need to add more information to the File, such as flags or metadata. But if that were the case, it would be better to create a has_many_through association containing the information to prevent the modification of Spree::Asset model.
app/models/decorators/solidus/product.rb
module Decorators
module Product
extend ActiveSupport::Concern
included do
has_many :files, as: :viewable,
dependent: :destroy,
class_name: 'MyAppScope::File'
end
end
end
Spree::Product.include(Decorators::Product)
Given we need to restrict Files access to Users, we added a couple of scopes to it. One of them to Products through Orders, and the other one to Files through Products.
app/models/decorators/solidus/user.rb
# frozen_string_literal: true
module Decorators
module Solidus
module User
extend ActiveSupport::Concern
included do
has_many :products,
-> { unscope(:order).distinct },
through: :orders
has_many :files,
-> { distinct },
through: :products
end
end
end
end
Spree::User.include(Decorators::Solidus::User)
File Access
Restricting Access
To restrict Files access, we created a PermissionSet where we look for the File ID which belongs to User file_ids.
lib/spree/permission_sets/file_ability.rb
# frozen_string_literal: true
module Spree
module PermissionSets
class FileAbility < PermissionSets::Base
def activate!
can :download,
MyAppScope::File,
id: files_ids
end
private
def files_ids
@files_ids ||= user.file_ids
end
end
end
end
Then, we used a controller to redirect to File presigned_url, delegating the action to the browser; E.g. Zip files will be downloaded while PDF or Images will be opened.
app/controllers/my_app_scope/files_controller.rb
module MyAppScope
class FilesController < Spree::StoreController
def download
authorize!(:download, file)
redirect_to file.download_url
end
private
def file
@file ||= MyAppScope::File.find(params[:id])
end
end
end
The route for this action will be the next one:
config/routes.rb
Rails.application.routes.draw do
scope module: 'my_app_scope' do
get 'download/:id', to: 'files#download', as: :download
end
end
Rendering Files
Showing Product Files List
In our case, we modified Account to have different sections using ViewComponents, but the simplest is:
<%- product.files.each do |file| %>
<li><%= link_to file.attachment_file_name, download_path(file), target: '_blank' %></li>
<% end %>
The last step would be to generate overrides, views, routes, and controllers for the Admin. However, I will not be covering that in this blog post. You can see examples of that here Gist: Code: Using Spree::Asset to add Files to Spree::Product.
I hope it was useful for you. Thanks for reading¡