[ActiveStorage] OverlayService to layer services for development purposes

Sometimes, I need to load a backup of the production database on my local environment.

All production blobs are stored using ActiveStorage on S3, so I have two options when doing this: either use the ActiveStorage production configuration and risk touching the production S3 bucket, or use a local DiskService but all the blobs existing in database will trigger an error.

To solve this issue, I created a new ActiveStorage::Service with a semantic similar to OverlayFS. You can configure an upper_layer where all write operations will be done, and a list of lower layers where all read operations will be delegated to (after first checking for the upper layer).

Here is the first version of the code for this service and I would very much appreciate feedbacks on the idea or the code.

class ActiveStorage::Service::OverlayService < ActiveStorage::Service
  attr_reader :upper_layer, :lower_layers

  def self.build(upper_layer:, lower_layers:, configurator:, **options) #:nodoc:
    new \
      upper_layer: configurator.build(upper_layer),
      lower_layers: lower_layers.map { |name| configurator.build name }
  end

  def initialize(upper_layer:, lower_layers:)
    @upper_layer, @lower_layers = upper_layer, lower_layers
  end

  delegate :upload, :update_metadata, :delete, :delete_prefixed, :url_for_direct_upload, :headers_for_direct_upload, to: :upper_layer

  def exist?(key)
    first_service_for(key) ? true : false
  end

  def download(key, &block)
    send_to_first_service :download, key, &block
  end

  def download_chunk(key, range)
    send_to_first_service :download_chunk, key, range
  end

  def url(key, **options)
    send_to_first_service :url, key, **options
  end

  def path_for(key)
    send_to_first_service :path_for, key
  end

  private
    def each_service(&block)
      [ upper_layer, *lower_layers ].each(&block)
    end

    def first_service_for(key)
      each_service do |service|
        return service if service.exist? key
      end
      nil
    end

    def send_to_first_service(method, key, *args, &block)
      service = first_service_for(key)
      service.public_send method, key, *args, &block if service
    end
end

It is configured in storage.yml in the same way as a Mirror service:

local_over_amazon:
  service: Overlay
  upper_layer: local
  lower_layers:
    - amazon
1 Like

This code is compatible with Rails 5.2 but does not seem to work with Rails 7.