Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/rails_multisite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
require 'rails_multisite/connection_management'
require 'rails_multisite/middleware'
require 'rails_multisite/cookie_salt'
require 'rails_multisite/action_view_helper'
13 changes: 13 additions & 0 deletions lib/rails_multisite/action_view_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module RailsMultisite
module ActionViewHelper
def config=(value)
value.define_singleton_method(:relative_url_root) do
RailsMultisite::ConnectionManagement.current_path_prefix
end

super(value)
end
end
end
70 changes: 64 additions & 6 deletions lib/rails_multisite/connection_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ class ConnectionManagement
attr_reader :config_filename, :db_spec_cache

class << self
attr_accessor :asset_hostnames
attr_accessor :asset_hostnames, :default_path_prefix

delegate :all_dbs,
:config_filename,
:connection_spec,
:current_db,
:current_path_prefix,
:default_connection_handler=,
:each_connection,
:establish_connection,
Expand Down Expand Up @@ -105,6 +106,7 @@ def load_config!
raise ArgumentError.new("Please do not name any db default!")
end
v[:db_key] = k
v[:path_prefix] = normalize_path_prefix(v["path_prefix"])
v[:prepared_statements] = false if no_prepared_statements
end

Expand All @@ -117,20 +119,42 @@ def load_config!
end
end

# Build a hash of hostname => spec
# Build a hash of hostname => spec (hostname-only sites only; path-prefix
# sites are excluded so unknown prefixes produce 404 rather than a wrong match)
new_host_spec_cache = {}
configs.each do |k, v|
next unless v["host_names"]
next if v[:path_prefix]
v["host_names"].each do |host|
new_host_spec_cache[host] = new_db_spec_cache[k]
end
end

# Add the default hostnames as well
@default_spec.config[:host_names].each do |host|
new_host_spec_cache[host] = @default_spec
# Add the default hostnames (unless default has a path prefix)
unless normalize_path_prefix(self.class.default_path_prefix)
@default_spec.config[:host_names].each do |host|
new_host_spec_cache[host] = @default_spec
end
end

# Build a hash of "hostname/path_prefix" => spec, sorted longest-key-first
new_path_spec_cache = {}
configs.each do |k, v|
next unless v["host_names"] && v[:path_prefix]
v["host_names"].each do |host|
new_path_spec_cache["#{host}#{v[:path_prefix]}"] = new_db_spec_cache[k]
end
end

@default_path_prefix = normalize_path_prefix(self.class.default_path_prefix)
if @default_path_prefix
@default_spec.config[:host_names].each do |host|
new_path_spec_cache["#{host}#{@default_path_prefix}"] = @default_spec
end
end

@path_spec_cache = new_path_spec_cache.sort_by { |k, _| -k.length }.to_h

removed_dbs = db_spec_cache.keys - new_db_spec_cache.keys
removed_specs = db_spec_cache.values_at(*removed_dbs)

Expand Down Expand Up @@ -195,6 +219,8 @@ def with_hostname(hostname)
rval
end

# TODO: add with_path_prefix or a composite key so that applications quickly switch

def with_connection(db = DEFAULT)
old = current_db
connected = ActiveRecord::Base.connection_pool.connected?
Expand Down Expand Up @@ -305,7 +331,32 @@ def host(env)
end

def connection_spec(opts)
opts[:host] ? @host_spec_cache[opts[:host]] : db_spec_cache[opts[:db]]
# if a key is provided (matching hostname/path_prefix or hostname), use it to find the spec directly
if opts[:key]
found_spec = @path_spec_cache[opts[:key]] || @host_spec_cache[opts[:key]]
return found_spec if found_spec
end

if opts[:host]
path_info = opts[:path]
if path_info && path_info.length > 1
request_path = "#{opts[:host]}#{path_info}"
@path_spec_cache.each do |composite_key, spec|
if composite_key == request_path || request_path.start_with?("#{composite_key}/")
return spec
end
end
end

@host_spec_cache[opts[:host]]
else
db_spec_cache[opts[:db]]
end
end

def current_path_prefix
return @default_path_prefix if current_db == DEFAULT
ConnectionSpecification.current.config[:path_prefix]
end

def clear_settings!
Expand All @@ -329,5 +380,12 @@ def default_connection_handler=(connection_handler)
def handler_key(spec)
self.class.handler_key(spec)
end

def normalize_path_prefix(raw)
return nil if raw.nil? || raw.to_s.strip.empty?
prefix = raw.to_s.strip
prefix = "/#{prefix}" unless prefix.start_with?("/")
prefix.chomp("/")
end
end
end
8 changes: 8 additions & 0 deletions lib/rails_multisite/connection_management/null_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def current_db
DEFAULT
end

def current_path_prefix
nil
end

def default_path_prefix
nil
end

def each_connection(_opts = nil, &blk)
with_connection(&blk)
end
Expand Down
18 changes: 17 additions & 1 deletion lib/rails_multisite/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ def call(env)
host = ConnectionManagement.host(env)
db = nil
begin
key = env["HTTP_MULTISITE_ROUTE"] if env["HTTP_MULTISITE_ROUTE"].present?
spec = ConnectionManagement.connection_spec(host: host, path: env["PATH_INFO"], key: key)

unless ConnectionManagement.connection_spec(host: host)
unless spec
db = @db_lookup && @db_lookup.call(env)
if db
host = nil
Expand All @@ -20,6 +22,20 @@ def call(env)
end
end

matched_prefix = spec&.config&.fetch(:path_prefix, nil) ||
(spec && !spec.config[:db_key] ? ConnectionManagement.default_path_prefix : nil)

if matched_prefix
path_info = env["PATH_INFO"].to_s
env["SCRIPT_NAME"] = env["SCRIPT_NAME"].to_s + matched_prefix
env["PATH_INFO"] = path_info[matched_prefix.length..]
env["PATH_INFO"] = "/" if env["PATH_INFO"].nil? || env["PATH_INFO"].empty?
if spec.config[:db_key]
db = spec.config[:db_key]
host = nil
end
end

ActiveRecord::Base.connection_handler.clear_active_connections!
ConnectionManagement.establish_connection(host: host, db: db)
CookieSalt.update_cookie_salts(env: env, host: host)
Expand Down
21 changes: 18 additions & 3 deletions lib/rails_multisite/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,38 @@ class Railtie < Rails::Railtie

config_file ||= ConnectionManagement.default_config_filename

default_path_prefix =
app.config.respond_to?(:multisite_default_path_prefix) &&
app.config.multisite_default_path_prefix.presence

if File.exist?(config_file)
ConnectionManagement.default_path_prefix = default_path_prefix
ConnectionManagement.config_filename = config_file
app.config.multisite = true
Rails.logger.formatter = RailsMultisite::Formatter.new
Rails.logger.formatter = RailsMultisite::Formatter.new if Rails.logger

if !skip_middleware?(app.config)
if !RailsMultisite::Railtie.skip_middleware?(app.config)
app.middleware.insert_after(ActionDispatch::Executor, RailsMultisite::Middleware)
app.middleware.delete(ActionDispatch::Executor)
end

ActiveSupport.on_load(:action_controller_base) do
self.config.define_singleton_method(:relative_url_root) do
RailsMultisite::ConnectionManagement.current_path_prefix
end
end
ActiveSupport.on_load(:action_view) do
# config is only set when the ActionView template is needed, prepend a setter.
prepend RailsMultisite::ActionViewHelper
end

if ENV['RAILS_DB'].present?
ConnectionManagement.establish_connection(db: ENV['RAILS_DB'], raise_on_missing: true)
end
end
end

def skip_middleware?(config)
def self.skip_middleware?(config)
return false if !config.respond_to?(:skip_multisite_middleware)
config.skip_multisite_middleware
end
Expand Down
89 changes: 89 additions & 0 deletions spec/connection_management_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ def with_connection(db)
expect(conn.all_dbs).to eq(['default', 'second'])
end

it 'finds spec by hostname key' do
spec = conn.connection_spec(key: 'second.localhost')
expect(spec).not_to be_nil
expect(spec.config[:db_key]).to eq('second')
end

it 'returns nil for an unknown hostname key' do
spec = conn.connection_spec(key: 'unknown.localhost')
expect(spec).to be_nil
end

context 'with second db' do
it "is configured correctly" do
with_connection('second') do
Expand Down Expand Up @@ -235,6 +246,84 @@ def with_connection(db)
end
end

describe 'path-prefix routing' do
before do
conn.config_filename = fixture_path('two_dbs_path_prefix.yml')
end

it 'finds site_a spec by host + path prefix' do
spec = conn.connection_spec(host: 'example.localhost', path: '/site_a/posts')
expect(spec).not_to be_nil
expect(spec.config[:db_key]).to eq('site_a')
end

it 'finds site_b spec by host + path prefix' do
spec = conn.connection_spec(host: 'example.localhost', path: '/site_b/topics/1')
expect(spec).not_to be_nil
expect(spec.config[:db_key]).to eq('site_b')
end

it 'matches when PATH_INFO equals the prefix exactly' do
spec = conn.connection_spec(host: 'example.localhost', path: '/site_a')
expect(spec.config[:db_key]).to eq('site_a')
end

it 'returns nil for unknown prefix when all sites on that host use path prefixes' do
spec = conn.connection_spec(host: 'example.localhost', path: '/unknown/page')
expect(spec).to be_nil
end

it 'finds site_a spec by composite key (host/path_prefix)' do
spec = conn.connection_spec(key: 'example.localhost/site_a')
expect(spec).not_to be_nil
expect(spec.config[:db_key]).to eq('site_a')
end

it 'finds site_b spec by composite key (host/path_prefix)' do
spec = conn.connection_spec(key: 'example.localhost/site_b')
expect(spec).not_to be_nil
expect(spec.config[:db_key]).to eq('site_b')
end

it 'returns nil for an unknown composite key' do
spec = conn.connection_spec(key: 'example.localhost/unknown')
expect(spec).to be_nil
end

it 'stores the normalized path_prefix on the spec config' do
spec = conn.connection_spec(host: 'example.localhost', path: '/site_a/posts')
expect(spec.config[:path_prefix]).to eq('/site_a')
end

it 'returns nil for current_path_prefix on a hostname-only site' do
conn.config_filename = fixture_path('two_dbs.yml')
conn.establish_connection(host: 'second.localhost')
expect(conn.current_path_prefix).to be_nil
end

it 'returns the path prefix for current_path_prefix when connected to a path-prefix site' do
with_connection('site_a') do
expect(conn.current_path_prefix).to eq('/site_a')
end
end

it 'returns nil for current_path_prefix when on the default db with no default path prefix' do
with_connection('default') do
expect(conn.current_path_prefix).to be_nil
end
end

it 'returns the default path prefix for current_path_prefix when on the default db' do
conn.default_path_prefix = '/root'
conn.config_filename = fixture_path('two_dbs_path_prefix.yml')
with_connection('default') do
expect(conn.current_path_prefix).to eq('/root')
end
ensure
conn.default_path_prefix = nil
end
end

describe '.default_connection_handler=' do
before do
conn.config_filename = fixture_path("two_dbs.yml")
Expand Down
13 changes: 13 additions & 0 deletions spec/fixtures/two_dbs_path_prefix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
site_a:
adapter: sqlite3
database: 'tmp/db_forum_a.test'
host_names:
- example.localhost
path_prefix: /site_a

site_b:
adapter: sqlite3
database: 'tmp/db_forum_b.test'
host_names:
- example.localhost
path_prefix: /site_b
Loading
Loading