From 11bf84ca11233a595f88ae38d053029addacaf7f Mon Sep 17 00:00:00 2001 From: Andrew Kodkod <678665+akodkod@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:00:28 +0100 Subject: [PATCH] Allow configuring the engine storage service independently --- README.md | 28 +++++++ .../active_storage_db/files_controller.rb | 3 +- lib/active_storage_db/engine.rb | 3 + spec/requests/configured_service_spec.rb | 80 +++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 spec/requests/configured_service_spec.rb diff --git a/README.md b/README.md index 10f1489..7bdd46f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,34 @@ db: ### Customizations +#### Use DB storage for selected attachments + +When your application uses another service by default, configure the engine's +service separately in `config/application.rb` or an environment configuration: + +```rb +config.active_storage.service = :local # Keep your existing default service +config.active_storage_db.service = :db +``` + +The `active_storage_db.service` value is the name of a DB service in +`config/storage.yml` (a string or symbol), not the adapter name `DB`. +Select that same service on attachments that should use database storage: + +```rb +class Report < ApplicationRecord + has_one_attached :template, service: :db +end +``` + +The engine uses the configured service for downloads and direct uploads without +changing the application's default service. The setting defaults to `nil`, which +preserves the existing behavior of using `ActiveStorage::Blob.service`. +An unknown service name raises the Active Storage registry's configuration error. +This setting selects one service for all engine requests. + +#### Separate database connection + To setup a separate database connection for the `ActiveStorageDB` migrations and files data: 1. Add a different database configuration per environment to `config/database.yml`, e.g: diff --git a/app/controllers/active_storage_db/files_controller.rb b/app/controllers/active_storage_db/files_controller.rb index 6601737..518db4e 100644 --- a/app/controllers/active_storage_db/files_controller.rb +++ b/app/controllers/active_storage_db/files_controller.rb @@ -33,7 +33,8 @@ def acceptable_content?(token) end def db_service - ActiveStorage::Blob.service + service_name = Rails.application.config.active_storage_db.service + service_name ? ActiveStorage::Blob.services.fetch(service_name) : ActiveStorage::Blob.service end def decode_verified_key diff --git a/lib/active_storage_db/engine.rb b/lib/active_storage_db/engine.rb index c847bb1..32410d2 100644 --- a/lib/active_storage_db/engine.rb +++ b/lib/active_storage_db/engine.rb @@ -11,5 +11,8 @@ module ActiveStorageDB class Engine < ::Rails::Engine isolate_namespace ActiveStorageDB + + config.active_storage_db = ActiveSupport::OrderedOptions.new + config.active_storage_db.service = nil end end diff --git a/spec/requests/configured_service_spec.rb b/spec/requests/configured_service_spec.rb new file mode 100644 index 0000000..0fd08e4 --- /dev/null +++ b/spec/requests/configured_service_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +RSpec.describe "Configured engine service" do + let(:data) { "Database attachment" } + let(:disk_service) { ActiveStorage::Blob.services.fetch(:test) } + let(:blob) do + ActiveStorage::Blob.create_and_upload!( + io: StringIO.new(data), filename: "template.txt", content_type: "text/plain", + service_name: "db" + ) + end + + before do + allow(ActiveStorage::Blob).to receive(:service).and_return(disk_service) + if ActiveStorage::Current.respond_to?(:url_options) + allow(ActiveStorage::Current).to receive(:url_options).and_return(protocol: "http://", host: "test.example.com") + else + allow(ActiveStorage::Current).to receive(:host).and_return("http://test.example.com") + end + end + + shared_examples "a configured DB service" do + it "downloads from DB storage without changing the default service", :aggregate_failures do + get blob.url + + expect(response).to have_http_status(:ok) + expect(response.body).to eq(data) + expect(ActiveStorage::Blob.service).to eq(disk_service) + expect(disk_service).not_to exist(blob.key) + end + + it "uploads directly to DB storage without changing the default service", :aggregate_failures do + pending_blob = ActiveStorage::Blob.create_before_direct_upload!( + filename: "template.txt", byte_size: data.bytesize, content_type: "text/plain", + checksum: Digest::MD5.base64digest(data), service_name: "db" + ) + + put pending_blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "text/plain" } + + expect(response).to have_http_status(:no_content) + expect(pending_blob.download).to eq(data) + expect(ActiveStorage::Blob.service).to eq(disk_service) + expect(disk_service).not_to exist(pending_blob.key) + end + end + + context "with a symbol service name" do + before { allow(Rails.application.config.active_storage_db).to receive(:service).and_return(:db) } + + it_behaves_like "a configured DB service" + end + + context "with a string service name" do + before { allow(Rails.application.config.active_storage_db).to receive(:service).and_return("db") } + + it_behaves_like "a configured DB service" + end + + context "without a configured service" do + before do + allow(Rails.application.config.active_storage_db).to receive(:service).and_return(nil) + allow(ActiveStorage::Blob).to receive(:service).and_return(ActiveStorage::Blob.services.fetch(:db)) + end + + it "downloads using the application's default service" do + get blob.url + + expect(response).to have_http_status(:ok) + expect(response.body).to eq(data) + end + end + + context "with an unknown service name" do + before { allow(Rails.application.config.active_storage_db).to receive(:service).and_return(:missing_db) } + + it "raises the registry error instead of falling back" do + expect { get blob.url }.to raise_error(KeyError, /missing_db/) + end + end +end