Cloudflare R2 specific ActiveStorage service

We’ve been working on migrating our ActiveStorage provider from AWS S3 to Cloudflare R2. Everything was working correctly, until we got to publicly displaying the images.

You can configure the R2 bucket to use presigned URLs to display images. However, we wanted to use a custom domain on the bucket so that our caching rules would be properly applied.

Note: We don’t have ActiveStorage behind our asset_host, which is used to serve static content from our site, not the dynamic images uploaded by users.

To address this, we created a service that allows a custom domain to be used for our purpose:

require 'active_storage/service'
require 'active_storage/service/s3_service'

module ActiveStorage
  class Service::R2Service < ActiveStorage::Service::S3Service
    def initialize(custom_domain:, **args)
      # Required values for R2’s S3-compatible usage
      args[:force_path_style] = true
      args[:request_checksum_calculation] = "when_required"
      args[:response_checksum_validation] = "when_required"
      args[:region] = "auto"
      super(**args)
      @custom_domain = custom_domain
    end

    def url(key, **options)
      URI.join(@custom_domain, key).to_s
    end
  end
end

There are some other options that may need to be considered, but this is the gist of it.

I was wondering if a PR back into Rails would be helpful?

2 Likes

Since R2 is an “S3 compatible”-flavored service, I’m not sure that it’s necessary to introduce a new Active Storage service class for it (unless there’s a compelling requirement that I’m missing). I tend to think about Rails contributions as: “What’s the minimal change needed to Rails for me to be able to ship a valuable gem/railtie/engine that doesn’t need to monkeypatch Rails code?” Or the alternative “… in order to be able to document this configuration in the Rails Guide?”

Looking at the code you provided, it seems like most of the changes can be made through the configuration parameters in config/storage.yml. But the “custom domain” behavior might be something that would be valuable to upstream (my mental models may be ill-formed, but it could be a new config param that defaults to the current behavior).

There is some prior art that seems related:

I’m not a core team member, so take all of this as just one person’s opinion.