How to re-use Spree::Asset in Solidus to attach Files to another model

Reading Time: 2 minutes

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.

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¡

0 Shares:
You May Also Like