kithe 1.1.2 → 2.0.0.pre.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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