Sharing my experience of how I can not get a callback on model.destroy

Hi,
I’ve been trying to implement a logic for not deleting the image from remote location on model delete as the models are softdeleted and could be restored.

I would like to share my experience of how I am reading the documentation and why I find it difficult. I hope this could help.

Here is my model using AR

# app/models/content_picture.rb
class ContentPicture < ApplicationRecord
  include ImageUploader::Attachment(:image) # adds an `image` virtual attribute 
end

# app/shrine/image_uploader.rb
class ImageUploader < Shrine

  def activerecord_before_save
    puts "activerecord_before_save"
  end
end

# config/initializers/shrine.rb
Shrine.storages = { 
    cache: Shrine::Storage::Memory.new,
    store: Shrine::Storage::Memory.new,
  }
Shrine.plugin :activerecord # or :activerecord 

Looking at the documentation at

https://shrinerb.com/docs/plugins/activerecord#callbacks

There is a section for overriding the callback and an example of

class Shrine::Attacher
  def activerecord_after_save
    super
    # ... 
  end
end

So I must have an Shrine::Attacher defined in probably app/shrine/shrine_attacher.rb, but what is not clear for me how to attach this shrine_attacher to the image_uploader. I see the text

Including a Shrine::Attachment module into an ActiveRecord::Base subclass will:

    add model attachment methods
    add validations and callbacks to tie attachment process to the record lifecycle

and I’ve included ImageUploader::Attachment. I assume ImageUploader::Attachment does the same thing as Shrine::Attachment. But if I have an activerecord_before_save it never gets called on changing the record.

I’ve also read that

The attachment logic is handled by a Shrine::Attacher object. The Shrine::Attachment module simply provides a convenience layer around a Shrine::Attacher object, which can be accessed via the #<name>_attacher attribute.

So I could get the attacher by using content_picture.image_attacher. But how could I set my own attacher that has callbacks overridden.

Thanks.

Update

I do see from the code that the method activerecord_before_save is called on the attacher

.rvm/gems/ruby-2.6.5/gems/shrine-3.2.1/lib/shrine/plugins/activerecord.rb
   43:             [:create, :update].each do |action|
   44:               byebug
   45:               model.after_commit on: action do
   46:                 byebug
   47:                 if send(:"#{name}_attacher").changed?
=> 48:                   send(:"#{name}_attacher").send(:activerecord_after_save)
   49:                 end
   50:               end
   51:             end

So it is not called on the AR but on the Attacher. I am searching how could I add my own attacher.

Update 2

Should I override the attacher method of the Attachment.

.rvm/gems/ruby-2.6.5/gems/shrine-3.2.1/lib/shrine/plugins/model.rb
   56: 
   57:         # Memoizes the attacher instance into an instance variable.
   58:         def attacher(record, **options)
   59:           return super unless model?
   60: 
=> 61:           if !record.instance_variable_get(:"@#{@name}_attacher") || options.any?
   62:             attacher = class_attacher(**options)
   63:             attacher.load_model(record, @name)
   64: 
   65:             record.instance_variable_set(:"@#{@name}_attacher", attacher)

Update 3

This was the piece that I was missing from the doc and examples. That you should do define a new Attacher in the Uploader that is included with Uploader::Attachment, that would wrap the calls to the Attacher.

class ImageUploader < Shrine
  
  class Attacher < Shrine::Attacher
    def activerecord_before_save
      puts "here" # byebug
    end
  end

end

For anyone interested the magic happens at

.rvm/gems/ruby-2.6.5/gems/shrine-3.2.1/lib/shrine/plugins/entity.rb
   62:         # Creates an instance of the corresponding attacher class with set
   63:         # name.
   64:         def class_attacher(**options)
   65:           attacher = shrine_class::Attacher.new(**@options, **options)
   66:           attacher.instance_variable_set(:@name, @name)
=> 67:           attacher
   68:         end
   69:       end

Where on line 65 we are calling shrine_class::Attacher.new(**@options, **options) which would load an attacher that you can define in your Uploader extending Shrine.

Wow. This is quite a journey. I don’t have any specific suggestions around the code you’ve written, but did you see that there is a plugin specifically for your use-case?

The description specifically notes your environment – soft-deletes.

Walter

Thanks. I did not. I keep getting lost in the documentation. All my specific recommendations get marked as spam from the “community”. My experience reading the documentation is that is assumes one knows things that are obvious once you’ve done them, but are really hard to understand that first time. Like that you must define a class in a class as is the current case.

I will take a look at keep_files if it keeps the files the first time the record is destroyed, but removes them on full destruction.

I have tried keep_files. I can’t use it for my case. On full destroy it would be great to destroy the files completely. Just the first destroy should keep them. Probably this could be configured in a way.

I think you may have to do something out-of-band to handle your specific issue. Like a nightly sweep (scheduled with cron or similar) that looks for orphaned resources and deletes them from the store. You should already be doing something similar with the cache, otherwise you’ll be saving every file you ever upload, twice.

Walter

Thanks.

Clearing the ‘/cache’ folder regularly is easy. (they are prefixed with ‘/cache’)
Iterating every night over all the “pictures” in my case and looking which of them are not longer referred from the db is too brute force. These are a lot of resources and they only grow.

Yes, I think I should implement something in between not keeping the resource and keep_files

I would then do something in a before_destroy hook that added the soon-to-be-gone file to a queue, and then iterate over those. As you note, there’s no point in iterating over all of the live files to find the dead ones.

Walter

Here is my plan- posting here for future references if anybody is interested.
I am using acts_as_paranoid. The first time activerecord.destroy is called it sets the deleted_at column to the current time. You can then activerecord.recover and business continue as usual.

If you call activerecord.destroy and then activerecord.destroy it will destroy the record fully. So I plan to add a logic in the Attacher for activerecord_after_destroy. If the column deleted_at was just set, then I will skip calling super and thus skip deleting the files from the storage. If the actual destroy is happening, then after the activerecord is destroyed I will call super on the attacher and this will destroy the images on the storage.

Let’s see how this goes. Will try to post the code.

I am looking at the

class Shrine::Attacher
  def activerecord_after_save
    super
    # ... 
  end
end

But I can not understand how could I access the active record in this method. Anyone happen to know?

Found it. There is a ‘.record’ method. But I can not find it in the instance methods of https://shrinerb.com/rdoc/classes/Shrine/Attacher/InstanceMethods.html.

Is this record method an API?

For what it is worth, I tried. When the second full destroy happens then shrine activerecord_after_destroy is not called. I guess that shrine considers the record destroy after the first destroy and it does not call anything on the record.