How to make derivative links still work using existing saved data when uploaded file has error?

So in my Rails app I have this ActiveRecord model:

# app/models/user.rb
class User < ApplicationRecord
  include UserAvatarUploader::Attachment(:avatar)

  before_save :save_avatar_derivatives, if: Proc.new{|x| x.avatar_changed? && x.errors[:avatar].empty? }

  private

  def save_avatar_derivatives
    self.avatar_derivatives!
  end
end

Using this uploader:

# app/uploaders/user_avatar_uploader.rb
require "image_processing/mini_magick"

class UserAvatarUploader < Shrine
  plugin :default_storage,
    cache: :user_avatar_cache,
    store: :user_avatar_store
  plugin :store_dimensions
  plugin :remote_url, max_size: 20*1024*1024 # 20MB
  plugin :validation_helpers
  plugin :determine_mime_type
  plugin :derivatives

  Attacher.validate do
    validate_min_size 1, message: "must not be empty"
    validate_max_size 5*1024*1024, message: "is too large (max is 5 MB)"
    validate_mime_type_inclusion %w[image/jpeg image/png]
  end

  Attacher.derivatives do |original|
    magick = ImageProcessing::MiniMagick.source(original)

    {
      :"64" =>  magick.resize_to_fill!( 64,  64, gravity: "Center"),
      :"128" => magick.resize_to_fill!(128, 128, gravity: "Center"),
      :"256" => magick.resize_to_fill!(256, 256, gravity: "Center"),
      :"512" => magick.resize_to_fill!(512, 512, gravity: "Center"),
    }
  end
end

Everything works fine with valid images, but when the user uploads say a .gif, which doesn’t match the allowed mime types, I correctly get an error message about it. So far, so good.

However, the problem is that in my navbar, I have a link to the user’s profile pic like this:

<img src="<%= current_user.avatar_url(:"256") %>" alt="Avatar image">

So when I re-render my Rails form_with(@user) while the uploaded avatar has an error, the navbar derivative and the <input type="file"> image preview are both broken due to the invalid @user.avatar.

One thing I tried is resetting the @user.avatar and @user.avatar_data to @user.avatar_was and @user.avatar_data_was, but that didn’t work.

Any idea on how I could change my approach to make this work?

Edit: this is using the latest RubyGems version of Shrine (3.3.0) on Rails 6.1.1

Unsure if this will solve your particular problem:

  • have you included the dirty state tracking in your user.rb file - to allow the avatar_data_was to be there?
# user.rb
class User
   include ActiveModel::Dirty
end

Also, I suspect that you could possibly move your validation logic to within the Attacher.validate block in your UserAvatarUploader…but i’m speaking in ignorance here.

Thanks for taking a stab at it but I don’t need to do that since this is ActiveRecord, not ActiveModel. Dirty tracking is already enabled without extra steps.

I can successfully set the avatar_data to avatar_data_was, but when I call .avatar_derivatives it still shows me an empty hash.

> @user.valid?
=> false

> @user.errors[:avatar]
=> ["type must be one of: image/jpeg, image/png"]

> @user.avatar_derivatives
=> {}

> @user.avatar
=> #<UserAvatarUploader::UploadedFile storage=:user_avatar_cache id="bb0a86ffecc6eae581a49baef0aedf84.gif" metadata={"filename"=>"2.gif", "size"=>21021, "mime_type"=>"image/gif", "width"=>139, "height"=>151}>

> @user.avatar_was
NoMethodError: undefined method `avatar_was' for #<ProspectiveUser:0x000055bac80b20d8>
Did you mean?  avatar_data
               avatar_url

> @user.avatar_data
=> {"id"=>"bb0a86ffecc6eae581a49baef0aedf84.gif",
 "storage"=>"user_avatar_cache",
 "metadata"=>
  {"filename"=>"2.gif",
   "size"=>21021,
   "mime_type"=>"image/gif",
   "width"=>139,
   "height"=>151}}

> @user.avatar_data_was
=> {"id"=>"a5b660f74987155535ea8535d3fb3716.jpg",
 "storage"=>"user_avatar_store",
 "metadata"=>
  {"size"=>34840,
   "width"=>354,
   "height"=>499,
   "filename"=>"51ls340iX3L._SX352_BO1,204,203,200_.jpg",
   "mime_type"=>"image/jpeg"},
 "derivatives"=>
  {"64"=>
    {"id"=>"e1f1eb48ca9cd585213c7d46eb52bbb7.jpg",
     "storage"=>"user_avatar_store",
     "metadata"=>
      {"size"=>2003,
       "width"=>64,
       "height"=>64,
       "filename"=>"image_processing20210223-1999612-xmcgsh.jpg",
       "mime_type"=>"image/jpeg"}},
   "128"=>
    {"id"=>"ab7420848bd076af9f873ed2e44fd9dd.jpg",
     "storage"=>"user_avatar_store",
     "metadata"=>
      {"size"=>6021,
       "width"=>128,
       "height"=>128,
       "filename"=>"image_processing20210223-1999612-2eb093.jpg",
       "mime_type"=>"image/jpeg"}},
   "256"=>
    {"id"=>"4866122fd83b8cc92079816392808150.jpg",
     "storage"=>"user_avatar_store",
     "metadata"=>
      {"size"=>16585,
       "width"=>256,
       "height"=>256,
       "filename"=>"image_processing20210223-1999612-1mkh79s.jpg",
       "mime_type"=>"image/jpeg"}},
   "512"=>
    {"id"=>"50c9406e72d0e50ec1e5540b5e839119.jpg",
     "storage"=>"user_avatar_store",
     "metadata"=>
      {"size"=>40261,
       "width"=>512,
       "height"=>512,
       "filename"=>"image_processing20210223-1999612-nsh7qu.jpg",
       "mime_type"=>"image/jpeg"}}}}

> @user.avatar_data = @user.avatar_data_was
> @user.avatar_derivatives
=> {}

> @user.reload
=> #<User id: 15, ...

> @user.avatar_derivatives
=> {:"64"=>
  #<UserAvatarUploader::UploadedFile storage=:user_avatar_store id="e1f1eb48ca9cd585213c7d46eb52bbb7.jpg" metadata={"size"=>2003, "width"=>64, "height"=>64, "filename"=>"image_processing20210223-1999612-xmcgsh.jpg", "mime_type"=>"image/jpeg"}>,
 :"128"=>
  #<UserAvatarUploader::UploadedFile storage=:user_avatar_store id="ab7420848bd076af9f873ed2e44fd9dd.jpg" metadata={"size"=>6021, "width"=>128, "height"=>128, "filename"=>"image_processing20210223-1999612-2eb093.jpg", "mime_type"=>"image/jpeg"}>,
 :"256"=>
  #<UserAvatarUploader::UploadedFile storage=:user_avatar_store id="4866122fd83b8cc92079816392808150.jpg" metadata={"size"=>16585, "width"=>256, "height"=>256, "filename"=>"image_processing20210223-1999612-1mkh79s.jpg", "mime_type"=>"image/jpeg"}>,
 :"512"=>
  #<UserAvatarUploader::UploadedFile storage=:user_avatar_store id="50c9406e72d0e50ec1e5540b5e839119.jpg" metadata={"size"=>40261, "width"=>512, "height"=>512, "filename"=>"image_processing20210223-1999612-nsh7qu.jpg", "mime_type"=>"image/jpeg"}>}

Not sure if this will work for you - I was receiving a parser error but I"ll post nonetheless:

# now you have the attacher as it was BEFORE the validation error:
attacher = UserAvatarUploader::Attacher.from_data(@user.avatar_data_was)

# simply access the derivatives you want
attacher.derivatives # => {:small=>#<MyUploader::UploadedFile storage=:store id="d507ce9311e3913499a3d2c284ad083b.png" metadata={"filename"=>"image_processing20210225-2807-k5tdz6.png", "size"=>999, "mime_type"=>nil}>, :medium=>#<MyUploader::UploadedFile storage=:store id="6d79d7628fccb361e5fada692c59d95b.png" metadata={"filename"=>"image_processing20210225-2807-ko35ox.png", "size"=>1755, "mime_type"=>nil}>, :large=>#<MyUploader::UploadedFile storage=:store id="ec597cef1881955600b3b917b3ffd4ce.png" metadata={"filename"=>"image_processing20210225-2807-1s3f7i3.png", "size"=>1893, "mime_type"=>nil}>}

You might want to double check that nothing crazy happens by this.

Perhaps there is a better way?

1 Like

Cool, thanks, that led me to a solution that seems to work for now. Added this stuff to my model:

after_validation :revert_avatar_data, if: ->(x){ x.errors[:avatar].present? && x.avatar_data_changed? }

private

def revert_avatar_data
  self.avatar_attacher.load_data(self.avatar_data_was)
end

I too am curious if there’s a different solution/approach but for now this is better than having broken <img>s in my navbar. Thanks!

If you want to automatically revert the invalid file that was attached when redisplaying the form, the remove_invalid plugin should take care of that.

1 Like

Son of a… can’t believe I missed that :man_facepalming: Thanks!