FEATURE: allow specifying s3 config via globals

This refactors handling of s3 so it can be specified via GlobalSetting

This means that in a multisite environment you can configure s3 uploads
without actual sites knowing credentials in s3

It is a critical setting for situations where assets are mirrored to s3.
This commit is contained in:
Sam 2017-10-06 16:20:01 +11:00
parent 5881355006
commit 70bb2aa426
28 changed files with 354 additions and 131 deletions

View File

@ -52,15 +52,29 @@ module ApplicationHelper
end end
end end
def is_brotli_req?
ENV["COMPRESS_BROTLI"] == "1" &&
request.env["HTTP_ACCEPT_ENCODING"] =~ /br/
end
def preload_script(script) def preload_script(script)
path = asset_path("#{script}.js") path = asset_path("#{script}.js")
if GlobalSetting.cdn_url && if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url
GlobalSetting.cdn_url.start_with?("https") && if GlobalSetting.cdn_url
ENV["COMPRESS_BROTLI"] == "1" && path.gsub!(GlobalSetting.cdn_url, GlobalSetting.s3_cdn_url)
request.env["HTTP_ACCEPT_ENCODING"] =~ /br/ else
path = "#{GlobalSetting.s3_cdn_url}#{path}"
end
if is_brotli_req?
path.gsub!(/\.([^.]+)$/, '.br.\1')
end
elsif GlobalSetting.cdn_url&.start_with?("https") && is_brotli_req?
path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/")
end end
"<link rel='preload' href='#{path}' as='script'/> "<link rel='preload' href='#{path}' as='script'/>
<script src='#{path}'></script>".html_safe <script src='#{path}'></script>".html_safe
end end

View File

@ -166,7 +166,7 @@ module Jobs
# we don't want to pull images hosted on the CDN (if we use one) # we don't want to pull images hosted on the CDN (if we use one)
return false if Discourse.asset_host.present? && URI.parse(Discourse.asset_host).hostname == hostname return false if Discourse.asset_host.present? && URI.parse(Discourse.asset_host).hostname == hostname
return false if SiteSetting.s3_cdn_url.present? && URI.parse(SiteSetting.s3_cdn_url).hostname == hostname return false if SiteSetting.Upload.s3_cdn_url.present? && URI.parse(SiteSetting.Upload.s3_cdn_url).hostname == hostname
# we don't want to pull images hosted on the main domain # we don't want to pull images hosted on the main domain
return false if URI.parse(Discourse.base_url_no_prefix).hostname == hostname return false if URI.parse(Discourse.base_url_no_prefix).hostname == hostname
# check the domains blacklist # check the domains blacklist

View File

@ -7,7 +7,7 @@ module Jobs
base_url = Discourse.store.internal? ? Discourse.store.relative_base_url : Discourse.store.absolute_base_url base_url = Discourse.store.internal? ? Discourse.store.relative_base_url : Discourse.store.absolute_base_url
s3_hostname = URI.parse(base_url).hostname s3_hostname = URI.parse(base_url).hostname
s3_cdn_hostname = URI.parse(SiteSetting.s3_cdn_url || "").hostname s3_cdn_hostname = URI.parse(SiteSetting.Upload.s3_cdn_url || "").hostname
# Any URLs in site settings are fair game # Any URLs in site settings are fair game
ignore_urls = [ ignore_urls = [

View File

@ -46,7 +46,7 @@ class Backup
def s3 def s3
require "s3_helper" unless defined? S3Helper require "s3_helper" unless defined? S3Helper
@s3_helper ||= S3Helper.new(s3_bucket) @s3_helper ||= S3Helper.new(s3_bucket, '', S3Helper.s3_options(SiteSetting))
end end
def upload_to_s3 def upload_to_s3

View File

@ -75,6 +75,21 @@ class GlobalSetting
end end
end end
def self.use_s3?
(@use_s3 ||=
begin
s3_bucket &&
s3_region && (
s3_use_iam_profile || (s3_access_key_id && s3_secret_access_key)
) ? :true : :false
end) == :true
end
# for testing
def self.reset_s3_cache!
@use_s3 = nil
end
def self.database_config def self.database_config
hash = { "adapter" => "postgresql" } hash = { "adapter" => "postgresql" }
%w{pool timeout socket host port username password replica_host replica_port}.each do |s| %w{pool timeout socket host port username password replica_host replica_port}.each do |s|

View File

@ -118,6 +118,43 @@ class SiteSetting < ActiveRecord::Base
def self.attachment_filename_blacklist_regex def self.attachment_filename_blacklist_regex
@attachment_filename_blacklist_regex ||= Regexp.union(SiteSetting.attachment_filename_blacklist.split("|")) @attachment_filename_blacklist_regex ||= Regexp.union(SiteSetting.attachment_filename_blacklist.split("|"))
end end
# helpers for getting s3 settings that fallback to global
class Upload
def self.s3_cdn_url
SiteSetting.enable_s3_uploads ? SiteSetting.s3_cdn_url : GlobalSetting.s3_cdn_url
end
def self.s3_region
SiteSetting.enable_s3_uploads ? SiteSetting.s3_region : GlobalSetting.s3_region
end
def self.s3_upload_bucket
SiteSetting.enable_s3_uploads ? SiteSetting.s3_upload_bucket : GlobalSetting.s3_bucket
end
def self.enable_s3_uploads
SiteSetting.enable_s3_uploads || GlobalSetting.use_s3?
end
def self.absolute_base_url
bucket = SiteSetting.enable_s3_uploads ? Discourse.store.s3_bucket_name : GlobalSetting.s3_bucket
# cf. http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
if SiteSetting.Upload.s3_region == "us-east-1"
"//#{bucket}.s3.amazonaws.com"
elsif SiteSetting.Upload.s3_region == 'cn-north-1'
"//#{bucket}.s3.cn-north-1.amazonaws.com.cn"
else
"//#{bucket}.s3-#{SiteSetting.Upload.s3_region}.amazonaws.com"
end
end
end
def self.Upload
SiteSetting::Upload
end
end end
# == Schema Information # == Schema Information

View File

@ -44,8 +44,8 @@ class TopicLinkClick < ActiveRecord::Base
end end
end end
if SiteSetting.s3_cdn_url.present? if SiteSetting.Upload.s3_cdn_url.present?
cdn_uri = URI.parse(SiteSetting.s3_cdn_url) rescue nil cdn_uri = URI.parse(SiteSetting.Upload.s3_cdn_url) rescue nil
if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path)
is_cdn_link = true is_cdn_link = true
path = uri.path[cdn_uri.path.length..-1] path = uri.path[cdn_uri.path.length..-1]

View File

@ -79,7 +79,7 @@ class Upload < ActiveRecord::Base
# we store relative urls, so we need to remove any host/cdn # we store relative urls, so we need to remove any host/cdn
url = url.sub(Discourse.asset_host, "") if Discourse.asset_host.present? url = url.sub(Discourse.asset_host, "") if Discourse.asset_host.present?
# when using s3, we need to replace with the absolute base url # when using s3, we need to replace with the absolute base url
url = url.sub(SiteSetting.s3_cdn_url, Discourse.store.absolute_base_url) if SiteSetting.s3_cdn_url.present? url = url.sub(SiteSetting.Upload.s3_cdn_url, Discourse.store.absolute_base_url) if SiteSetting.Upload.s3_cdn_url.present?
# always try to get the path # always try to get the path
uri = URI(url) rescue nil uri = URI(url) rescue nil

View File

@ -52,11 +52,11 @@
Discourse.Session.currentProp("safe_mode", <%= normalized_safe_mode.inspect.html_safe %>); Discourse.Session.currentProp("safe_mode", <%= normalized_safe_mode.inspect.html_safe %>);
<%- end %> <%- end %>
Discourse.HighlightJSPath = <%= HighlightJs.path.inspect.html_safe %>; Discourse.HighlightJSPath = <%= HighlightJs.path.inspect.html_safe %>;
<%- if SiteSetting.enable_s3_uploads %> <%- if SiteSetting.Upload.enable_s3_uploads %>
<%- if SiteSetting.s3_cdn_url.present? %> <%- if SiteSetting.Upload.s3_cdn_url.present? %>
Discourse.S3CDN = '<%= SiteSetting.s3_cdn_url %>'; Discourse.S3CDN = '<%= SiteSetting.Upload.s3_cdn_url %>';
<%- end %> <%- end %>
Discourse.S3BaseUrl = '<%= Discourse.store.absolute_base_url %>'; Discourse.S3BaseUrl = '<%= SiteSetting.Upload.absolute_base_url %>';
<%- end %> <%- end %>
})(); })();
</script> </script>

View File

@ -156,3 +156,12 @@ secret_key_base =
# in multi host setups this allows you to have old unicorn instances serve # in multi host setups this allows you to have old unicorn instances serve
# newly compiled assets # newly compiled assets
fallback_assets_path = fallback_assets_path =
# S3 settings used for serving ALL public files
# be sure to configre a CDN as well per cdn_url
s3_bucket =
s3_region =
s3_access_key_id =
s3_secret_access_key =
s3_use_iam_profile = false
s3_cdn_url =

View File

@ -28,10 +28,10 @@ module Autospec
def self.reload(pattern); RELOADERS << pattern; end def self.reload(pattern); RELOADERS << pattern; end
def reloaders; RELOADERS; end def reloaders; RELOADERS; end
# We need to reload the whole app when changing any of these files # we are using a simple runner at the moment, whole idea of using a reloader is no longer needed
reload("spec/rails_helper.rb") watch("spec/rails_helper.rb")
reload(%r{config/.+\.rb}) watch(%r{config/.+\.rb})
reload(%r{app/helpers/.+\.rb}) #reload(%r{app/helpers/.+\.rb})
def failed_specs def failed_specs
specs = [] specs = []

View File

@ -337,7 +337,7 @@ class CookedPostProcessor
end end
end end
use_s3_cdn = SiteSetting.enable_s3_uploads && SiteSetting.s3_cdn_url.present? use_s3_cdn = SiteSetting.Upload.enable_s3_uploads && SiteSetting.Upload.s3_cdn_url.present?
%w{href data-download-href}.each do |selector| %w{href data-download-href}.each do |selector|
@doc.css("a[#{selector}]").each do |a| @doc.css("a[#{selector}]").each do |a|

View File

@ -391,7 +391,7 @@ module Discourse
end end
def self.store def self.store
if SiteSetting.enable_s3_uploads? if SiteSetting.Upload.enable_s3_uploads
@s3_store_loaded ||= require 'file_store/s3_store' @s3_store_loaded ||= require 'file_store/s3_store'
FileStore::S3Store.new FileStore::S3Store.new
else else

View File

@ -56,22 +56,17 @@ module FileStore
base_hostname = URI.parse(absolute_base_url).hostname base_hostname = URI.parse(absolute_base_url).hostname
return true if url[base_hostname] return true if url[base_hostname]
return false if SiteSetting.s3_cdn_url.blank? return false if SiteSetting.Upload.s3_cdn_url.blank?
cdn_hostname = URI.parse(SiteSetting.s3_cdn_url || "").hostname cdn_hostname = URI.parse(SiteSetting.Upload.s3_cdn_url || "").hostname
cdn_hostname.presence && url[cdn_hostname] cdn_hostname.presence && url[cdn_hostname]
end end
def absolute_base_url def s3_bucket_name
bucket = @s3_helper.s3_bucket_name @s3_helper.s3_bucket_name
end
# cf. http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region def absolute_base_url
@absolute_base_url ||= if SiteSetting.s3_region == "us-east-1" @absolute_base_url ||= SiteSetting.Upload.absolute_base_url
"//#{bucket}.s3.amazonaws.com"
elsif SiteSetting.s3_region == 'cn-north-1'
"//#{bucket}.s3.cn-north-1.amazonaws.com.cn"
else
"//#{bucket}.s3-#{SiteSetting.s3_region}.amazonaws.com"
end
end end
def external? def external?
@ -88,9 +83,9 @@ module FileStore
end end
def cdn_url(url) def cdn_url(url)
return url if SiteSetting.s3_cdn_url.blank? return url if SiteSetting.Upload.s3_cdn_url.blank?
schema = url[/^(https?:)?\/\//, 1] schema = url[/^(https?:)?\/\//, 1]
url.sub("#{schema}#{absolute_base_url}", SiteSetting.s3_cdn_url) url.sub("#{schema}#{absolute_base_url}", SiteSetting.Upload.s3_cdn_url)
end end
def cache_avatar(avatar, user_id) def cache_avatar(avatar, user_id)
@ -104,8 +99,8 @@ module FileStore
end end
def s3_bucket def s3_bucket
raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.s3_upload_bucket.blank? raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.Upload.s3_upload_bucket.blank?
SiteSetting.s3_upload_bucket.downcase SiteSetting.Upload.s3_upload_bucket.downcase
end end
end end
end end

View File

@ -170,7 +170,7 @@ class FinalDestination
return false unless @uri && @uri.host return false unless @uri && @uri.host
# Whitelisted hosts # Whitelisted hosts
return true if hostname_matches?(SiteSetting.s3_cdn_url) || return true if hostname_matches?(SiteSetting.Upload.s3_cdn_url) ||
hostname_matches?(GlobalSetting.try(:cdn_url)) || hostname_matches?(GlobalSetting.try(:cdn_url)) ||
hostname_matches?(Discourse.base_url_no_prefix) hostname_matches?(Discourse.base_url_no_prefix)

View File

@ -8,8 +8,8 @@ module GlobalPath
end end
def upload_cdn_path(p) def upload_cdn_path(p)
if SiteSetting.s3_cdn_url.present? if SiteSetting.Upload.s3_cdn_url.present?
p = p.sub(Discourse.store.absolute_base_url, SiteSetting.s3_cdn_url) p = p.sub(Discourse.store.absolute_base_url, SiteSetting.Upload.s3_cdn_url)
end end
p =~ /^http/ ? p : cdn_path(p) p =~ /^http/ ? p : cdn_path(p)
end end

View File

@ -141,9 +141,9 @@ module PrettyText
CDN: Rails.configuration.action_controller.asset_host, CDN: Rails.configuration.action_controller.asset_host,
} }
if SiteSetting.enable_s3_uploads? if SiteSetting.Upload.enable_s3_uploads
if SiteSetting.s3_cdn_url.present? if SiteSetting.Upload.s3_cdn_url.present?
paths[:S3CDN] = SiteSetting.s3_cdn_url paths[:S3CDN] = SiteSetting.Upload.s3_cdn_url
end end
paths[:S3BaseUrl] = Discourse.store.absolute_base_url paths[:S3BaseUrl] = Discourse.store.absolute_base_url
end end
@ -250,7 +250,7 @@ module PrettyText
add_rel_nofollow_to_user_content(doc) add_rel_nofollow_to_user_content(doc)
end end
if SiteSetting.enable_s3_uploads && SiteSetting.s3_cdn_url.present? if SiteSetting.Upload.enable_s3_uploads && SiteSetting.Upload.s3_cdn_url.present?
add_s3_cdn(doc) add_s3_cdn(doc)
end end

View File

@ -6,12 +6,12 @@ class S3Helper
attr_reader :s3_bucket_name attr_reader :s3_bucket_name
def initialize(s3_upload_bucket, tombstone_prefix = '', options = {}) def initialize(s3_bucket_name, tombstone_prefix = '', options = {})
@s3_options = default_s3_options.merge(options) @s3_options = default_s3_options.merge(options)
@s3_bucket_name, @s3_bucket_folder_path = begin @s3_bucket_name, @s3_bucket_folder_path = begin
raise Discourse::InvalidParameters.new("s3_bucket") if s3_upload_bucket.blank? raise Discourse::InvalidParameters.new("s3_bucket_name") if s3_bucket_name.blank?
s3_upload_bucket.downcase.split("/".freeze, 2) s3_bucket_name.downcase.split("/".freeze, 2)
end end
@tombstone_prefix = @tombstone_prefix =
@ -20,8 +20,6 @@ class S3Helper
else else
tombstone_prefix tombstone_prefix
end end
check_missing_options
end end
def upload(file, path, options = {}) def upload(file, path, options = {})
@ -80,8 +78,8 @@ class S3Helper
update_lifecycle("purge_tombstone", grace_period, prefix: @tombstone_prefix) update_lifecycle("purge_tombstone", grace_period, prefix: @tombstone_prefix)
end end
def list def list(prefix = "")
s3_bucket.objects(prefix: @s3_bucket_folder_path) s3_bucket.objects(prefix: @s3_bucket_folder_path.to_s + prefix)
end end
def tag_file(key, tags) def tag_file(key, tags)
@ -99,24 +97,36 @@ class S3Helper
) )
end end
def self.s3_options(obj)
opts = { region: obj.s3_region }
unless obj.s3_use_iam_profile
opts[:access_key_id] = obj.s3_access_key_id
opts[:secret_access_key] = obj.s3_secret_access_key
end
opts
end
private private
def default_s3_options
if SiteSetting.enable_s3_uploads?
options = self.class.s3_options(SiteSetting)
check_missing_site_options
options
elsif GlobalSetting.use_s3?
self.class.s3_options(GlobalSetting)
else
{}
end
end
def get_path_for_s3_upload(path) def get_path_for_s3_upload(path)
path = File.join(@s3_bucket_folder_path, path) if @s3_bucket_folder_path path = File.join(@s3_bucket_folder_path, path) if @s3_bucket_folder_path
path path
end end
def default_s3_options
opts = { region: SiteSetting.s3_region }
unless SiteSetting.s3_use_iam_profile
opts[:access_key_id] = SiteSetting.s3_access_key_id
opts[:secret_access_key] = SiteSetting.s3_secret_access_key
end
opts
end
def s3_resource def s3_resource
Aws::S3::Resource.new(@s3_options) Aws::S3::Resource.new(@s3_options)
end end
@ -127,10 +137,10 @@ class S3Helper
bucket bucket
end end
def check_missing_options def check_missing_site_options
unless SiteSetting.s3_use_iam_profile unless SiteSetting.s3_use_iam_profile
raise SettingMissing.new("access_key_id") if @s3_options[:access_key_id].blank? raise SettingMissing.new("access_key_id") if SiteSetting.s3_access_key_id.blank?
raise SettingMissing.new("secret_access_key") if @s3_options[:secret_access_key].blank? raise SettingMissing.new("secret_access_key") if SiteSetting.s3_secret_access_key.blank?
end end
end end
end end

View File

@ -11,15 +11,12 @@ def gzip_s3_path(path)
end end
def should_skip?(path) def should_skip?(path)
return true if ENV['FORCE_S3_UPLOADS'] return false if ENV['FORCE_S3_UPLOADS']
@existing_assets ||= Set.new(helper.list.map(&:key)) @existing_assets ||= Set.new(helper.list("assets/").map(&:key))
@existing_assets.include?('assets/' + path) @existing_assets.include?(path)
end end
def upload_asset(helper, path, recurse: true, content_type: nil, fullpath: nil, content_encoding: nil) def upload(path, remote_path, content_type, content_encoding = nil)
fullpath ||= (Rails.root + "public/assets/#{path}").to_s
content_type ||= MiniMime.lookup_by_filename(path).content_type
options = { options = {
cache_control: 'max-age=31556952, public, immutable', cache_control: 'max-age=31556952, public, immutable',
@ -32,50 +29,46 @@ def upload_asset(helper, path, recurse: true, content_type: nil, fullpath: nil,
options[:content_encoding] = content_encoding options[:content_encoding] = content_encoding
end end
if should_skip?(path) if should_skip?(remote_path)
puts "Skipping: #{path}" puts "Skipping: #{remote_path}"
else else
puts "Uploading: #{path}" puts "Uploading: #{remote_path}"
helper.upload(fullpath, path, options) helper.upload(path, remote_path, options)
end end
end
if recurse def helper
if File.exist?(fullpath + ".br") @helper ||= S3Helper.new(GlobalSetting.s3_bucket.downcase, '', S3Helper.s3_options(GlobalSetting))
brotli_path = brotli_s3_path(path)
upload_asset(helper, brotli_path,
fullpath: fullpath + ".br",
recurse: false,
content_type: content_type,
content_encoding: 'br'
)
end
if File.exist?(fullpath + ".gz")
gzip_path = gzip_s3_path(path)
upload_asset(helper, gzip_path,
fullpath: fullpath + ".gz",
recurse: false,
content_type: content_type,
content_encoding: 'gzip'
)
end
if File.exist?(fullpath + ".map")
upload_asset(helper, path + ".map", recurse: false, content_type: 'application/json')
end
end
end end
def assets def assets
cached = Rails.application.assets&.cached cached = Rails.application.assets&.cached
manifest = Sprockets::Manifest.new(cached, Rails.root + 'public/assets', Rails.application.config.assets.manifest) manifest = Sprockets::Manifest.new(cached, Rails.root + 'public/assets', Rails.application.config.assets.manifest)
raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.s3_upload_bucket.blank? results = []
manifest.assets
end
def helper manifest.assets.each do |_, path|
@helper ||= S3Helper.new(SiteSetting.s3_upload_bucket.downcase + '/assets') fullpath = (Rails.root + "public/assets/#{path}").to_s
content_type = MiniMime.lookup_by_filename(fullpath).content_type
asset_path = "assets/#{path}"
results << [fullpath, asset_path, content_type]
if File.exist?(fullpath + '.br')
results << [fullpath + '.br', brotli_s3_path(asset_path), content_type, 'br']
end
if File.exist?(fullpath + '.gz')
results << [fullpath + '.gz', gzip_s3_path(asset_path), content_type, 'gzip']
end
if File.exist?(fullpath + '.map')
results << [fullpath + '.map', asset_path + '.map', 'application/json']
end
end
results
end end
def in_manifest def in_manifest
@ -102,13 +95,23 @@ def in_manifest
Set.new(found) Set.new(found)
end end
def ensure_s3_configured!
unless GlobalSetting.use_s3?
STDERR.puts "ERROR: Ensure S3 is configured in config/discourse.conf of environment vars"
exit 1
end
end
task 's3:upload_assets' => :environment do task 's3:upload_assets' => :environment do
assets.each do |name, fingerprint| ensure_s3_configured!
upload_asset(helper, fingerprint)
assets.each do |asset|
upload(*asset)
end end
end end
task 's3:expire_missing_assets' => :environment do task 's3:expire_missing_assets' => :environment do
ensure_s3_configured!
keep = in_manifest keep = in_manifest
count = 0 count = 0

View File

@ -114,13 +114,13 @@ def migrate_from_s3
require "file_store/s3_store" require "file_store/s3_store"
# make sure S3 is disabled # make sure S3 is disabled
if SiteSetting.enable_s3_uploads if SiteSetting.Uploads.enable_s3_uploads
puts "You must disable S3 uploads before running that task." puts "You must disable S3 uploads before running that task."
return return
end end
# make sure S3 bucket is set # make sure S3 bucket is set
if SiteSetting.s3_upload_bucket.blank? if SiteSetting.Upload.s3_upload_bucket.blank?
puts "The S3 upload bucket must be set before running that task." puts "The S3 upload bucket must be set before running that task."
return return
end end
@ -188,14 +188,14 @@ end
def migrate_to_s3 def migrate_to_s3
# make sure s3 is enabled # make sure s3 is enabled
if !SiteSetting.enable_s3_uploads if !SiteSetting.Upload.enable_s3_uploads
puts "You must enable s3 uploads before running that task" puts "You must enable s3 uploads before running that task"
return return
end end
db = RailsMultisite::ConnectionManagement.current_db db = RailsMultisite::ConnectionManagement.current_db
puts "Migrating uploads to S3 (#{SiteSetting.s3_upload_bucket}) for '#{db}'..." puts "Migrating uploads to S3 (#{SiteSetting.Upload.s3_upload_bucket}) for '#{db}'..."
# will throw an exception if the bucket is missing # will throw an exception if the bucket is missing
s3 = FileStore::S3Store.new s3 = FileStore::S3Store.new

View File

@ -16,6 +16,7 @@ describe FileStore::S3Store do
SiteSetting.s3_upload_bucket = "s3-upload-bucket" SiteSetting.s3_upload_bucket = "s3-upload-bucket"
SiteSetting.s3_access_key_id = "s3-access-key-id" SiteSetting.s3_access_key_id = "s3-access-key-id"
SiteSetting.s3_secret_access_key = "s3-secret-access-key" SiteSetting.s3_secret_access_key = "s3-secret-access-key"
SiteSetting.enable_s3_uploads = true
end end
shared_context 's3 helpers' do shared_context 's3 helpers' do

View File

@ -292,6 +292,7 @@ describe FinalDestination do
end end
it "returns true for the S3 CDN url" do it "returns true for the S3 CDN url" do
SiteSetting.enable_s3_uploads = true
SiteSetting.s3_cdn_url = "https://s3.example.com" SiteSetting.s3_cdn_url = "https://s3.example.com"
expect(fd("https://s3.example.com/some/thing").is_dest_valid?).to eq(true) expect(fd("https://s3.example.com/some/thing").is_dest_valid?).to eq(true)
end end

View File

@ -623,29 +623,56 @@ describe PrettyText do
expect(PrettyText.cook(raw)).to eq(html.strip) expect(PrettyText.cook(raw)).to eq(html.strip)
end end
it 'can substitute s3 cdn correctly' do describe 's3_cdn' do
SiteSetting.enable_s3_uploads = true
SiteSetting.s3_access_key_id = "XXX"
SiteSetting.s3_secret_access_key = "XXX"
SiteSetting.s3_upload_bucket = "test"
SiteSetting.s3_cdn_url = "https://awesome.cdn"
# add extra img tag to ensure it does not blow up def test_s3_cdn
raw = <<~HTML # add extra img tag to ensure it does not blow up
<img> raw = <<~HTML
<img src='https:#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'> <img>
<img src='http:#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'> <img src='https:#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'>
<img src='#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'> <img src='http:#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'>
HTML <img src='#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'>
HTML
html = <<~HTML html = <<~HTML
<p><img><br> <p><img><br>
<img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br> <img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br>
<img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br> <img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br>
<img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"></p> <img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"></p>
HTML HTML
expect(PrettyText.cook(raw)).to eq(html.strip) expect(PrettyText.cook(raw)).to eq(html.strip)
end
before do
GlobalSetting.reset_s3_cache!
end
after do
GlobalSetting.reset_s3_cache!
end
it 'can substitute s3 cdn when added via global setting' do
global_setting :s3_access_key_id, 'XXX'
global_setting :s3_secret_access_key, 'XXX'
global_setting :s3_bucket, 'XXX'
global_setting :s3_region, 'XXX'
global_setting :s3_cdn_url, 'https://awesome.cdn'
test_s3_cdn
end
it 'can substitute s3 cdn correctly' do
SiteSetting.s3_access_key_id = "XXX"
SiteSetting.s3_secret_access_key = "XXX"
SiteSetting.s3_upload_bucket = "test"
SiteSetting.s3_cdn_url = "https://awesome.cdn"
SiteSetting.enable_s3_uploads = true
test_s3_cdn
end
end end
describe "emoji" do describe "emoji" do

View File

@ -20,8 +20,13 @@ describe Stylesheet::Importer do
end end
it "applies S3 CDN to background category images" do it "applies S3 CDN to background category images" do
SiteSetting.s3_use_iam_profile = true
SiteSetting.s3_upload_bucket = 'test'
SiteSetting.s3_region = 'ap-southeast-2'
SiteSetting.s3_cdn_url = "https://s3.cdn" SiteSetting.s3_cdn_url = "https://s3.cdn"
SiteSetting.enable_s3_uploads = true
background = Fabricate(:upload_s3) background = Fabricate(:upload_s3)
category = Fabricate(:category, uploaded_background: background) category = Fabricate(:category, uploaded_background: background)

View File

@ -2,6 +2,50 @@ require 'rails_helper'
describe ApplicationHelper do describe ApplicationHelper do
describe "preload_script" do
it "provides brotli links to brotli cdn" do
set_cdn_url "https://awesome.com"
set_env "COMPRESS_BROTLI", "1"
helper.request.env["HTTP_ACCEPT_ENCODING"] = 'br'
link = helper.preload_script('application')
expect(link).to eq("<link rel='preload' href='https://awesome.com/brotli_asset/application.js' as='script'/>\n<script src='https://awesome.com/brotli_asset/application.js'></script>")
end
context "with s3 CDN" do
before do
global_setting :s3_bucket, 'test_bucket'
global_setting :s3_region, 'ap-australia'
global_setting :s3_access_key_id, '123'
global_setting :s3_secret_access_key, '123'
global_setting :s3_cdn_url, 'https://s3cdn.com'
set_env "COMPRESS_BROTLI", "1"
end
it "returns magic brotli mangling for brotli requests" do
helper.request.env["HTTP_ACCEPT_ENCODING"] = 'br'
link = helper.preload_script('application')
expect(link).to eq("<link rel='preload' href='https://s3cdn.com/assets/application.br.js' as='script'/>\n<script src='https://s3cdn.com/assets/application.br.js'></script>")
end
it "gives s3 cdn if asset host is not set" do
link = helper.preload_script('application')
expect(link).to eq("<link rel='preload' href='https://s3cdn.com/assets/application.js' as='script'/>\n<script src='https://s3cdn.com/assets/application.js'></script>")
end
it "gives s3 cdn even if asset host is set" do
set_cdn_url "https://awesome.com"
link = helper.preload_script('application')
expect(link).to eq("<link rel='preload' href='https://s3cdn.com/assets/application.js' as='script'/>\n<script src='https://s3cdn.com/assets/application.js'></script>")
end
end
end
describe "escape_unicode" do describe "escape_unicode" do
it "encodes tags" do it "encodes tags" do
expect(helper.escape_unicode("<tag>")).to eq("\u003ctag>") expect(helper.escape_unicode("<tag>")).to eq("\u003ctag>")

View File

@ -9,6 +9,21 @@ end
describe GlobalSetting do describe GlobalSetting do
describe '.use_s3_assets?' do
it 'returns false by default' do
expect(GlobalSetting.use_s3?).to eq(false)
end
it 'returns true once set' do
global_setting :s3_bucket, 'test_bucket'
global_setting :s3_region, 'ap-australia'
global_setting :s3_access_key_id, '123'
global_setting :s3_secret_access_key, '123'
expect(GlobalSetting.use_s3?).to eq(true)
end
end
describe '.safe_secret_key_base' do describe '.safe_secret_key_base' do
it 'sets redis token if it is somehow flushed after 30 seconds' do it 'sets redis token if it is somehow flushed after 30 seconds' do

View File

@ -145,6 +145,9 @@ describe TopicLinkClick do
it "works with s3 urls" do it "works with s3 urls" do
SiteSetting.s3_cdn_url = "https://discourse-s3-cdn.global.ssl.fastly.net" SiteSetting.s3_cdn_url = "https://discourse-s3-cdn.global.ssl.fastly.net"
SiteSetting.s3_access_key_id = 'X'
SiteSetting.s3_secret_access_key = 'X'
SiteSetting.enable_s3_uploads = true
post = Fabricate(:post, topic: @topic, raw: "[test](//test.localhost/uploads/default/my-test-link)") post = Fabricate(:post, topic: @topic, raw: "[test](//test.localhost/uploads/default/my-test-link)")
TopicLink.extract_from(post) TopicLink.extract_from(post)

View File

@ -124,6 +124,11 @@ RSpec.configure do |config|
Sidekiq::Worker.clear_all Sidekiq::Worker.clear_all
I18n.locale = :en I18n.locale = :en
if $test_cleanup_callbacks
$test_cleanup_callbacks.reverse_each(&:call)
$test_cleanup_callbacks = nil
end
end end
class TestCurrentUserProvider < Auth::DefaultCurrentUserProvider class TestCurrentUserProvider < Auth::DefaultCurrentUserProvider
@ -146,6 +151,45 @@ class TrackTimeStub
end end
end end
def before_next_spec(&callback)
($test_cleanup_callbacks ||= []) << callback
end
def global_setting(name, value)
GlobalSetting.reset_s3_cache!
GlobalSetting.stubs(name).returns(value)
before_next_spec do
GlobalSetting.reset_s3_cache!
end
end
def set_env(var, value)
old = ENV.fetch var, :missing
ENV[var] = value
before_next_spec do
if old == :missing
ENV.delete var
else
ENV[var] = old
end
end
end
def set_cdn_url(cdn_url)
global_setting :cdn_url, cdn_url
Rails.configuration.action_controller.asset_host = cdn_url
ActionController::Base.asset_host = cdn_url
before_next_spec do
Rails.configuration.action_controller.asset_host = nil
ActionController::Base.asset_host = nil
end
end
def freeze_time(now = Time.now) def freeze_time(now = Time.now)
datetime = DateTime.parse(now.to_s) datetime = DateTime.parse(now.to_s)
time = Time.parse(now.to_s) time = Time.parse(now.to_s)