kithe 1.1.2 → 2.0.0.pre.alpha1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bde82499d288e869bcdc6ca8155591e2dd2f5bbbd99da67fac721817eab72d6a
4
- data.tar.gz: 451c606f2840694677c01eac5af0bc2594e9dba98837cdc56701ffa0774f4ee4
3
+ metadata.gz: 2821832cedc85d2e7cf3b503dfdbbfb8786b92eb9797067362534e76dc926390
4
+ data.tar.gz: 71af9ce59d1a87339503b832da9d9fc4b2622850e2d554116fa3c024236c36c3
5
5
  SHA512:
6
- metadata.gz: 0de2c06c7a1f60d73da84697406a92350467ae0a8734a21e55ce2b29646a647fb5f06ef1ea0b58135758b86f8f48dbd5f95c1be92ef164ebf5e66429d1c3463d
7
- data.tar.gz: d910de267c9b7753f4879147c0bf77e893507d87df923e27722c47a6002acc95ffc5414751a5dedc0ccac48e8845aa2e95cc290ed670bdf3f7fef554a84c7182
6
+ metadata.gz: fc7d9197215d95a0386dac5fe4ccef822b92c3459afbfb898f6a3a7f48b20245200c97fef3ea7659778e7d29962f26a631ba47e33860a364dccc58bb0e9c0a17
7
+ data.tar.gz: e93788031c2a37533a20f6fcaba47d88cea0c1a4a3f0657a5cadff7e6711e376bf49cb7eab6ea2139edc1a330fc125731f4eecf170178198f0ba244f80680b1e
@@ -1,7 +1,10 @@
1
1
  module Kithe
2
2
  class AssetDeleteJob < Job
3
- def perform(data)
4
- AssetUploader::Attacher.delete(data)
3
+ def perform(attacher_class, data)
4
+ attacher_class = Object.const_get(attacher_class)
5
+
6
+ attacher = attacher_class.from_data(data)
7
+ attacher.destroy
5
8
  end
6
9
  end
7
10
  end
@@ -1,7 +1,17 @@
1
1
  module Kithe
2
2
  class AssetPromoteJob < Job
3
- def perform(data)
4
- AssetUploader::Attacher.promote(data)
3
+ # note we add a `promotion_directives` arg on the end, differing from standard
4
+ # shrine. It is a hash.
5
+ def perform(attacher_class, record_class, record_id, name, file_data, promotion_directives)
6
+ attacher_class = Object.const_get(attacher_class)
7
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
8
+
9
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
10
+ attacher.set_promotion_directives(promotion_directives)
11
+
12
+ attacher.atomic_promote
13
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
14
+ # attachment has changed or record has been deleted, nothing to do
5
15
  end
6
16
  end
7
17
  end
@@ -19,7 +19,7 @@ class Kithe::Asset < Kithe::Model
19
19
 
20
20
  # TODO we may need a way for local app to provide custom uploader class.
21
21
  # or just override at ./kithe/asset_uploader.rb locally?
22
- include Kithe::AssetUploader::Attachment.new(:file)
22
+ include Kithe::AssetUploader::Attachment(:file)
23
23
 
24
24
  # for convenience, let's delegate some things to shrine parts
25
25
  delegate :content_type, :original_filename, :size, :height, :width, :page_count,
@@ -42,8 +42,11 @@ class Kithe::Asset < Kithe::Model
42
42
  # to happen only after asset is promoted, like derivatives.
43
43
  define_model_callbacks :promotion
44
44
 
45
+ before_promotion :refresh_metadata_before_promotion
45
46
  after_promotion :schedule_derivatives
46
47
 
48
+
49
+
47
50
  # Establish a derivative definition that will be used to create a derivative
48
51
  # when #create_derivatives is called, for instance automatically after promotion.
49
52
  #
@@ -183,15 +186,15 @@ class Kithe::Asset < Kithe::Model
183
186
 
184
187
  # Runs the shrine promotion step, that we normally have in backgrounding, manually
185
188
  # and in foreground. You might use this if a promotion failed and you need to re-run it,
186
- # perhaps in bulk. It's also useful in tests.
189
+ # perhaps in bulk. It's also useful in tests. Also persists, using shrine `atomic_promote`.
187
190
  #
188
191
  # This will no-op unless the attached file is stored in cache -- that is, it
189
192
  # will no-op if the file has already been promoted. In this way it matches ordinary
190
193
  # shrine promotion. (Do we need an option to force promotion anyway?)
191
194
  #
192
- # Note that calling `file_attacher.promote` on it's own won't do quite the right thing,
193
- # and won't respect that the file is already cached.
194
- def promote(action: :store, context: {})
195
+ # Note that calling `file_attacher.promote` or `atomic_promote` on it's own won't do
196
+ # quite the same things.
197
+ def promote(action: :store, **context)
195
198
  return unless file_attacher.cached?
196
199
 
197
200
  context = {
@@ -199,7 +202,7 @@ class Kithe::Asset < Kithe::Model
199
202
  record: self
200
203
  }.merge(context)
201
204
 
202
- file_attacher.promote(file_attacher.get, **context)
205
+ file_attacher.atomic_promote(**context)
203
206
  end
204
207
 
205
208
  # The derivative creator sets metadata when it's created all derivatives
@@ -269,6 +272,11 @@ class Kithe::Asset < Kithe::Model
269
272
  end
270
273
  end
271
274
 
275
+ # Called by before_promotion hook
276
+ def refresh_metadata_before_promotion
277
+ file.refresh_metadata!(promoting: true)
278
+ end
279
+
272
280
  # Meant to be called in after_save hook, looks at activerecord dirty tracking in order
273
281
  # to removes all derivatives if the asset sha512 has changed
274
282
  def remove_invalid_derivatives
@@ -51,8 +51,7 @@ class Kithe::Asset::DerivativeUpdater
51
51
 
52
52
  # add our derivative key to context when uploading, so Kithe::DerivativeUploader can
53
53
  # use it if needed.
54
- uploaded_file = uploader.upload(io, record: deriv, metadata: metadata.merge(kithe_derivative_key: key))
55
-
54
+ uploaded_file = uploader.upload(io, record: deriv, metadata: metadata, kithe_derivative_key: key)
56
55
  optimistically_save_derivative(uploaded_file: uploaded_file, derivative: deriv)
57
56
  end
58
57
 
@@ -63,7 +62,7 @@ class Kithe::Asset::DerivativeUpdater
63
62
  # This method calls itself recursively to do that. Gives up after max_optimistic_tries,
64
63
  # at which point it'll just raise the constraint violation exception.
65
64
  def optimistically_save_derivative(uploaded_file:, derivative:, tries: 0)
66
- derivative.file_attacher.set(uploaded_file)
65
+ derivative.file_attacher.change(uploaded_file)
67
66
  save_deriv_ensuring_unchanged_asset(derivative)
68
67
  rescue ActiveRecord::RecordNotUnique => e
69
68
  if tries < max_optimistic_tries
@@ -30,6 +30,10 @@ module Kithe
30
30
  # so it can be submitted again.
31
31
  plugin :cached_attachment_data
32
32
 
33
+ # Used in a before_promotion hook to have our metadata extraction happen on
34
+ # promotion, possibly in the background.
35
+ plugin :refresh_metadata
36
+
33
37
  # Marcel analyzer is pure-ruby and fast. It's from Basecamp and is what
34
38
  # ActiveStorage uses. It is very similar to :mimemagic (and uses mimemagic
35
39
  # under the hood), but mimemagic seems not to be maintained with up to date
@@ -50,9 +54,10 @@ module Kithe
50
54
  end
51
55
 
52
56
  # Will save height and width to metadata for image types. (Won't for non-image types)
53
- plugin :store_dimensions
57
+ # ignore errors (often due to storing a non-image file), consistent with shrine 2.x behavior.
58
+ plugin :store_dimensions, on_error: :ignore
54
59
 
55
- # promotion and deletion will be in background.
60
+ # promotion and deletion will (sometimes) be in background.
56
61
  plugin :backgrounding
57
62
 
58
63
  # Useful in case consumers want it, and doesn't harm anything to be available.
@@ -63,16 +68,13 @@ module Kithe
63
68
  # feature can be used to make promotion not happen at all, or happen in foreground.
64
69
  # asset.file_attacher.set_promotion_directives(promote: false)
65
70
  # asset.file_attacher.set_promotion_directives(promote: "inline")
66
- Attacher.promote do |data|
67
- Kithe::TimingPromotionDirective.new(key: :promote, directives: data["promotion_directives"]) do |directive|
71
+ Attacher.promote_block do
72
+ Kithe::TimingPromotionDirective.new(key: :promote, directives: self.promotion_directives) do |directive|
68
73
  if directive.inline?
69
- # Foreground, but you'll still need to #reload your asset to see changes,
70
- # since backgrounding mechanism still reloads a new instance, sorry.
71
- #Kithe::AssetPromoteJob.perform_now(data)
72
- self.class.promote(data)
74
+ promote
73
75
  elsif directive.background?
74
- # What shrine normally expects for backgrounding
75
- Kithe::AssetPromoteJob.perform_later(data)
76
+ # What shrine normally expects for backgrounding, plus promotion_directives
77
+ Kithe::AssetPromoteJob.perform_later(self.class.name, record.class.name, record.id, name.to_s, file_data, self.promotion_directives)
76
78
  end
77
79
  end
78
80
  end
@@ -80,22 +82,19 @@ module Kithe
80
82
  # Delete using shrine backgrounding, but can be effected
81
83
  # by promotion_directives[:delete], similar to promotion above.
82
84
  # Yeah, not really a "promotion" directive, oh well.
83
- Attacher.delete do |data|
84
- Kithe::TimingPromotionDirective.new(key: :delete, directives: data["promotion_directives"]) do |directive|
85
+ Attacher.destroy_block do
86
+ Kithe::TimingPromotionDirective.new(key: :delete, directives: self.promotion_directives) do |directive|
85
87
  if directive.inline?
86
- self.class.delete(data)
88
+ destroy
87
89
  elsif directive.background?
88
90
  # What shrine normally expects for backgrounding
89
- Kithe::AssetDeleteJob.perform_later(data)
91
+ Kithe::AssetDeleteJob.perform_later(self.class.name, data)
90
92
  end
91
93
  end
92
94
  end
93
95
 
94
96
  plugin :add_metadata
95
97
 
96
- # So we can assign a hash representing cached file
97
- plugin :parsed_json
98
-
99
98
  # Makes files stored as /asset/#{asset_pk}/#{random_uuid}.#{original_suffix}
100
99
  plugin :kithe_storage_location
101
100
 
@@ -113,7 +112,7 @@ module Kithe
113
112
  # checksums until then anyway.
114
113
  plugin :signature
115
114
  add_metadata do |io, context|
116
- if context[:action] == :store
115
+ if context[:action] != :cache
117
116
  {
118
117
  md5: calculate_signature(io, :md5),
119
118
  sha1: calculate_signature(io, :sha1),
@@ -123,8 +122,12 @@ module Kithe
123
122
  end
124
123
  metadata_method :md5, :sha1, :sha512
125
124
 
126
- # This makes sure metadata is extracted on promotion, and also supports promotion
127
- # callbacks (before/after/around) on the Kithe::Asset classes.
128
- plugin :kithe_promotion_hooks
125
+
126
+ # Gives us (set_)promotion_directives methods on our attacher to
127
+ # house lifecycle directives, about whether promotion, deletion,
128
+ # derivatives happen in foreground, background, or not at all.
129
+ plugin :kithe_promotion_directives
130
+
131
+ plugin :kithe_promotion_callbacks
129
132
  end
130
133
  end
@@ -9,7 +9,10 @@ module Kithe
9
9
  plugin :activerecord
10
10
 
11
11
  plugin :determine_mime_type, analyzer: :marcel
12
- plugin :store_dimensions
12
+
13
+ # ignore error, often from storing a non-image file which can't have dimensions
14
+ # extracted. behavior consistent with shrine 2.x.
15
+ plugin :store_dimensions, on_error: :ignore
13
16
 
14
17
  # Useful in case consumers want it, and doesn't harm anything to be available.
15
18
  # https://github.com/shrinerb/shrine/blob/master/doc/plugins/rack_response.md
@@ -32,10 +35,11 @@ module Kithe
32
35
  def extract_metadata(io, context = {})
33
36
  result = super
34
37
 
35
- if context.dig(:metadata, :kithe_derivative_key) &&
38
+ if context[:kithe_derivative_key] &&
36
39
  context[:record]
37
40
  extension = MiniMime.lookup_by_content_type(result["mime_type"] || "")&.extension
38
- result["filename"] = "#{context[:record].asset.friendlier_id}_#{context[:metadata][:kithe_derivative_key]}.#{extension}"
41
+ result["filename"] = "#{context[:record].asset.friendlier_id}_#{context[:kithe_derivative_key]}.#{extension}"
42
+ result["kithe_derivative_key"] = context[:kithe_derivative_key]
39
43
  end
40
44
 
41
45
  return result
data/lib/kithe/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kithe
2
- VERSION = '1.1.2'
2
+ VERSION = '2.0.0-alpha1'
3
3
  end
@@ -8,7 +8,19 @@ class Shrine
8
8
  # It also supports specifying custom request headers for those remote URLs, to
9
9
  # support OAuth2 Authorization headers, with browse-everything or similar.
10
10
  #
11
- # It uses the shrine-url storage, registering it as storage with key "remote_url";
11
+ # model.attachment_column = {
12
+ # "storate" => "remote_url",
13
+ # "id" => "http://example.com/image.jpg",
14
+ # "headers" => {
15
+ # "Authorization" => "something"
16
+ # }
17
+ # }
18
+ #
19
+ # If you provide the (optional) "headers" key, they will wind up stored with
20
+ # file data in "metadata" hash, as "remote_url_headers". And they will be
21
+ # used with HTTP request to fetch the URL given, when promoting.
22
+ #
23
+ # The implementation uses the shrine-url storage, registering it as storage with key "remote_url";
12
24
  # our custom kithe_multi_cache plugin to allow this alternate storage to be set as
13
25
  # cache even though it's not main cache; and a #promote override suggested by
14
26
  # Janko@shrine to get request headers to be supported.
@@ -18,14 +30,14 @@ class Shrine
18
30
  # FUTURE. Need to whitelist allowed URLs/hostnames. :( A pain cause it'll
19
31
  # be different for different apps, so we need to allow uploader customization?
20
32
  class KitheAcceptRemoteUrl
33
+ STORAGE_KEY = :remote_url
34
+ METADATA_KEY = "remote_url_headers"
35
+
21
36
  def self.configure(uploader, options = {})
22
37
  # Ensure remote_url is registered as a storage
23
- # Note, using downloader: :net_http so it can be tested with WebMock, would be
24
- # better not to have to do that.
25
- # https://github.com/shrinerb/shrine-url/issues/5
26
38
  #
27
- # Lazy default to make it easier to specify other.
28
- uploader.storages[:remote_url] ||= Shrine::Storage::Url.new
39
+ # Lazy default to make it easier to specify other if an app wants to.
40
+ uploader.storages[STORAGE_KEY] ||= Shrine::Storage::Url.new
29
41
  end
30
42
 
31
43
  def self.load_dependencies(uploader, *)
@@ -34,17 +46,32 @@ class Shrine
34
46
  uploader.plugin :kithe_multi_cache, additional_cache: :remote_url
35
47
  end
36
48
 
49
+ module FileMethods
50
+ attr_reader :url_fetch_headers
51
+ def initialize(data)
52
+ # passed in as headers top-level key, but any but whitelisted keys actually
53
+ # end up being thrown out by shrine, and too hard to change that, so
54
+ # we'll copy it to 'metadata'
55
+ if data["storage"].to_s == STORAGE_KEY.to_s && data["headers"]
56
+ data["metadata"] ||= {}
57
+ data["metadata"][METADATA_KEY] = data["headers"]
58
+ end
59
+ super
60
+ end
61
+ end
62
+
37
63
  module AttacherMethods
64
+
38
65
  # Override to use 'headers' key in UploadedFile data for making remote request,
39
66
  # when remote_url is being supplied.
40
- def promote(cached_file = get, **options)
41
- if cached_file.storage_key.to_s == "remote_url" && cached_file.data["headers"]
67
+ def promote(storage: store_key, **options)
68
+ if file.storage_key.to_s == STORAGE_KEY.to_s && file.data.dig("metadata", METADATA_KEY)
42
69
  # instead of having Shrine "open" the file itself, we'll "open" it ourselves, so
43
70
  # we can add supplied headers. Due to the beauty of design of `down` and `shrine-url`,
44
71
  # and lazy opening, they'll end up using what we already opened. This approach
45
72
  # suggested by Janko.
46
73
  # https://groups.google.com/d/msg/ruby-shrine/SbeGujDa_k8/PeSGwpl9BAAJ
47
- cached_file.open(headers: cached_file.data["headers"])
74
+ file.open(headers: file.data.dig("metadata", METADATA_KEY))
48
75
  end
49
76
  super
50
77
  end
@@ -24,20 +24,17 @@ class Shrine
24
24
  # And the data can be saved, and the remote url (shrine-url) file will be promoted as usual,
25
25
  # even though it's not registered as the cache storage.
26
26
  #
27
- # NOTE: This implementation can be made a lot simpler once this PR is in a shrine release:
28
- # https://github.com/shrinerb/shrine/pull/319
29
- # https://github.com/shrinerb/shrine/commit/88c23d54814568b04987680f00b6b36f421c8d81
30
27
  module KitheMultiCache
31
28
  def self.configure(uploader, options = {})
32
- uploader.opts[:kithe_multi_cache_keys] = Array(options[:additional_cache]).collect(&:to_s)
29
+ uploader.opts[:kithe_multi_cache_keys] = Array(options[:additional_cache]).collect(&:to_sym)
33
30
  end
34
31
 
35
32
  # override #cache to lazily extend with our custom module. Kinda hacky,
36
33
  # but couldn't think of any other way to only extend the "cache" uploader,
37
34
  # and not the "store" uploader.
38
35
  module AttacherMethods
39
- def cached?(file = get)
40
- super || (file && shrine_class.opts[:kithe_multi_cache_keys].include?(file.storage_key))
36
+ def cached?(file = self.file)
37
+ super || (file && shrine_class.opts[:kithe_multi_cache_keys].include?(file.storage_key.to_sym))
41
38
  end
42
39
  end
43
40
  end
@@ -0,0 +1,82 @@
1
+ class Shrine
2
+ module Plugins
3
+ # We want ActiveSupport-style callbacks around "promotion" -- the shrine process of finalizing
4
+ # a file by moving it from 'cache' storage to 'store' storage.
5
+ #
6
+ # We want to suport after_promotion hooks, and before_promotion hooks, the before_promotion hooks
7
+ # should be able to cancel promotion. (A convenient way to do validation even with backgrounding promotion,
8
+ # although you'd want to record the validation fail somewhere)
9
+ #
10
+ # For now, the actual hooks are registered in the `Asset` activerecord model. This works because
11
+ # our Asset model only has ONE shrine attachment, it is backwards compatible with kithe 1.
12
+ # It might make more sense to have the callbacks on the Uploader itself, in the future though.
13
+ #
14
+ # We want to be able to register these callbacks, and have them invoked regardless of how
15
+ # promotion happens -- inline; in a background job; or even explicitly calling Asset#promote
16
+ #
17
+ # ## Weird implementation
18
+ #
19
+ # It's a bit hard to get this to happen in shrine architecture. We end up needing to assume
20
+ # activerecord and wrap the activerecord_after_save method (for inline promotion). And then also
21
+ # overwrite atomic_promote to get background promotion and other cases.
22
+ #
23
+ # Because getting this right required some shuffling around of where the wrapping happened, it
24
+ # was convenient and avoided confusion to isolate wrapping in a class method that can be used
25
+ # anywhere, and only depends on args passed in, no implicit state anywhere.
26
+ class KithePromotionCallbacks
27
+ # promotion logic differs somewhat in different modes of use (bg or inline promotion),
28
+ # so we extract the wrapping logic here. Exactly what the logic wrapped is can
29
+ # differ.
30
+ #
31
+ # Kithe::PromotionCallbacks.with_promotion_callbacks(record) do
32
+ # promotion_logic # sometimes `super`
33
+ # end
34
+ #
35
+ def self.with_promotion_callbacks(model)
36
+ # If callbacks haven't been skipped, and we have a model that implements
37
+ # callbacks, wrap yield in callbacks.
38
+ #
39
+ # Otherwise, just do it.
40
+ if ( !model.file_attacher.promotion_directives["skip_callbacks"] &&
41
+ model &&
42
+ model.class.respond_to?(:_promotion_callbacks) )
43
+ model.run_callbacks(:promotion) do
44
+ yield
45
+ end
46
+ else
47
+ yield
48
+ end
49
+ end
50
+
51
+ module AttacherMethods
52
+ # For INLINE promotion, we need to wrap this one in callbacks, in order to be
53
+ # wrapping enough to a) be able to cancel in `before`, and b) have `after`
54
+ # actually be running after promotion is complete and persisted.
55
+ #
56
+ # But we only want to do it here for 'inline' promotion mode. For 'false'
57
+ # disabled promotion, we don't want to run callbacks at all; and for 'background'
58
+ # this is too early, we want callbacks to run in bg job, not here. AND only
59
+ # if we're actually promoting, otherwise we don't want to run callbacks!
60
+ def activerecord_after_save
61
+ if self.promotion_directives["promote"] == "inline" && promote?
62
+ Shrine::Plugins::KithePromotionCallbacks.with_promotion_callbacks(record) do
63
+ super
64
+ end
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ # Wrapping atomic_promote in callbacks gets background promotion, since the shrine pattern
71
+ # for background job for promotion uses atomic_promote. It also gets any 'manual' use of atomic
72
+ # promote, such as from our Asset#promote method.
73
+ def atomic_promote(*args, **kwargs)
74
+ Shrine::Plugins::KithePromotionCallbacks.with_promotion_callbacks(record) do
75
+ super
76
+ end
77
+ end
78
+ end
79
+ end
80
+ register_plugin(:kithe_promotion_callbacks, KithePromotionCallbacks)
81
+ end
82
+ end
@@ -0,0 +1,111 @@
1
+ class Shrine
2
+ module Plugins
3
+ # This adds some features around shrine promotion that we found useful for dealing
4
+ # with backgrounding promotion.
5
+ #
6
+ # By default the kithe setup:
7
+ #
8
+ # * Does "promotion" in background job -- metadata extraction, and moving file from 'cache' to 'store'
9
+ # * Runs ActiveSupport-style callbacks around promotion (before_promotion, after_promotion)
10
+ # * Uses these callbacks to make sure we extract metadata on promotion (which shrine doesn't do by default)
11
+ # * Uses these callbacks to trigger our custom create_derivatives code *after* promotion
12
+ #
13
+ # There are times you want to customize these life cycle actions, either disabling them, or switching
14
+ # them from a background job to happen inline in the foreground. Some use cases for this are: 1) in
15
+ # automated testing; 2) when you are running a batch job (eg batch import), you might want to
16
+ # disable some expensive things per-record to instead do them all in batch at the end, or
17
+ # run them inline to keep from clogging up your bg job queue, and have better 'backpressure'.
18
+ #
19
+ # We provide something we call "promotion directives" to let you customize these. You can set
20
+ # them on a shrine Attacher; or on a Kithe `Asset` model individually, or globally on the class.
21
+ #
22
+ # ## Directives
23
+ #
24
+ # * `promote`: default `:background`; set to `:inline` to do promotion inline instead of a background
25
+ # job, or `false` to make promotion not happen automatically at all.
26
+ #
27
+ # * `skip_callbacks`: default `false`, set to `true` to disable our custom promotion callbacks
28
+ # entirely, including disabling our default callbacks such as derivative creation and
29
+ # promotion metadata extraction.
30
+ #
31
+ # * `create_derivatives`: default `background` (create a separate bg job). Also can be `false`
32
+ # to disable, or `inline` to create derivatives 'inline' when the after_promotion hook
33
+ # occurs -- which could already be in a bg job depending on `promote` directive!
34
+ #
35
+ # * `delete`: should _deletion_ of shrine attachment happen in a bg job? Default `:background`,
36
+ # can also be `false` (can't think of a good use case), or `:inline`.
37
+ #
38
+ # # Examples of setting
39
+ #
40
+ # ## Globally on Kithe::Asset
41
+ #
42
+ # Useful for batch processing or changing test defaults.
43
+ #
44
+ # Kithe::Asset.promotion_directives = { promote: :inline, create_derivatives: false }
45
+ #
46
+ # ## On a Kithe::Asset individual model
47
+ #
48
+ # asset = Kithe:Assst.new
49
+ # asset.set_promotion_directives(create_derivatives: :inline)
50
+ #
51
+ # (Aggregates on top of whatever was set at class level of previously set with `Asset#set_promotion_directives)`,
52
+ # does not replace previously settings but merges into them!
53
+ #
54
+ # ## Directly on a shrine attacher
55
+ #
56
+ # some_asset.file = some_assignable_file
57
+ # some_asset.file_attacher.set_promotion_directives(skip_callbacks: true)
58
+ # some_asset.save!
59
+ #
60
+ # (Aggregates on top of whatever was already set, merges into it, does not replace!)
61
+ #
62
+ # ## Checking current settings
63
+ #
64
+ # some_asset.promotion_directives
65
+ #
66
+ # or
67
+ #
68
+ # some_asset.file_attacher.promotion_directives
69
+ #
70
+ class KithePromotionDirectives
71
+ # whitelist of allowed promotion_directive keys, so we can raise on typos but still
72
+ # be extensible. Also serves as some documentation of what directives available.
73
+ class_attribute :allowed_promotion_directives,
74
+ instance_writer: false,
75
+ default: [:promote, :skip_callbacks, :create_derivatives, :delete]
76
+
77
+ module AttacherMethods
78
+
79
+ # Set one or more promotion directives, stored context[:promotion_directives], that
80
+ # will be serialized and restored to context for bg promotion. The values are intended
81
+ # to be simple strings or other json-serializable primitives.
82
+ #
83
+ # set_promotion_directives will merge it's results into existing promotion directives,
84
+ # existing keys will remain. So you can set multiple directives with multiple
85
+ # calls to set_promotion_directives, or pass multiple keys to one calls.
86
+ #
87
+ # @example
88
+ # some_model.file_attacher.set_promotion_directives(skip_callbacks: true)
89
+ # some_model.save!
90
+ def set_promotion_directives(hash)
91
+ # ActiveJob sometimes has trouble if there are symbols in there, somewhat
92
+ # unpredictably.
93
+ hash = hash.collect { |k, v| [k.to_s, v === Symbol ? v.to_s : v.to_s]}.to_h
94
+
95
+ unrecognized = hash.keys.collect(&:to_sym) - KithePromotionDirectives.allowed_promotion_directives
96
+ unless unrecognized.length == 0
97
+ raise ArgumentError.new("Unrecognized promotion directive key: #{unrecognized.join('')}")
98
+ end
99
+
100
+ promotion_directives.merge!(hash)
101
+ end
102
+
103
+ # context[:promotion_directives], lazily initializing to hash for convenience.
104
+ def promotion_directives
105
+ context[:promotion_directives] ||= {}
106
+ end
107
+ end
108
+ end
109
+ register_plugin(:kithe_promotion_directives, KithePromotionDirectives)
110
+ end
111
+ end