Active Storage set blob key on direct upload (changelog)

Referencing this PR: https://github.com/rails/rails/commit/4dba136c83cc808282625c0d5b195ce5e0bbaa68 We can now specify a blob key when using the traditional attach method. However, I cannot figure out how to specify the key when using direct uploads. The direct upload method was also updated in this PR, so I’m hoping I’m missing something.

As far as I can tell, if I want to send in a custom key such as user.company/public/filename using direct uploads I…can’t? I’d have to hook in here: https://github.com/rails/rails/blob/b5dac96b59a582c7968b0c33d6ff39d1cfc336dc/activestorage/app/assets/javascripts/activestorage.js#L690 var blob = new BlobRecord(_this.file, checksum, _this.url); Even if I send in a this.key to my direct uploads controller, I don’t want to change the call to BlobRecord and I don’t want to patch activestorage.js b/c that will certainly cause trouble in the future.

I can override key in blob.rb, but I want to prefix based on user.company, so I need to send in the user details somehow.

TLDR: Can I send a key variable with this new PR via direct uploads? I know this was previously not how ASt was designed, but the above PR makes me hopeful there may be a way.

1 Like

Note: On Rails 6.0
I’ve sorted out that to enable changing the key on direct uploads you can’t just override key on blob.rb because the key is being set before def key is called. By the time you hit the def key method self[:key] has already been set by generate_unique_secure_token which IIRC is called from activerecord. Anyway, since I’m only using direct uploads I patched create_before_direct_upload! to force the key in the format I want.

    def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
      key = SecureRandom.base36(28) + "/" + filename
      create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
    end

Would love to hear any other ideas.

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

Is this still the best solution? Or is there something simpler now?

I’d love to just send a key name in the js.

Yes, there are already easier solutions. But for this, it is better to contact the Ruby on rails developers in Factor Dedicated Teams. I work with them myself.