Я использую активное хранилище с 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-система, которая упрощает взаимодействие с базой данных.
Возможно, это не то решение, которое вы ищете, но вы можете рассмотреть возможность использования наследования одной таблицы и определения отдельного класса:
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
Вероятно, вы могли бы переписать это лучше и сделать более динамичным и способным обрабатывать столько сегментов, сколько захотите, но это то, что сработало для меня.