Class: Shrine::Plugins::KithePromotionCallbacks

Inherits:
Object
  • Object
show all
Defined in:
lib/shrine/plugins/kithe_promotion_callbacks.rb

Overview

We want ActiveSupport-style callbacks around “promotion” – the shrine process of finalizing a file by moving it from ‘cache’ storage to ‘store’ storage.

We want to suport after_promotion hooks, and before_promotion hooks, the before_promotion hooks should be able to cancel promotion. (A convenient way to do validation even with backgrounding promotion, although you’d want to record the validation fail somewhere)

For now, the actual hooks are registered in the ‘Asset` activerecord model. This works because our Asset model only has ONE shrine attachment, it is backwards compatible with kithe 1. It might make more sense to have the callbacks on the Uploader itself, in the future though.

We want to be able to register these callbacks, and have them invoked regardless of how promotion happens – inline; in a background job; or even explicitly calling Asset#promote

## Weird implementation

It’s a bit hard to get this to happen in shrine architecture. We end up needing to assume activerecord and wrap the activerecord_after_save method (for inline promotion). And then also overwrite atomic_promote to get background promotion and other cases.

Because getting this right required some shuffling around of where the wrapping happened, it was convenient and avoided confusion to isolate wrapping in a class method that can be used anywhere, and only depends on args passed in, no implicit state anywhere.

## Sharing same tempfile for processing

If we have maybe multiple before_promotion hooks that all need an on-disk file, PLUS maybe multiple ‘add_metadata` hooks that need an on-desk file – we ABSOLUTELY do NOT each to do a separate copy/download from possibly remote cloud source!

The default shrine implementation using Down::ChunkedIO may avoid that (not necessarily as a contract!), BUT still makes multiple local copies, which can still be a performance issue for large files.

The solution is the [Shrine tempfile plugin](shrinerb.com/docs/plugins/tempfile), which requires the UploadedFile to be opened around all uses of ‘Shrine.with_file`.

We include some pretty kludgey code to make this a thing.

To take advantage of it, you DO need to add ‘Shrine.plugin :tempfile` in your app, we don’t want to force this global on apps!

Note after_promotion hooks won’t share the common tempfile, since the UploadedFile location has been changed from the one we opened!

Defined Under Namespace

Modules: AttacherMethods

Class Method Summary collapse

Class Method Details

.load_dependencies(uploader) ⇒ Object



48
49
50
# File 'lib/shrine/plugins/kithe_promotion_callbacks.rb', line 48

def self.load_dependencies(uploader, *)
  uploader.plugin :kithe_promotion_directives
end

.with_promotion_callbacks(model) ⇒ Object

promotion logic differs somewhat in different modes of use (bg or inline promotion), so we extract the wrapping logic here. Exactly what the logic wrapped is can differ.

Kithe::PromotionCallbacks.with_promotion_callbacks(record) do
   promotion_logic # sometimes `super`
end

This also contains some pretty kludgey code to open the underlying Shrine::UploadedFile around the before_promotion hooks AND promotion. When combined with Shrine tempfile plugin, this means all ‘before_promotion` hooks and `add_metadata` hooks can use `Shrine.with_file` to get the



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/shrine/plugins/kithe_promotion_callbacks.rb', line 64

def self.with_promotion_callbacks(model)
  # only if Shrine::UploadedFile isn't *already* open we definitely want
  # to open it and keep it open -- so WITH Shrine tempfile plugin, we can have
  # various hooks sharing the same tempfile instead of making multiple copies.
  unless model.file_attacher.file.opened?
    model.file_attacher.file.open
    self_opened_file = model.file_attacher.file
  end


  # If callbacks haven't been skipped, and we have a model that implements
  # callbacks, wrap yield in callbacks.
  #
  # Otherwise, just do it.
  if (  !model.file_attacher.promotion_directives["skip_callbacks"] &&
        model &&
        model.class.respond_to?(:_promotion_callbacks) )

    model.run_callbacks(:promotion) do
      yield
    end

  else
    yield
  end
ensure
  # only if we DID open the Shrine::UploadedFile ourselves, for the purpose of
  # using a common tempfile with Shrine tempfile plugin,
  # make sure to clean up after ourselves by closing it.
  self_opened_file.close if self_opened_file && self_opened_file.opened?
end