[SOLVED] How can I enumerate all of the Shrine attachments in my models?

Hello,

I’ve just rewritten all the generate_location methods for my Shrine uploaders. I’m working on a rake task that will copy all of my uploaded files into their new locations, and then schedule a background job to delete all of the old files after 7 days. (Including a “dry” mode that just prints all of the actions first.)

I would like to use some metaprogramming to enumerate all of the Shrine attachments for all of my models. So far I’ve figured out that I can call: Shrine.subclasses, but then I don’t know how to get any more information out of each Shrine subclass.

I’m digging around the source code on GitHub, and I saw how the Attachment module is generated and included. I’m just struggling to figure out how I can find the model class and attachment name.

Thanks for your help!

Hahaha this is a really gross hack, but it works!

shrine_attachments = []
Dir.glob(Rails.root.join('app', 'models', '**', '*.rb')).each do |model_file|
  model_source = File.read(model_file)
  model_name = File.basename(model_file, '.rb').camelize
  model_source.scan(
    /include ([\w]+Uploader)\.attachment\(:([^)]+)\)/
  ).each do |result|
    shrine_attachments << {
      model_class: model_name,
      uploader_class: result[0],
      attachment_name: result[1],
    }
  end
end

You can use metaprogramming on your Active Record models to make it a bit simpler:

shrine_attachments = ActiveRecord::Base.subclasses.flat_map do |model|
  model.ancestors.grep(Shrine::Attachment) do |attachment|
    {
      model_class:     model,
      upload_class:    attachment.shrine_class,
      attachment_name: attachment.attachment_name,
    }
  end
end

Oh that’s much better, thank you! I use ApplicationRecord in my application, and the grep didn’t seem to work, so I used select + is_a?. The last thing was to make sure all models are loaded while testing in development/test. So this is working for me:

if Rails.env.development? || Rails.env.test?
  Rails.application.eager_load!
end

shrine_attachments = ApplicationRecord.subclasses.flat_map do |model|
  model.ancestors.select { |c| c.is_a? Shrine::Attachment }.map do |attachment|
    {
      model_class: model,
      upload_class: attachment.shrine_class,
      attachment_name: attachment.attachment_name,
    }
  end
end

I just have a few more questions about this file migration task:

  1. How could I check to see if the attachment’s location matches the current output of the #generate_location method? (including uploaders that use the :versions plugin.) Is there a single method I can call, something like record.file.location_changed?

  2. Do you have any advice on how I could replace the attachment without deleting the original file, and then scheduling the deletion job for the future?

I’ve only been providing presigned URLs for S3, so every S3 URL will expire after 1 hour. That means that none of my users have ongoing access to any files in the S3 bucket, so I’m free to move them to new locations. However, they might need to access the old URLs for up to one hour. I thought it would also be good to keep the old files for a week, just in case something goes wrong. I realized that I could do this by sending a bunch of scheduled deletion jobs into Sidekiq.

But if I do this, the original files are deleted immediately:

    record.file_attacher.copy(record.file_attacher)
    record.save

I know how to manually call my Sidekiq deletion job with perform_at, but I can’t figure out how to save the record without deleting the previous attachment.

Thanks for your help!

  1. How could I check to see if the attachment’s location matches the current output of the #generate_location method?

You can do a regex match on the file’s location:

record.file.id.match?(/.../)
  1. Do you have any advice on how I could replace the attachment without deleting the original file, and then scheduling the deletion job for the future?

Hmm, deletion of replaced files should use the same background job as the deletion of files whose record has been deleted, so you should be able to use job scheduling :thinking: (even with the copy plugin)

If you’re using Shrine 3.0.0.rc, there is a guide with some advices on moving files to a new location. It doesn’t provide answers for your specific problem, but maybe it will be of some help.

Phew, I finally finished my Shrine file migration task: https://gist.github.com/ndbroadbent/ab4f08c4dff55665681317f2a034bff8

I just needed to inject the keep_files plugin into the uploader class, so that the original file wouldn’t be deleted automatically (and then remove that behavior afterwards.)

I also added a complete set of integration tests that cover all of the Shrine attachments in my application, so hopefully I didn’t miss anything! Will try it out on my staging server now.

Thanks a lot for your help!

P.S. It’s currently very difficult to disable/enable any specific plugins, for example if you need to prevent one file from being deleted, or copy something synchronously after setting up the backgrounding plugin. So it would be nice if some of these things could be a bit more ergonomic. (And please let me know if there are some easier ways to achieve the same results.)