Skip to content

Instantly share code, notes, and snippets.

@mumoc
Last active December 1, 2020 18:33
Show Gist options
  • Save mumoc/fcc7038ed4c7b76e899acedd433214de to your computer and use it in GitHub Desktop.
Save mumoc/fcc7038ed4c7b76e899acedd433214de to your computer and use it in GitHub Desktop.
How to re-use Spree::Asset in Solidus to attach Files to another model

Recently, in a Solidus project, we faced the need to add Files to Products for customers to be able to download.

In general terms, the business logic was:

  • A Product can have many Files
  • Customers can download Files from purchased Products
  • File links cannot be shared
  • Files must be administrables

Given the project was using Amazon S3 to store the files and we didn't wanted to modify Solidus a lot, our approach was:

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 or validations weren't modified, but you can easily define them as needed.

Link File Model to Product and User

We decorate Product adding the has_many association to Files; in our case we didn't have the need to add more information to the File, like flags or metadata, but if that would be the case, it would be better to create a has_many_through association containing the information preventing 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 for a User, we added a couple of scopes to it. One to Products through his Orders and another to Files through the 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 to 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 redirecto 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

and the route for this action:

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 very basic is:

<%- product.files.each do |file| %>
  <li><%= link_to file.attachment_file_name, download_path(file), target: '_blank' %></li>
<% end %>

The last thing would be to generate overrides, views, routes, and controller for the Admin, but I'll not cover that in this post; you can see example code in this Gist: Code: Using Spree::Asset to add Files to Spree::Product.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment