diff --git a/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb index 0b2ce257c8..c72b53f9f7 100644 --- a/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb +++ b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb @@ -2,13 +2,14 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change create_table :active_storage_blobs do |t| - t.string :key, null: false - t.string :filename, null: false + t.string :key, null: false + t.string :filename, null: false t.string :content_type t.text :metadata - t.bigint :byte_size, null: false - t.string :checksum, null: false - t.datetime :created_at, null: false + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false t.index [ :key ], unique: true end diff --git a/actionmailbox/test/dummy/db/schema.rb b/actionmailbox/test/dummy/db/schema.rb index 76c979bcec..478ac1cbdb 100644 --- a/actionmailbox/test/dummy/db/schema.rb +++ b/actionmailbox/test/dummy/db/schema.rb @@ -36,6 +36,7 @@ t.string "filename", null: false t.string "content_type" t.text "metadata" + t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum", null: false t.datetime "created_at", null: false diff --git a/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb b/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb index 0b2ce257c8..c72b53f9f7 100644 --- a/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb +++ b/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb @@ -2,13 +2,14 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change create_table :active_storage_blobs do |t| - t.string :key, null: false - t.string :filename, null: false + t.string :key, null: false + t.string :filename, null: false t.string :content_type t.text :metadata - t.bigint :byte_size, null: false - t.string :checksum, null: false - t.datetime :created_at, null: false + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false t.index [ :key ], unique: true end diff --git a/actiontext/test/dummy/db/schema.rb b/actiontext/test/dummy/db/schema.rb index 03e99b29d2..051c6094dd 100644 --- a/actiontext/test/dummy/db/schema.rb +++ b/actiontext/test/dummy/db/schema.rb @@ -37,6 +37,7 @@ t.string "filename", null: false t.string "content_type" t.text "metadata" + t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum", null: false t.datetime "created_at", null: false diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 781108f839..8061276739 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,17 @@ +* Allow storage services to be configured per attachment + + ```ruby + class User < ActiveRecord::Base + has_one_attached :avatar, service: :s3 + end + + class Gallery < ActiveRecord::Base + has_many_attached :photos, service: :s3 + end + ``` + + *Dmitry Tsepelev* + * You can optionally provide a custom blob key when attaching a new file: ```ruby diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 8d526c2c87..35a7f590e5 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -38,10 +38,24 @@ class ActiveStorage::Blob < ActiveRecord::Base scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) } + after_initialize do + self.service_name ||= Rails.configuration.active_storage.service + end + before_destroy(prepend: true) do raise ActiveRecord::InvalidForeignKey if attachments.exists? end + validates :service_name, presence: true + + validate do + if service_name_changed? && service_name + ActiveStorage::ServiceRegistry.fetch(service_name) do + errors.add(:service_name, :invalid) + end + end + end + class << self # You can use the signed ID of a blob to refer to it on the client side without fear of tampering. # This is particularly helpful for direct uploads where the client-side needs to refer to the blob @@ -52,22 +66,22 @@ def find_signed(id, record: nil) find ActiveStorage.verifier.verify(id, purpose: :blob_id) end - def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc: - new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob| + def build_after_upload(io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc: + new(filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob| blob.upload(io, identify: identify) end end deprecate :build_after_upload - def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc: - new(key: key, filename: filename, content_type: content_type, metadata: metadata).tap do |blob| + def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc: + new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob| blob.unfurl(io, identify: identify) end end - def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc: - build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!) + def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc: + build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!) end # Creates a new blob instance and then uploads the contents of @@ -75,8 +89,8 @@ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadat # be saved before the upload begins to prevent the upload clobbering another due to key collisions. # When providing a content type, pass identify: false to bypass # automatic content type inference. - def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) - create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap do |blob| + def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) + create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob| blob.upload_without_unfurling(io) end end @@ -89,8 +103,8 @@ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: ni # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob. # Once the form using the direct upload is submitted, the blob can be associated with the right record using # the signed ID. - def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, record: nil) - create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata + def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil) + create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name end # To prevent problems with case-insensitive filesystems, especially in combination @@ -248,6 +262,11 @@ def purge_later ActiveStorage::PurgeJob.perform_later(self) end + # Returns an instance of service, which can be configured globally or per attachment + def service + ActiveStorage::ServiceRegistry.fetch(service_name) + end + private def compute_checksum_in_chunks(io) Digest::MD5.new.tap do |checksum| diff --git a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb index cfaf01cd5e..8b469badac 100644 --- a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb +++ b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb @@ -1,13 +1,14 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2] def change create_table :active_storage_blobs do |t| - t.string :key, null: false - t.string :filename, null: false + t.string :key, null: false + t.string :filename, null: false t.string :content_type t.text :metadata - t.bigint :byte_size, null: false - t.string :checksum, null: false - t.datetime :created_at, null: false + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false t.index [ :key ], unique: true end diff --git a/activestorage/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb b/activestorage/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb new file mode 100644 index 0000000000..1b3949e35d --- /dev/null +++ b/activestorage/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb @@ -0,0 +1,13 @@ +class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] + def up + unless column_exists?(:active_storage_blobs, :service_name) + add_column :active_storage_blobs, :service_name, :string + + if configured_service = Rails.configuration.active_storage.service + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) + end + + change_column :active_storage_blobs, :service_name, :string, null: false + end + end +end diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index c35a9920d6..88b78f400e 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -38,6 +38,7 @@ module ActiveStorage autoload :Attached autoload :Service + autoload :ServiceRegistry autoload :Previewer autoload :Analyzer diff --git a/activestorage/lib/active_storage/attached/changes/create_one.rb b/activestorage/lib/active_storage/attached/changes/create_one.rb index bade4ba496..420ea04547 100644 --- a/activestorage/lib/active_storage/attached/changes/create_one.rb +++ b/activestorage/lib/active_storage/attached/changes/create_one.rb @@ -58,14 +58,20 @@ def find_or_build_blob filename: attachable.original_filename, content_type: attachable.content_type, record: record, + service_name: attachment_service_name ) when Hash - ActiveStorage::Blob.build_after_unfurling(**attachable.merge(record: record)) + args = attachable.reverse_merge(record: record, service_name: attachment_service_name) + ActiveStorage::Blob.build_after_unfurling(**args) when String ActiveStorage::Blob.find_signed(attachable, record: record) else raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}" end end + + def attachment_service_name + record.attachment_reflections[name].options[:service_name] + end end end diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb index b531186a34..f5fed48582 100644 --- a/activestorage/lib/active_storage/attached/model.rb +++ b/activestorage/lib/active_storage/attached/model.rb @@ -32,7 +32,17 @@ module Attached::Model # # If the +:dependent+ option isn't set, the attachment will be purged # (i.e. destroyed) whenever the record is destroyed. - def has_one_attached(name, dependent: :purge_later) + # + # If you need the attachment to use a service which differs from the globally configured one, + # pass the +:service+ option. For instance: + # + # class User < ActiveRecord::Base + # has_one_attached :avatar, service: :s3 + # end + # + def has_one_attached(name, dependent: :purge_later, service: nil) + validate_service_configuration(name, service) + generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name} @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self) @@ -57,11 +67,14 @@ def #{name}=(attachable) after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) } - ActiveRecord::Reflection.add_attachment_reflection( - self, + reflection = ActiveRecord::Reflection.create( + :has_one_attached, name, - ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self) + nil, + { dependent: dependent, service_name: service }, + self ) + ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection) end # Specifies the relation between multiple attachments and the model. @@ -88,7 +101,17 @@ def #{name}=(attachable) # # If the +:dependent+ option isn't set, all the attachments will be purged # (i.e. destroyed) whenever the record is destroyed. - def has_many_attached(name, dependent: :purge_later) + # + # If you need the attachment to use a service which differs from the globally configured one, + # pass the +:service+ option. For instance: + # + # class Gallery < ActiveRecord::Base + # has_many_attached :photos, service: :s3 + # end + # + def has_many_attached(name, dependent: :purge_later, service: nil) + validate_service_configuration(name, service) + generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name} @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self) @@ -130,12 +153,24 @@ def purge_later after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) } - ActiveRecord::Reflection.add_attachment_reflection( - self, + reflection = ActiveRecord::Reflection.create( + :has_many_attached, name, - ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self) + nil, + { dependent: dependent, service_name: service }, + self ) + ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection) end + + private + def validate_service_configuration(association_name, service) + return unless service + + ServiceRegistry.fetch(service) do + raise ArgumentError, "Cannot configure service :#{service} for #{name}##{association_name}" + end + end end def attachment_changes #:nodoc: diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 9d9cd02d12..4a1ce0fc0c 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -102,26 +102,9 @@ class Engine < Rails::Engine # :nodoc: initializer "active_storage.services" do ActiveSupport.on_load(:active_storage_blob) do if config_choice = Rails.configuration.active_storage.service - configs = Rails.configuration.active_storage.service_configurations ||= begin - config_file = Pathname.new(Rails.root.join("config/storage.yml")) - raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist? - - require "yaml" - require "erb" - - YAML.load(ERB.new(config_file.read).result) || {} - rescue Psych::SyntaxError => e - raise "YAML syntax error occurred while parsing #{config_file}. " \ - "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ - "Error: #{e.message}" + ActiveStorage::Blob.service = ActiveStorage::ServiceRegistry.fetch(config_choice) do + raise ArgumentError, "Cannot load `Rails.application.config.active_storage.service`:\n#{config_choice}" end - - ActiveStorage::Blob.service = - begin - ActiveStorage::Service.configure config_choice, configs - rescue => e - raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace - end end end end diff --git a/activestorage/lib/active_storage/service_registry.rb b/activestorage/lib/active_storage/service_registry.rb new file mode 100644 index 0000000000..b08b6ec194 --- /dev/null +++ b/activestorage/lib/active_storage/service_registry.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActiveStorage + class ServiceRegistry #:nodoc: + class << self + def fetch(service_name, &block) + services.fetch(service_name.to_s, &block) + end + + private + def services + @services ||= configs.keys.each_with_object({}) do |service_name, hash| + hash[service_name] = ActiveStorage::Service.configure(service_name, configs) + end + end + + def configs + Rails.configuration.active_storage.service_configurations ||= begin + config_file = Pathname.new(Rails.root.join("config/storage.yml")) + raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist? + + require "yaml" + require "erb" + + YAML.load(ERB.new(config_file.read).result) || {} + rescue Psych::SyntaxError => e + raise "YAML syntax error occurred while parsing #{config_file}. " \ + "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ + "Error: #{e.message}" + end + end + end + end +end diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb index 76e1d18b88..d9434ec880 100644 --- a/activestorage/test/models/attached/many_test.rb +++ b/activestorage/test/models/attached/many_test.rb @@ -578,6 +578,36 @@ def highlights end end + test "attaching a new blob from a Hash with a custom service" do + with_service("mirror") do + @user.highlights.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + @user.vlogs.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + + assert_instance_of ActiveStorage::Service::MirrorService, @user.highlights.first.service + assert_instance_of ActiveStorage::Service::DiskService, @user.vlogs.first.service + end + end + + test "attaching a new blob from an uploaded file with a custom_ ervice" do + with_service("mirror") do + @user.highlights.attach fixture_file_upload("racecar.jpg") + @user.vlogs.attach fixture_file_upload("racecar.jpg") + + assert_instance_of ActiveStorage::Service::MirrorService, @user.highlights.first.service + assert_instance_of ActiveStorage::Service::DiskService, @user.vlogs.first.service + end + end + + test "raises error when misconfigured service is passed" do + error = assert_raises ArgumentError do + User.class_eval do + has_many_attached :featured_photos, service: :unknown + end + end + + assert_match(/Cannot configure service :unknown for User#featured_photos/, error.message) + end + private def append_on_assign ActiveStorage.replace_on_assign_to_many, previous = false, ActiveStorage.replace_on_assign_to_many diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb index 5a68e65d9b..748374c5e4 100644 --- a/activestorage/test/models/attached/one_test.rb +++ b/activestorage/test/models/attached/one_test.rb @@ -43,7 +43,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase test "attaching a new blob from a Hash to an existing record passes record" do hash = { io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" } blob = ActiveStorage::Blob.build_after_unfurling(**hash) - arguments = [hash.merge(record: @user)] + arguments = [hash.merge(record: @user, service_name: nil)] assert_called_with(ActiveStorage::Blob, :build_after_unfurling, arguments, returns: blob) do @user.avatar.attach hash end @@ -59,7 +59,7 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase def upload.open @io ||= StringIO.new("") end - arguments = { io: upload.open, filename: upload.original_filename, content_type: upload.content_type, record: @user } + arguments = { io: upload.open, filename: upload.original_filename, content_type: upload.content_type, record: @user, service_name: nil } blob = ActiveStorage::Blob.build_after_unfurling(**arguments) assert_called_with(ActiveStorage::Blob, :build_after_unfurling, [arguments], returns: blob) do @user.avatar.attach upload @@ -544,4 +544,34 @@ def avatar User.remove_method :avatar end end + + test "attaching a new blob from a Hash with a custom service" do + with_service("mirror") do + @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + @user.cover_photo.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" + + assert_instance_of ActiveStorage::Service::MirrorService, @user.avatar.service + assert_instance_of ActiveStorage::Service::DiskService, @user.cover_photo.service + end + end + + test "attaching a new blob from an uploaded file with a custom_service" do + with_service("mirror") do + @user.avatar.attach fixture_file_upload("racecar.jpg") + @user.cover_photo.attach fixture_file_upload("racecar.jpg") + + assert_instance_of ActiveStorage::Service::MirrorService, @user.avatar.service + assert_instance_of ActiveStorage::Service::DiskService, @user.cover_photo.service + end + end + + test "raises error when misconfigured service is passed" do + error = assert_raises ArgumentError do + User.class_eval do + has_one_attached :featured_photo, service: :unknown + end + end + + assert_match(/Cannot configure service :unknown for User#featured_photo/, error.message) + end end diff --git a/activestorage/test/models/attachment_test.rb b/activestorage/test/models/attachment_test.rb index 94f354d116..16a9004ff1 100644 --- a/activestorage/test/models/attachment_test.rb +++ b/activestorage/test/models/attachment_test.rb @@ -26,28 +26,15 @@ class ActiveStorage::AttachmentTest < ActiveSupport::TestCase end test "mirroring a directly-uploaded blob after attaching it" do - previous_service, ActiveStorage::Blob.service = ActiveStorage::Blob.service, build_mirror_service + with_service("mirror") do + blob = directly_upload_file_blob + assert_not ActiveStorage::Blob.service.mirrors.second.exist?(blob.key) - blob = directly_upload_file_blob - assert_not ActiveStorage::Blob.service.mirrors.second.exist?(blob.key) + perform_enqueued_jobs do + @user.highlights.attach(blob) + end - perform_enqueued_jobs do - @user.highlights.attach(blob) + assert ActiveStorage::Blob.service.mirrors.second.exist?(blob.key) end - - assert ActiveStorage::Blob.service.mirrors.second.exist?(blob.key) - ensure - ActiveStorage::Blob.service = previous_service end - - private - def build_mirror_service - ActiveStorage::Service::MirrorService.new \ - primary: build_disk_service("primary"), - mirrors: 3.times.collect { |i| build_disk_service("mirror_#{i + 1}") } - end - - def build_disk_service(purpose) - ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests_#{purpose}")) - end end diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index b9dff6bbcd..290dd70419 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -245,6 +245,21 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase end end + test "uses service from blob when provided" do + with_service("mirror") do + blob = create_blob(filename: "funky.jpg", service_name: :local) + assert_instance_of ActiveStorage::Service::DiskService, blob.service + end + end + + test "invalidates record when provided service_name is invalid" do + blob = create_blob(filename: "funky.jpg") + blob.update(service_name: :unknown) + + assert_not blob.valid? + assert_equal ["is invalid"], blob.errors[:service_name] + end + private def expected_url_for(blob, disposition: :attachment, filename: nil, content_type: nil) filename ||= blob.filename diff --git a/activestorage/test/models/reflection_test.rb b/activestorage/test/models/reflection_test.rb index 98606b0617..d3f8b39cad 100644 --- a/activestorage/test/models/reflection_test.rb +++ b/activestorage/test/models/reflection_test.rb @@ -9,6 +9,9 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase assert_equal :avatar, reflection.name assert_equal :has_one_attached, reflection.macro assert_equal :purge_later, reflection.options[:dependent] + + reflection = User.reflect_on_attachment(:cover_photo) + assert_equal :local, reflection.options[:service_name] end test "reflection on a singular attachment with the same name as an attachment on another model" do @@ -22,6 +25,9 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase assert_equal :highlights, reflection.name assert_equal :has_many_attached, reflection.macro assert_equal :purge_later, reflection.options[:dependent] + + reflection = User.reflect_on_attachment(:vlogs) + assert_equal :local, reflection.options[:service_name] end test "reflecting on all attachments" do diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index b0a39ae3af..bfe7d16e93 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -34,7 +34,16 @@ end require "tmpdir" -ActiveStorage::Blob.service = ActiveStorage::Service::DiskService.new(root: Dir.mktmpdir("active_storage_tests")) + +Rails.configuration.active_storage.service_configurations = { + "local" => { "service" => "Disk", "root" => Dir.mktmpdir("active_storage_tests") }, + "disk_mirror_1" => { "service" => "Disk", "root" => Dir.mktmpdir("active_storage_tests_1") }, + "disk_mirror_2" => { "service" => "Disk", "root" => Dir.mktmpdir("active_storage_tests_2") }, + "disk_mirror_3" => { "service" => "Disk", "root" => Dir.mktmpdir("active_storage_tests_3") }, + "mirror" => { "service" => "Mirror", "primary" => "local", "mirrors" => ["disk_mirror_1", "disk_mirror_2", "disk_mirror_3"] } +} + +Rails.configuration.active_storage.service = "local" ActiveStorage.logger = ActiveSupport::Logger.new(nil) ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing") @@ -51,8 +60,8 @@ class ActiveSupport::TestCase end private - def create_blob(key: nil, data: "Hello world!", filename: "hello.txt", content_type: "text/plain", identify: true, record: nil) - ActiveStorage::Blob.create_and_upload! key: key, io: StringIO.new(data), filename: filename, content_type: content_type, identify: identify, record: record + def create_blob(key: nil, data: "Hello world!", filename: "hello.txt", content_type: "text/plain", identify: true, service_name: nil, record: nil) + ActiveStorage::Blob.create_and_upload! key: key, io: StringIO.new(data), filename: filename, content_type: content_type, identify: identify, service_name: service_name, record: record end def create_file_blob(key: nil, filename: "racecar.jpg", content_type: "image/jpeg", metadata: nil, record: nil) @@ -89,6 +98,18 @@ def extract_metadata_from(blob) def fixture_file_upload(filename) Rack::Test::UploadedFile.new file_fixture(filename).to_s end + + def with_service(service_name) + service = ActiveStorage::ServiceRegistry.fetch(service_name) + ActiveStorage::Blob.service, previous_service = service, ActiveStorage::Blob.service + + Rails.configuration.active_storage.service, previous_service_name = service_name, Rails.configuration.active_storage.service + + yield + ensure + ActiveStorage::Blob.service = previous_service + Rails.configuration.active_storage.service = previous_service_name + end end require "global_id" @@ -99,10 +120,10 @@ class User < ActiveRecord::Base validates :name, presence: true has_one_attached :avatar - has_one_attached :cover_photo, dependent: false + has_one_attached :cover_photo, dependent: false, service: :local has_many_attached :highlights - has_many_attached :vlogs, dependent: false + has_many_attached :vlogs, dependent: false, service: :local end class Group < ActiveRecord::Base diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index a78373f732..6c3eb42599 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -287,6 +287,15 @@ Call `avatar.attached?` to determine whether a particular user has an avatar: user.avatar.attached? ``` +In some cases you might want to override a default service for a specific attachment. +You can configure specific services per attachment using the `service` option: + +```ruby +class User < ApplicationRecord + has_one_attached :avatar, service: :s3 +end +``` + ### `has_many_attached` The `has_many_attached` macro sets up a one-to-many relationship between records @@ -329,6 +338,14 @@ Call `images.attached?` to determine whether a particular message has any images @message.images.attached? ``` +Overriding the default service is done done the same way as `has_one_attached`, by using the `service` option: + +```ruby +class Message < ApplicationRecord + has_many_attached :images, service: :s3 +end +``` + ### Attaching File/IO Objects Sometimes you need to attach a file that doesn’t arrive via an HTTP request.