Динамически менять службу активного хранилища на модели рельсов?

Я использую активное хранилище с s3, чтобы прикрепить файл к модели под названием Document. Мне нужно добавить поддержку пользователей из ЕС, которые хотят, чтобы их файлы документов хранились в корзине s3 в ЕС.

У меня есть файл Storage.yml, настроенный следующим образом:

amazon:
    service: S3
    access_key_id: <%= ENV['S3_ACCESS_KEY_ID'] %>
    secret_access_key: <%= ENV['S3_SECRET_KEY_ACCESS'] %>
    region: <%= ENV['S3_REGION_EU'] %>
    bucket: <%= ENV['S3_BUCKET_NAME'] %>

amazon_eu:
    service: S3
    access_key_id: <%= ENV['S3_ACCESS_KEY_ID'] %>
    secret_access_key: <%= ENV['S3_SECRET_KEY_ACCESS'] %>
    region: <%= ENV['S3_REGION_EU'] %>
    bucket: <%= ENV['S3_BUCKET_NAME_EU'] %>

Есть ли способ динамически переключать службу в модели документа на основе региона, установленного в учетной записи? Что-то вроде:

class Document < ApplicationRecord
  belongs_to :account
  
  if account.region == 'eu'
    has_one_attached :file, service: amazon_eu
  else
    has_one_attached :file, service: amazon
  end

end

Или вообще указать конкретную службу, которую я хочу использовать динамически во время выполнения?

🤔 А знаете ли вы, что...
Одной из ключевых особенностей Rails является активная запись (Active Record) - ORM-система, которая упрощает взаимодействие с базой данных.


2
54
2

Ответы:

Возможно, это не то решение, которое вы ищете, но вы можете рассмотреть возможность использования наследования одной таблицы и определения отдельного класса:

class EUDocument < Document
  belongs_to :account
  has_one_attached :file, service: :amazon_eu
end

ActiveStorage построен на основе макроса уровня класса и на самом деле не имеет каких-либо документированных функций, таких как реконфигурация уровня экземпляра.

Хотя вы можете делать сумасшедшие хаки, например использовать class_eval или заглушать методы экземпляра, сгенерированные макросом, это будет очень хрупко.

Если это является препятствием, вы можете рассмотреть возможность использования драгоценного камня S3 напрямую для реализации этой функции, поскольку ActiveStorage — это очень самоуверенный дизайн, который в то же время пытается быть сверхгибким.


Решено

Я понял, как это сделать. Я создал специальную службу для активного хранилища, которая унаследована от службы s3, и создал второй экземпляр сегмента в классе для региона ЕС, который я хочу использовать.

# lib/active_storage/service/dynamic_storage_service.rb
require "active_storage/service/s3_service"

module ActiveStorage
  class Service::DynamicStorageService < ActiveStorage::Service::S3Service
    # create attributes for another client and bucket
    attr_reader :client_eu, :bucket_eu
    

    # override the initializer with options to pass in an eu bucket from the storage.yml
    # this is where you would create whatever extra buckets you need
    def initialize(bucket:, bucket_eu:, upload: {}, public: false, **options)
      eu_options = options.except(:region)
      eu_options[:region] = eu_options[:region_eu]

      @client_eu = Aws::S3::Resource.new(eu_options.except(:region_eu))
      @bucket_eu = @client_eu.bucket(bucket_eu)

      super(bucket: bucket, upload: upload, public: public, **options.except(:region_eu))
    end

    private
      # override the method where the bucket is used
      # this is where you would add whatever logic you need to select the bucket
      # this is the implementation that works for me
      # the method just needs to return the S3 object and everything else will work
      def object_for(key)
        # this is how you get the record based on the key
        document_id = ActiveStorage::Attachment.find_by(blob_id: ActiveStorage::Blob.find_by(key: key).id).record_id

        document = Document.find(document_id)

        if document.account.region == 'eu'
          return bucket_eu.object(key)
        else
          return bucket.object(key)
        end
      end
  end
end

И тогда вы будете использовать его в Storage.yml вместо обычного сервиса S3:

# config/storage.yml
amazon:
    service: DynamicStorage
    access_key_id: <%= ENV['S3_ACCESS_KEY_ID'] %>
    secret_access_key: <%= ENV['S3_SECRET_KEY_ACCESS'] %>
    region: <%= ENV['S3_REGION'] %>
    bucket: <%= ENV['S3_BUCKET_NAME'] %>
    region_eu: <%= ENV['S3_REGION_EU'] %>
    bucket_eu: <%= ENV['S3_BUCKET_NAME_EU'] %>

И Document.rb может работать как обычно.

# document.rb
class Document < ApplicationRecord
  belongs_to :account
  
  has_one_attached :file

end

Вероятно, вы могли бы переписать это лучше и сделать более динамичным и способным обрабатывать столько сегментов, сколько захотите, но это то, что сработало для меня.