Search code examples
ruby-on-railsrails-activestorage

Change ActiveStorage DirectDisk service configuration at runtime


I am using Rails 5.2 and ActiveStorage 5.2.3 and DirectDiskService.

In order to have user uploads grouped nicely in directories and in order to be able to use CDNs as I please (eg CloudFlare or CloudFront or any other), I am trying to set up a method in ApplicationController that sets the (local) path for uploads, something like:

class ApplicationController < ActionController::Base
  before_action :set_uploads_path

  # ...

private
  def set_upload_paths
     # this doesn't work
     ActiveStorage::Service::DirectDiskService.set_root p: "users/#{current_user.username}"
     # ... expecting the new root to become "public/users/yaddayadda"
  end
end

In config/initializers/active_storage.rb I have:

module SetDirectDiskServiceRoot
    def set_root(p:)
        @root = Rails.root.join("public", p)
        @public_root = p
        @public_root.prepend('/') unless @public_root.starts_with?('/')
    end
end

ActiveStorage::Service::DirectDiskService.module_eval { attr_writer :root }
ActiveStorage::Service::DirectDiskService.class_eval { include SetDirectDiskServiceRoot }

It does let me set the root on a new instance of the service but not on the one the Rails application is using.

What am I doing wrong? Or how do I get this done?


Solution

  • Here's a dirty hack that helps you keep your local (disk) uploads grouped nice under public/websites/domain_name/uploads.

    Step 1) install ActiveStorage DirectDisk service from here: https://github.com/sandrew/activestorage_direct_disk

    Step 2) in app/models/active_storage/current.rb

    class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
      attribute :host
      attribute :domain_name
    end
    

    Step 3) lib/set_direct_disk_service_path.rb

    module SetCurrentDomainName
        def set_domain_name(d)
            self.domain_name = d
        end
    end
    
    ActiveStorage::Current.class_eval { include SetCurrentDomainName }
    
    module SetDirectDiskServiceRoot
        def initialize(p:, public: false, **options)
            @root = Rails.root.join("public", p)
            @public_root = p
            @public_root.prepend('/') unless @public_root.starts_with?('/')
            puts options
        end
    
        def current_domain_name
            ActiveStorage::Current.domain_name
        end
    
        def folder_for(key)
            # original: File.join root, folder_for(key), key
            p = [ current_domain_name, "uploads", "all", key ]
            blob = ActiveStorage::Blob.find_by(key: key)
            if blob
                att = blob.attachments.first
                if att
                    rec = att.record
                    if rec
                        p = [ current_domain_name, "uploads", rec.class.name.split("::").last.downcase, rec.id.to_s, att.name, key ]
                    end
                end
            end
            return File.join p
        end
    end
    
    ActiveStorage::Service::DirectDiskService.module_eval { attr_writer :root }
    ActiveStorage::Service::DirectDiskService.class_eval { include SetDirectDiskServiceRoot }
    

    Step 4) in config/initializers/active_storage.rb

    require Rails.root.join("lib", "set_direct_disk_service_path.rb")
    

    Step 5) in app/controllers/application_controller.rb

    before_action :set_active_storage_domain_name
    
    # ...
    def set_active_storage_domain_name
        ActiveStorage::Current.domain_name = current_website.domain_name # or request.host
    end
    

    Step 6) in config/storage.yml

    development:
      service: DirectDisk
      root: 'websites_development'
    
    production:
      service: DirectDisk
      root: 'websites'
    

    Disadvantages:

    While ActiveRecord technically "works", it's missing some very important features that make it unusable for most people, so eventually the developer(s) will listen and adjust; at that time you may need to revisit this code AND all of your uploads.

    The service attempts to "guess" the class name a blob is attached to since AS doesn't pass that, so it runs extra 2-3 queries against your database. If this bothers you just remove that bit and let it all go under websites/domain_name/uploads/all/

    In some cases (eg variants, or a new record with action_text column) it can't figure out the attachment record and its class name, so it will upload under websites/domain/uploads/all/...