mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 01:16:22 +08:00
29ef2cb550
In "GlobalSetting.redirect_avatar_requests" mode, when the application gets an avatar request it returns a "redirect" to the S3 CDN. This shields the application from caching avatars and downloading from S3. However clients will make 2 requests per avatar. (one to get redirect, second to get avatar) A one hour cache on a redirect means there may be an increase in CDN traffic, given more clients will ask for the redirect every hour. This may also lead to an increase in origin requests to the application. To mitigate lets cache the CDN URL for 1 day. The downside is that any changes to S3 CDN need extra care to allow for the extra 1 day delay. (leave data around for 1 extra day)
220 lines
6.2 KiB
Ruby
220 lines
6.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class UserAvatarsController < ApplicationController
|
|
skip_before_action :preload_json,
|
|
:redirect_to_login_if_required,
|
|
:check_xhr,
|
|
:verify_authenticity_token,
|
|
only: %i[show show_letter show_proxy_letter]
|
|
|
|
before_action :apply_cdn_headers, only: %i[show show_letter show_proxy_letter]
|
|
|
|
def refresh_gravatar
|
|
user = User.find_by(username_lower: params[:username].downcase)
|
|
guardian.ensure_can_edit!(user)
|
|
|
|
if user
|
|
hijack do
|
|
user.create_user_avatar(user_id: user.id) unless user.user_avatar
|
|
user.user_avatar.update_gravatar!
|
|
|
|
gravatar =
|
|
if user.user_avatar.gravatar_upload_id
|
|
{
|
|
gravatar_upload_id: user.user_avatar.gravatar_upload_id,
|
|
gravatar_avatar_template:
|
|
User.avatar_template(user.username, user.user_avatar.gravatar_upload_id),
|
|
}
|
|
else
|
|
{ gravatar_upload_id: nil, gravatar_avatar_template: nil }
|
|
end
|
|
|
|
render json: gravatar
|
|
end
|
|
else
|
|
raise Discourse::NotFound
|
|
end
|
|
end
|
|
|
|
def show_proxy_letter
|
|
is_asset_path
|
|
|
|
if SiteSetting.external_system_avatars_url !~ %r{^/letter_avatar_proxy}
|
|
raise Discourse::NotFound
|
|
end
|
|
|
|
params.require(:letter)
|
|
params.require(:color)
|
|
params.require(:version)
|
|
params.require(:size)
|
|
|
|
hijack do
|
|
begin
|
|
proxy_avatar(
|
|
"https://avatars.discourse-cdn.com/#{params[:version]}/letter/#{params[:letter]}/#{params[:color]}/#{params[:size]}.png",
|
|
Time.new(1990, 01, 01),
|
|
)
|
|
rescue OpenURI::HTTPError
|
|
render_blank
|
|
end
|
|
end
|
|
end
|
|
|
|
def show_letter
|
|
is_asset_path
|
|
|
|
params.require(:username)
|
|
params.require(:version)
|
|
params.require(:size)
|
|
|
|
no_cookies
|
|
|
|
return render_blank if params[:version] != LetterAvatar.version
|
|
|
|
hijack do
|
|
image = LetterAvatar.generate(params[:username].to_s, params[:size].to_i)
|
|
|
|
response.headers["Last-Modified"] = File.ctime(image).httpdate
|
|
response.headers["Content-Length"] = File.size(image).to_s
|
|
immutable_for(1.year)
|
|
send_file image, disposition: nil
|
|
end
|
|
end
|
|
|
|
def show
|
|
is_asset_path
|
|
|
|
# we need multisite support to keep a single origin pull for CDNs
|
|
RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do
|
|
hijack { show_in_site(params[:hostname]) }
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def show_in_site(hostname)
|
|
username = params[:username].to_s
|
|
return render_blank unless user = User.find_by(username_lower: username.downcase)
|
|
|
|
upload_id, version = params[:version].split("_")
|
|
|
|
version = (version || OptimizedImage::VERSION).to_i
|
|
|
|
# old versions simply get new avatar
|
|
return render_blank if version > OptimizedImage::VERSION
|
|
|
|
upload_id = upload_id.to_i
|
|
return render_blank unless upload_id > 0
|
|
|
|
size = params[:size].to_i
|
|
return render_blank if size < 8 || size > 1000
|
|
|
|
if !Discourse.avatar_sizes.include?(size) && Discourse.store.external?
|
|
closest = Discourse.avatar_sizes.to_a.min { |a, b| (size - a).abs <=> (size - b).abs }
|
|
avatar_url =
|
|
UserAvatar.local_avatar_url(
|
|
hostname,
|
|
user.encoded_username(lower: true),
|
|
upload_id,
|
|
closest,
|
|
)
|
|
return redirect_to cdn_path(avatar_url), allow_other_host: true
|
|
end
|
|
|
|
upload = Upload.find_by(id: upload_id) if user&.user_avatar&.contains_upload?(upload_id)
|
|
upload ||= user.uploaded_avatar if user.uploaded_avatar_id == upload_id
|
|
|
|
if user.uploaded_avatar && !upload
|
|
avatar_url =
|
|
UserAvatar.local_avatar_url(
|
|
hostname,
|
|
user.encoded_username(lower: true),
|
|
user.uploaded_avatar_id,
|
|
size,
|
|
)
|
|
return redirect_to cdn_path(avatar_url), allow_other_host: true
|
|
elsif upload && optimized = get_optimized_image(upload, size)
|
|
if optimized.local?
|
|
optimized_path = Discourse.store.path_for(optimized)
|
|
image = optimized_path if File.exist?(optimized_path)
|
|
elsif GlobalSetting.redirect_avatar_requests
|
|
return redirect_s3_avatar(Discourse.store.cdn_url(optimized.url))
|
|
else
|
|
return proxy_avatar(Discourse.store.cdn_url(optimized.url), upload.created_at)
|
|
end
|
|
end
|
|
|
|
if image
|
|
response.headers["Last-Modified"] = File.ctime(image).httpdate
|
|
response.headers["Content-Length"] = File.size(image).to_s
|
|
immutable_for 1.year
|
|
send_file image, disposition: nil
|
|
else
|
|
render_blank
|
|
end
|
|
rescue OpenURI::HTTPError
|
|
render_blank
|
|
end
|
|
|
|
# Allow plugins to overwrite max file size value
|
|
def max_file_size
|
|
1.megabyte
|
|
end
|
|
|
|
PROXY_PATH = Rails.root + "tmp/avatar_proxy"
|
|
def proxy_avatar(url, last_modified)
|
|
url = (SiteSetting.force_https ? "https:" : "http:") + url if url[0..1] == "//"
|
|
|
|
sha = Digest::SHA1.hexdigest(url)
|
|
filename = "#{sha}#{File.extname(url)}"
|
|
path = "#{PROXY_PATH}/#{filename}"
|
|
|
|
unless File.exist? path
|
|
FileUtils.mkdir_p PROXY_PATH
|
|
tmp =
|
|
FileHelper.download(
|
|
url,
|
|
max_file_size: max_file_size,
|
|
tmp_file_name: filename,
|
|
follow_redirect: true,
|
|
read_timeout: 10,
|
|
)
|
|
|
|
return render_blank if tmp.nil?
|
|
|
|
FileUtils.mv tmp.path, path
|
|
end
|
|
|
|
response.headers["Last-Modified"] = last_modified.httpdate
|
|
response.headers["Content-Length"] = File.size(path).to_s
|
|
immutable_for(1.year)
|
|
send_file path, disposition: nil
|
|
end
|
|
|
|
def redirect_s3_avatar(url)
|
|
immutable_for 1.day
|
|
redirect_to url, allow_other_host: true
|
|
end
|
|
|
|
# this protects us from a DoS
|
|
def render_blank
|
|
path = Rails.root + "public/images/avatar.png"
|
|
expires_in 10.minutes, public: true
|
|
response.headers["Last-Modified"] = Time.new(1990, 01, 01).httpdate
|
|
response.headers["Content-Length"] = File.size(path).to_s
|
|
send_file path, disposition: nil
|
|
end
|
|
|
|
protected
|
|
|
|
# consider removal of hacks some time in 2019
|
|
|
|
def get_optimized_image(upload, size)
|
|
return if !upload
|
|
return upload if upload.extension == "svg"
|
|
|
|
upload.get_optimized_image(size, size)
|
|
# TODO decide if we want to detach here
|
|
end
|
|
end
|