Do not use ActiveStorage variants, process original avatar
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

Variants are currently broken. So we process the original file with the
most common avatar dimensions and stripping metadata, then hash and
upload only that version.
This commit is contained in:
Râu Cao 2025-05-14 14:42:03 +04:00
parent 1884f082ee
commit 417e346074
Signed by: raucao
GPG Key ID: 37036C356E56CC51
5 changed files with 53 additions and 35 deletions

View File

@ -5,22 +5,20 @@ class AvatarsController < ApplicationController
sha256_hash = params[:hash] sha256_hash = params[:hash]
format = params[:format]&.to_sym || :png format = params[:format]&.to_sym || :png
size = params[:size]&.to_sym || :original # size = params[:size]&.to_sym || :original
unless user.avatar.filename.to_s == "#{sha256_hash}.#{format}" unless user.avatar.filename.to_s == "#{sha256_hash}.#{format}"
http_status :not_found and return http_status :not_found and return
end end
blob = if size == :original # TODO See note for avatar_variant in user model
user.avatar.blob # blob = if size == :original
else # user.avatar.blob
# TODO Variants use the same custom storage key/path, which # else
# makes blob downloads always fetch the original version instead # user.avatar_variant(size: size)&.blob
# of the variant. Needs to be fixed/added in Rails. # end
user.avatar_variant(size: size)&.blob
end
data = blob.download data = user.avatar.blob.download
send_data data, type: "image/#{format}", disposition: "inline" send_data data, type: "image/#{format}", disposition: "inline"
else else
http_status :not_found http_status :not_found

View File

@ -191,9 +191,14 @@ class SettingsController < ApplicationController
end end
def store_user_avatar def store_user_avatar
data = @user.avatar_new.tempfile.read io = @user.avatar_new.tempfile
@user.avatar_new.tempfile.rewind img_data = process_avatar(io)
hash = Digest::SHA256.hexdigest(data) tempfile = Tempfile.create
tempfile.binmode
tempfile.write(img_data)
tempfile.rewind
hash = Digest::SHA256.hexdigest(img_data)
ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg" ext = @user.avatar_new.content_type == "image/png" ? "png" : "jpg"
filename = "#{hash}.#{ext}" filename = "#{hash}.#{ext}"
@ -202,8 +207,22 @@ class SettingsController < ApplicationController
false false
else else
key = "users/#{@user.cn}/avatars/#{filename}" key = "users/#{@user.cn}/avatars/#{filename}"
@user.avatar.attach io: @user.avatar_new.tempfile, key: key, filename: filename @user.avatar.attach io: tempfile, key: key, filename: filename
@user.save @user.save
end end
end end
def process_avatar(io)
processed = ImageProcessing::Vips
.source(io)
.resize_to_fill(400, 400)
.saver(strip: true)
.call
io.rewind
processed.read
rescue Vips::Error => e
Sentry.capture_exception(e) if Setting.sentry_enabled?
Rails.logger.error { "Image processing failed for avatar: #{e.message}" }
nil
end
end end

View File

@ -56,6 +56,7 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validate :acceptable_avatar validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? } validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
@ -83,6 +84,10 @@ class User < ApplicationRecord
:timeoutable, :timeoutable,
:rememberable :rememberable
#
# Methods
#
def ldap_before_save def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
self.ou = dn.split(',') self.ou = dn.split(',')
@ -165,23 +170,19 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def avatar_base64(size: :medium) # TODO Variant keys are currently broken for some reason
return nil unless avatar.attached? # (They use the same key as the main blob, when it should be
variant = avatar_variant(size: size) # "/variants/#{key)"
data = ActiveStorage::Blob.service.download(variant.key) # def avatar_variant(size: :medium)
Base64.strict_encode64(data) # dimensions = case size
end # when :large then [400, 400]
# when :medium then [256, 256]
def avatar_variant(size: :medium) # when :small then [64, 64]
dimensions = case size # else [256, 256]
when :large then [400, 400] # end
when :medium then [256, 256] # format = avatar.content_type == "image/png" ? :png : :jpeg
when :small then [64, 64] # avatar.variant(resize_to_fill: dimensions, format: format)
else [256, 256] # end
end
format = avatar.content_type == "image/png" ? :png : :jpeg
avatar.variant(resize_to_fill: dimensions, format: format)
end
def nostr_pubkey def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key] @nostr_pubkey ||= ldap_entry[:nostr_key]

View File

@ -14,15 +14,16 @@ module LdapManager
end end
img_data = @user.avatar.blob.download img_data = @user.avatar.blob.download
jpg_data = process(img_data) jpg_data = process_avatar
Rails.logger.debug { "Storing new jpegPhoto for user #{@user.cn} in LDAP" }
result = replace_attribute(@dn, :jpegPhoto, jpg_data) result = replace_attribute(@dn, :jpegPhoto, jpg_data)
result == 0 result == 0
end end
private private
def process(data) def process_avatar
@user.avatar.blob.open do |file| @user.avatar.blob.open do |file|
processed = ImageProcessing::Vips processed = ImageProcessing::Vips
.source(file) .source(file)

View File

@ -38,8 +38,7 @@
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<% if @user.avatar.attached? %> <% if @user.avatar.attached? %>
<p class="flex-none"> <p class="flex-none">
<%= image_tag @user.avatar_variant(size: :medium), <%= image_tag image_url_for(@user.avatar), class: "h-24 w-24 rounded-lg" %>
class: "h-24 w-24 rounded-lg" %>
</p> </p>
<% end %> <% end %>
<div class="grow"> <div class="grow">