Patch Blob model in Active Storage

1.2k views Asked by At

I want to patch Rails 6.0 to include part of this PR: https://github.com/rails/rails/commit/4dba136c83cc808282625c0d5b195ce5e0bbaa68 I'm only using direct uploads so I'm only patching create_before_direct_upload! at the moment. Here is what I have tried:

  1. In initializers/active_storage.rb
module BlobOverride
  class << self
    def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
      puts "In Blob Override Patch"
      byebug
      create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
    end
  end
end

 ActiveStorage::Blob.prepend(BlobOverride)

This returns the undefined methodhas_one_attached'` error, which I tracked down to a github issue here: https://github.com/rails/rails/issues/38876 Which basically says you can't use load the model from the initializer.
2. I then tried loading the module this way:

ActiveSupport.on_load(:active_storage_blob) do
  ActiveStorage::Blob.prepend(BlobOverride)
end

And I didn't get an error but my patch wasn't hit.
3. I tried this:

Rails.application.config.to_prepare do
  require 'active_storage/blob'
  ActiveStorage::Blob.class_eval do
    def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
      puts "in create before direct upload patch"
      byebug
      create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
    end
  end
end

No error, patched method wasn't hit.

TLDR

How do I patch the blob model on active storage to support a custom key? The standard monkey patching isn't working for some reason.

3

There are 3 answers

0
Arian Faurtosh On

This applies for Rails 6.1:

ActiveRecord now has a thing called has_secure_token, which ActiveStorage is using on Blob. This has_secure_token is setting a before_create with the following code:

before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }

This in turn sets self[:key] to be a secure_token

When it's being called like this, the self[:key] method only sets it if it doesn't exist

ActiveStorage::Blob.class_eval do
  def key
    self[:key] ||= 'your custom key'
  end
end

I fixed this by doing the following in config/application.rb

class Application < Rails::Application
  # ...
  # ...
  # ...

  Rails.application.config.to_prepare do
    require 'active_storage/blob'
    class ActiveStorage::Blob
      def self.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
        gust = SecureRandom.base36(length)
        epoch = Time.now.to_i.to_s
        checksum = Digest::MD5.hexdigest(gust + epoch)

        folder = checksum.chars.first(9).each_slice(3).to_a.map(&:join).join('/')

        "#{folder}/#{gust}"
      end
    end

    # Rails 5 doesn't have secure_token for Blob, so this will monkey patch it
    ActiveStorage::Blob.class_eval do
      def key
        self[:key] ||= generate_key_with_folder
      end

      def generate_key_with_folder
        gust = self.class.generate_unique_secure_token
        epoch = Time.now.to_i.to_s
        checksum = Digest::MD5.hexdigest(gust + epoch)

        folder = checksum.chars.first(9).each_slice(3).to_a.map(&:join).join('/')

        "#{folder}/#{gust}"
      end
    end
  end
end
1
gabriel On

Thanks to @Arian's answer, here is my implementation in a Rails 5 app.

I created an initializer config/initializers/active_storage_custom_key.rb with the following code

Rails.application.config.to_prepare do
  require 'active_storage/blob'
  ActiveStorage::Blob.class_eval do
    def key
      self[:key] ||= File.join(self.class.generate_unique_secure_token, filename.to_s)
    end
  end
end

This generates keys with the form SZJLX7fLpsv1dzzj2PhRg7ve/my_filename.jpg

0
dezman On

Rails 7 (probably works on versions > 5)

Just to add to the options here... I think it is clean to put a line like this in config/application.rb:

  class Application < Rails::Application
    config.to_prepare do
      ActiveStorage::Blob.include Models::Blob
    end
  end

and then in lib/models/blob.rb:

module Models
  module Blob
    def potato
      'potato'
    end
  end
end

It works!

working monkey patched active storage blob