During my experimenting with ActiveStorage I came to learn that while it’s been in Rails since version 5.2, it still lacks both validations and callbacks for you attached blobs. Luckily the there’s a community maintained gem with some basic validations, called active_storage_validations, the callbacks you have to add on your own.

To understand the challenge, let us first look at the flow of attaching an ActiveStorage blob to a new ActiveRecord model (let’s call it User):

  1. Blob gets uploaded
  2. Blob is attached to a in-memory instance of the new User
  3. If the User object is valid, it, and the Blob gets persisted.
  4. Asynchronously, the Analyzer completes it work, and updates the Blob with new metadata.

I want my user to be notified after the blob gets updated with new metadata, so I can refresh the Elasticsearch index with the new information. And while the association model between my User and Blob, ActiveStorage::Attachment has a belongs_to :record, touch: true, nothing happens if a blob gets updated.

So let us utilize some of the magic in Ruby, and add some functionality to ActiveStorage::Blob:

Rails.configuration.to_prepare do
  module ActiveStorageTouchRecordsAfterAnalyze
    # Gives us some convenient shortcuts, like `prepended`
    extend ActiveSupport::Concern

    # When prepended into a class, define our callback
    prepended do
      after_update_commit :touch_all_records
    end

    # Iterate all attached records, and touch them
    def touch_all_records
      attachments.each do |attachment|
        # there's a theoretical chance that the record is missing,
        # therefore I .try() the touch.
        attachment.record.try(:touch)
      end
    end

  end

  # After defining the module, call on ActiveStorage::Blob to prepend it in.
  ActiveStorage::Blob.prepend ActiveStorageTouchRecordsAfterAnalyze
end

The above code goes into config/initializers/active_storage_touch_records_after_analyze.rb. After a reload, you can verify that it’s working by calling .analyze on one of your blobs.

to_prepare: Run after the initializers are run for all Railties (including the application itself), but before eager loading and the middleware stack is built. More importantly, will run upon every code reload in development, but only once (during boot-up) in production and test.

Note that I’m using a .to_prepare block around this initializer. This ensures that every bit of Rails is loaded enough, so I can safely inject my code. It also gives the benefit of being reloaded in development, which is quite helpful when making changes to initializers. For more in-depth information on the Rails boot-up stages, check out the official documentation.