shrine 3.0.0.alpha → 3.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -2
  3. data/README.md +1 -1
  4. data/doc/creating_persistence_plugins.md +20 -63
  5. data/doc/plugins/activerecord.md +13 -106
  6. data/doc/plugins/add_metadata.md +31 -9
  7. data/doc/plugins/atomic_helpers.md +33 -9
  8. data/doc/plugins/cached_attachment_data.md +3 -3
  9. data/doc/plugins/data_uri.md +16 -19
  10. data/doc/plugins/default_storage.md +32 -8
  11. data/doc/plugins/default_url.md +21 -9
  12. data/doc/plugins/derivation_endpoint.md +48 -0
  13. data/doc/plugins/derivatives.md +74 -63
  14. data/doc/plugins/entity.md +27 -2
  15. data/doc/plugins/included.md +8 -7
  16. data/doc/plugins/metadata_attributes.md +20 -5
  17. data/doc/plugins/model.md +22 -2
  18. data/doc/plugins/persistence.md +89 -0
  19. data/doc/plugins/remote_url.md +41 -45
  20. data/doc/plugins/remove_attachment.md +23 -4
  21. data/doc/plugins/remove_invalid.md +3 -4
  22. data/doc/plugins/restore_cached_data.md +3 -1
  23. data/doc/plugins/sequel.md +13 -105
  24. data/doc/plugins/signature.md +3 -3
  25. data/lib/shrine/attachment.rb +11 -1
  26. data/lib/shrine/plugins/_persistence.rb +69 -0
  27. data/lib/shrine/plugins/activerecord.rb +31 -81
  28. data/lib/shrine/plugins/atomic_helpers.rb +13 -3
  29. data/lib/shrine/plugins/backgrounding.rb +8 -8
  30. data/lib/shrine/plugins/cached_attachment_data.rb +2 -6
  31. data/lib/shrine/plugins/data_uri.rb +2 -6
  32. data/lib/shrine/plugins/default_storage.rb +28 -2
  33. data/lib/shrine/plugins/derivation_endpoint.rb +3 -9
  34. data/lib/shrine/plugins/derivatives.rb +26 -17
  35. data/lib/shrine/plugins/entity.rb +24 -16
  36. data/lib/shrine/plugins/included.rb +1 -0
  37. data/lib/shrine/plugins/infer_extension.rb +2 -0
  38. data/lib/shrine/plugins/metadata_attributes.rb +18 -8
  39. data/lib/shrine/plugins/model.rb +35 -14
  40. data/lib/shrine/plugins/remote_url.rb +2 -6
  41. data/lib/shrine/plugins/remove_attachment.rb +2 -6
  42. data/lib/shrine/plugins/remove_invalid.rb +10 -6
  43. data/lib/shrine/plugins/sequel.rb +31 -78
  44. data/lib/shrine/plugins/upload_options.rb +2 -2
  45. data/lib/shrine/plugins/validation.rb +1 -11
  46. data/lib/shrine/version.rb +1 -1
  47. metadata +4 -2
@@ -7,40 +7,58 @@ location.
7
7
  plugin :remote_url, max_size: 20*1024*1024
8
8
  ```
9
9
 
10
- If for example your attachment is called "avatar", this plugin will add
11
- `#avatar_remote_url` and `#avatar_remote_url=` methods to your model.
10
+ The plugin will add the `#<name>_remote_url` writer to your model, which
11
+ downloads the remote file and uploads it to temporary storage.
12
12
 
13
13
  ```rb
14
- user.avatar #=> nil
15
- user.avatar_remote_url = "http://example.com/cool-image.png"
16
- user.avatar #=> #<Shrine::UploadedFile>
17
-
18
- user.avatar.mime_type #=> "image/png"
19
- user.avatar.size #=> 43423
20
- user.avatar.original_filename #=> "cool-image.png"
14
+ photo.image_remote_url = "http://example.com/cool-image.png"
15
+ photo.image.mime_type #=> "image/png"
16
+ photo.image.size #=> 43423
17
+ photo.image.original_filename #=> "cool-image.png"
21
18
  ```
22
19
 
23
- You can also use `#remote_url=` and `#remote_url` methods directly on the
24
- `Shrine::Attacher`:
20
+ If you're using `Shrine::Attacher` directly, you can use
21
+ `Attacher#assign_remote_url`:
25
22
 
26
23
  ```rb
27
- attacher.remote_url = "http://example.com/cool-image.png"
24
+ attacher.assign_remote_url("http://example.com/cool-image.png")
25
+ attacher.file.mime_type #=> "image/png"
26
+ attacher.file.size #=> 43423
27
+ attacher.file.original_filename #=> "cool-image.png"
28
28
  ```
29
29
 
30
+ ## Downloading
31
+
30
32
  By default, the file will be downloaded using `Down.download` from the [Down]
31
- gem. This will use the `Down::NetHttp` backend by default, which is a wrapper
33
+ gem. This will use the [Down::NetHttp] backend by default, which is a wrapper
32
34
  around [open-uri].
33
35
 
34
- ## Dynamic options
35
-
36
- You can dynamically pass options to the downloader by using
37
- `Attacher#assign_remote_url`:
36
+ You can pass options to the downloader via the `:downloader` option:
38
37
 
39
38
  ```rb
40
39
  attacher.assign_remote_url(url, downloader: { 'Authorization' => 'Basic ...' })
41
40
  ```
42
41
 
43
- You can also pass any other `Shrine#upload` options:
42
+ You can also change the downloader:
43
+
44
+ ```rb
45
+ # Gemfile
46
+ gem "http"
47
+ ```
48
+ ```rb
49
+ require "down/http"
50
+
51
+ down = Down::Http.new do |client|
52
+ client.follow(max_hops: 2).timeout(connect: 2, read: 2)
53
+ end
54
+
55
+ plugin :remote_url, downloader: down.method(:download), ...
56
+ ```
57
+
58
+ ## Uploader options
59
+
60
+ Any additional options passed to `Attacher#assign_remote_url` will be forwarded
61
+ to `Attacher#assign` (and `Shrine#upload`):
44
62
 
45
63
  ```rb
46
64
  attacher.assign_remote_url(url, metadata: { "mime_type" => "text/plain" })
@@ -63,28 +81,6 @@ you don't want to limit the maximum file size, you can set `:max_size` to nil:
63
81
  plugin :remote_url, max_size: nil
64
82
  ```
65
83
 
66
- ## Custom downloader
67
-
68
- If you want to customize how the file is downloaded, you can override the
69
- `:downloader` parameter and provide your own implementation. For example, you
70
- can use the [http.rb] Down backend for downloading:
71
-
72
- ```rb
73
- # Gemfile
74
- gem "http"
75
- ```
76
- ```rb
77
- require "down/http"
78
-
79
- down = Down::Http.new do |client|
80
- client
81
- .follow(max_hops: 2)
82
- .timeout(connect: 2, read: 2)
83
- end
84
-
85
- plugin :remote_url, max_size: 20*1024*1024, downloader: down.method(:download)
86
- ```
87
-
88
84
  ## Errors
89
85
 
90
86
  If download errors, the error is rescued and a validation error is added equal
@@ -103,11 +99,10 @@ file ID, and pair that with the `backgrounding` plugin.
103
99
 
104
100
  ## File extension
105
101
 
106
- When attaching from a remote URL, the uploaded file location will have the
107
- extension inferred from the URL. However, some URLs might not have an
108
- extension, in which case the uploaded file location also won't have the
109
- extension. If you want the upload location to always have an extension, you can
110
- load the `infer_extension` plugin to infer it from the MIME type.
102
+ When attaching from a remote URL, the uploaded file location will inherit the
103
+ extension from the URL. However, some URLs might not have an extension. To
104
+ handle this case, you can use the `infer_extension` plugin to infer the
105
+ extension from the MIME type.
111
106
 
112
107
  ```rb
113
108
  plugin :infer_extension
@@ -158,6 +153,7 @@ plugin :remote_url, log_subscriber: nil
158
153
 
159
154
  [remote_url]: /lib/shrine/plugins/remote_url.rb
160
155
  [Down]: https://github.com/janko/down
156
+ [Down::NetHttp]: https://github.com/janko/down#downnethttp
161
157
  [open-uri]: https://ruby-doc.org/stdlib/libdoc/open-uri/rdoc/OpenURI.html
162
158
  [http.rb]: https://github.com/httprb/http
163
159
  [shrine-url]: https://github.com/shrinerb/shrine-url
@@ -7,12 +7,31 @@ attachments through checkboxes on the web form.
7
7
  plugin :remove_attachment
8
8
  ```
9
9
 
10
- If for example your attachment is called "avatar", this plugin will add
11
- `#remove_avatar` and `#remove_avatar=` methods to your model. This allows you
12
- to add a form field for removing attachments:
10
+ The plugin adds the `#remove_<name>` accessor to your model, which removes the
11
+ attached file if it receives a truthy value:
13
12
 
14
13
  ```rb
15
- form.check_box :remove_avatar
14
+ photo.image #=> #<Shrine::UploadedFile>
15
+ photo.remove_image = 'true'
16
+ photo.image #=> nil
17
+ ```
18
+
19
+ This allows you to add a checkbox form field for removing attachments:
20
+
21
+ ```rb
22
+ form_for photo do |f|
23
+ # ...
24
+ f.check_box :remove_image
25
+ end
26
+ ```
27
+
28
+ If you're using the `Shrine::Attacher` directly, you can use the
29
+ `Attacher#remove` accessor:
30
+
31
+ ```rb
32
+ attacher.file #=> #<Shrine::UploadedFile>
33
+ attacher.remove = '1'
34
+ attacher.file #=> nil
16
35
  ```
17
36
 
18
37
  [remove_attachment]: /lib/shrine/plugins/remove_attachment.rb
@@ -1,9 +1,8 @@
1
1
  # Remove Invalid
2
2
 
3
- The [`remove_invalid`][remove_invalid] plugin automatically deletes a new
4
- assigned file if it was invalid and deassigns it from the record. If there was
5
- a previous file attached, it will be assigned back, otherwise no attachment
6
- will be assigned.
3
+ The [`remove_invalid`][remove_invalid] plugin automatically deletes and
4
+ deassigns a new assigned file if it was invalid. If there was a previous file
5
+ attached, it will be assigned back.
7
6
 
8
7
  ```rb
9
8
  plugin :remove_invalid
@@ -11,6 +11,8 @@ extracted on the client side.
11
11
  plugin :restore_cached_data
12
12
  ```
13
13
 
14
- It uses the `refresh_metadata` plugin to re-extract metadata.
14
+ It uses the [`refresh_metadata`][refresh_metadata] plugin to re-extract
15
+ metadata.
15
16
 
16
17
  [restore_cached_data]: /lib/shrine/plugins/restore_cached_data.rb
18
+ [refresh_metadata]: /doc/plugins/refresh_metadata.md#readme
@@ -21,7 +21,7 @@ end
21
21
 
22
22
  ### Callbacks
23
23
 
24
- #### Save
24
+ #### After Save
25
25
 
26
26
  After a record is saved and the transaction is committed, `Attacher#finalize`
27
27
  is called, which promotes cached file to permanent storage and deletes previous
@@ -37,7 +37,7 @@ photo.save
37
37
  photo.image.storage_key #=> :store
38
38
  ```
39
39
 
40
- #### Destroy
40
+ #### After Destroy
41
41
 
42
42
  After a record is destroyed and the transaction is committed,
43
43
  `Attacher#destroy_attached` method is called, which deletes stored attached
@@ -52,7 +52,7 @@ photo.destroy
52
52
  photo.image.exists? #=> false
53
53
  ```
54
54
 
55
- #### Skipping
55
+ #### Skipping Callbacks
56
56
 
57
57
  If you don't want the attachment module to add any callbacks to your Sequel
58
58
  model, you can set `:callbacks` to `false`:
@@ -82,7 +82,7 @@ photo.valid?
82
82
  photo.errors #=> { image: ["size must not be greater than 10.0 MB"] }
83
83
  ```
84
84
 
85
- #### Presence
85
+ #### Attachment Presence
86
86
 
87
87
  If you want to validate presence of the attachment, you can use Sequel's
88
88
  presence validator:
@@ -100,7 +100,7 @@ class Photo < Sequel::Model
100
100
  end
101
101
  ```
102
102
 
103
- #### Skipping
103
+ #### Skipping Validations
104
104
 
105
105
  If don't want the attachment module to merge file validations errors into
106
106
  model errors, you can set `:validations` to `false`:
@@ -114,111 +114,19 @@ plugin :sequel, validations: false
114
114
  This section will cover methods added to the `Shrine::Attacher` instance. If
115
115
  you're not familar with how to obtain it, see the [`model`][model] plugin docs.
116
116
 
117
- ### Atomic promotion
117
+ The following persistence methods are added to the attacher:
118
118
 
119
- If you're promoting cached file to permanent storage
120
- [asynchronously][backgrounding], you might want to handle the possibility of
121
- the attachment changing during promotion. You can do that with
122
- `Attacher#atomic_promote`:
119
+ | Method | Description |
120
+ | :----- | :---------- |
121
+ | `Attacher#atomic_promote` | calls `Attacher#promote` and persists if the attachment hasn't changed |
122
+ | `Attacher#atomic_persist` | saves changes if the attachment hasn't changed |
123
+ | `Attacher#persist` | saves any changes to the underlying record |
123
124
 
124
- ```rb
125
- # in your controller
126
- attacher.attach_cached(io)
127
- attacher.cached? #=> true
128
- ```
129
- ```rb
130
- # in a background job
131
- attacher.atomic_promote # promotes cached file and persists
132
- attacher.stored? #=> true
133
- ```
134
-
135
- After cached file is uploaded to permanent storage, the record is reloaded in
136
- order to check whether the attachment hasn't changed, and if it hasn't the
137
- attachment is persisted. If the attachment has changed,
138
- `Shrine::AttachmentChanged` exception is raised.
139
-
140
- Additional options are passed to `Attacher#promote`.
141
-
142
- #### Reloader & persister
143
-
144
- You can change how the record is reloaded or persisted during atomic promotion:
145
-
146
- ```rb
147
- # reloader
148
- attacher.atomic_promote(reload: :lock) # uses database locking (default)
149
- attacher.atomic_promote(reload: :fetch) # reloads with no locking
150
- attacher.atomic_promote(reload: ->(&b){}) # custom reloader (see atomic_helpers plugin docs)
151
- attacher.atomic_promote(reload: false) # skips reloading
152
-
153
- # persister
154
- attacher.atomic_promote(persist: :save) # persists stored file (default)
155
- attacher.atomic_promote(persist: ->{}) # custom persister (see atomic_helpers plugin docs)
156
- attacher.atomic_promote(persist: false) # skips persistence
157
- ```
158
-
159
- For more details, see the [`atomic_helpers`][atomic_helpers] plugin docs.
160
-
161
- ### Atomic persistence
162
-
163
- If you're updating something based on the attached file
164
- [asynchronously][backgrounding], you might want to handle the possibility of
165
- the attachment changing in the meanwhile. You can do that with
166
- `Attacher#atomic_persist`:
167
-
168
- ```rb
169
- # in a background job
170
- attacher.refresh_metadata! # refresh_metadata plugin
171
- attacher.atomic_persist # persists attachment data
172
- ```
173
-
174
- The record is first reloaded in order to check whether the attachment hasn't
175
- changed, and if it hasn't the attachment is persisted. If the attachment has
176
- changed, `Shrine::AttachmentChanged` exception is raised.
177
-
178
- #### Reloader & persister
179
-
180
- You can change how the record is reloaded or persisted during atomic
181
- persistence:
182
-
183
- ```rb
184
- # reloader
185
- attacher.atomic_persist(reload: :lock) # uses database locking (default)
186
- attacher.atomic_persist(reload: :fetch) # reloads with no locking
187
- attacher.atomic_persist(reload: ->(&b){...}) # custom reloader (see atomic_helpers plugin docs)
188
- attacher.atomic_persist(reload: false) # skips reloading
189
-
190
- # persister
191
- attacher.atomic_persist(persist: :save) # persists stored file (default)
192
- attacher.atomic_persist(persist: ->{...}) # custom persister (see atomic_helpers plugin docs)
193
- attacher.atomic_persist(persist: false) # skips persistence
194
- ```
195
-
196
- For more details, see the [`atomic_helpers`][atomic_helpers] plugin docs.
197
-
198
- ### Persistence
199
-
200
- You can call `Attacher#persist` to save any changes to the underlying record:
201
-
202
- ```rb
203
- attacher.attach(io)
204
- attacher.persist # saves the underlying record
205
- ```
206
-
207
- ### With other database plugins
208
-
209
- If you have another database plugin loaded together with the `sequel` plugin,
210
- you can prefix any method above with `sequel_*` to avoid naming clashes:
211
-
212
- ```rb
213
- attacher.sequel_atomic_promote
214
- attacher.sequel_atomic_persist
215
- attacher.sequel_persist
216
- ```
125
+ See [persistence] docs for more details.
217
126
 
218
127
  [sequel]: /lib/shrine/plugins/sequel.rb
219
128
  [Sequel]: https://sequel.jeremyevans.net/
220
129
  [model]: /doc/plugins/model.md#readme
221
130
  [hooks]: http://sequel.jeremyevans.net/rdoc/files/doc/model_hooks_rdoc.html
222
131
  [validation]: /doc/plugins/validation.md#readme
223
- [atomic_helpers]: /doc/plugins/atomic_helpers.md#readme
224
- [backgrounding]: /doc/plugins/backgrounding.md#readme
132
+ [persistence]: /doc/plugins/persistence.md#readme
@@ -39,7 +39,7 @@ calculated hash.
39
39
  ```rb
40
40
  plugin :add_metadata
41
41
 
42
- add_metadata :md5 do |io, context|
42
+ add_metadata :md5 do |io|
43
43
  calculate_signature(io, :md5)
44
44
  end
45
45
  ```
@@ -48,8 +48,8 @@ This will generate a hash for each uploaded file, but if you want to generate
48
48
  one only for the original file, you can add a conditional:
49
49
 
50
50
  ```rb
51
- add_metadata :md5 do |io, context|
52
- calculate_signature(io, :md5) if context[:action] == :cache
51
+ add_metadata :md5 do |io, action: nil, **|
52
+ calculate_signature(io, :md5) if action == :cache
53
53
  end
54
54
  ```
55
55
 
@@ -29,6 +29,16 @@ class Shrine
29
29
  @options = options
30
30
  end
31
31
 
32
+ def included(klass)
33
+ super
34
+
35
+ attachment = self
36
+
37
+ klass.send(:define_singleton_method, :"#{@name}_attacher") do |**options|
38
+ attachment.shrine_class::Attacher.new(**attachment.options, **options)
39
+ end
40
+ end
41
+
32
42
  # Returns name of the attachment this module provides.
33
43
  def attachment_name
34
44
  @name
@@ -43,7 +53,7 @@ class Shrine
43
53
  #
44
54
  # Shrine::Attachment.new(:image).to_s #=> "#<Shrine::Attachment(image)>"
45
55
  def inspect
46
- "#<#{self.class.inspect}(#{attachment_name})>"
56
+ "#<#{self.class.inspect}(#{@name})>"
47
57
  end
48
58
  alias to_s inspect
49
59
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Helper plugin that defines persistence methods on the attacher according
6
+ # to convention.
7
+ #
8
+ # plugin :_persistence, plugin: MyPlugin
9
+ module Persistence
10
+ def self.load_dependencies(uploader, *)
11
+ uploader.plugin :atomic_helpers
12
+ end
13
+
14
+ # Defines the following attacher methods for a persistence plugin:
15
+ #
16
+ # * #persist (calls #<name>_persist and #<name>?)
17
+ # * #atomic_persist (calls #<name>_reload, #<name>_persist and #<name>?)
18
+ # * #atomic_promote (calls #<name>_reload, #<name>_persist and #<name>?)
19
+ def self.configure(uploader, plugin:)
20
+ plugin_name = plugin.to_s.split("::").last.downcase
21
+
22
+ plugin::AttacherMethods.module_eval do
23
+ define_method :atomic_promote do |**options, &block|
24
+ return super(**options, &block) unless send(:"#{plugin_name}?")
25
+
26
+ abstract_atomic_promote(
27
+ reload: method(:"#{plugin_name}_reload"),
28
+ persist: method(:"#{plugin_name}_persist"),
29
+ **options, &block
30
+ )
31
+ end
32
+
33
+ define_method :atomic_persist do |*args, **options, &block|
34
+ return super(*args, **options, &block) unless send(:"#{plugin_name}?")
35
+
36
+ abstract_atomic_persist(
37
+ *args,
38
+ reload: method(:"#{plugin_name}_reload"),
39
+ persist: method(:"#{plugin_name}_persist"),
40
+ **options, &block
41
+ )
42
+ end
43
+
44
+ define_method :persist do
45
+ return super() unless send(:"#{plugin_name}?")
46
+
47
+ send(:"#{plugin_name}_persist")
48
+ end
49
+ end
50
+ end
51
+
52
+ module AttacherMethods
53
+ def atomic_promote(*)
54
+ raise NotImplementedError, "unhandled by a persistence plugin"
55
+ end
56
+
57
+ def atomic_persist(*)
58
+ raise NotImplementedError, "unhandled by a persistence plugin"
59
+ end
60
+
61
+ def persist(*)
62
+ raise NotImplementedError, "unhandled by a persistence plugin"
63
+ end
64
+ end
65
+ end
66
+
67
+ register_plugin(:_persistence, Persistence)
68
+ end
69
+ end
@@ -10,7 +10,7 @@ class Shrine
10
10
  module Activerecord
11
11
  def self.load_dependencies(uploader, **)
12
12
  uploader.plugin :model
13
- uploader.plugin :atomic_helpers
13
+ uploader.plugin :_persistence, plugin: self
14
14
  end
15
15
 
16
16
  def self.configure(uploader, **opts)
@@ -24,15 +24,15 @@ class Shrine
24
24
 
25
25
  return unless model < ::ActiveRecord::Base
26
26
 
27
- name = attachment_name
27
+ name = @name
28
28
 
29
29
  if shrine_class.opts[:activerecord][:validations]
30
+ # add validation plugin integration
30
31
  model.validate do
31
- # validation plugin integration
32
- if send(:"#{name}_attacher").respond_to?(:errors)
33
- send(:"#{name}_attacher").errors.each do |message|
34
- errors.add(name, *message)
35
- end
32
+ next unless send(:"#{name}_attacher").respond_to?(:errors)
33
+
34
+ send(:"#{name}_attacher").errors.each do |message|
35
+ errors.add(name, *message)
36
36
  end
37
37
  end
38
38
  end
@@ -48,7 +48,7 @@ class Shrine
48
48
  model.after_commit on: action do
49
49
  if send(:"#{name}_attacher").changed?
50
50
  send(:"#{name}_attacher").finalize
51
- send(:"#{name}_attacher").activerecord_persist
51
+ send(:"#{name}_attacher").persist
52
52
  end
53
53
  end
54
54
  end
@@ -68,81 +68,13 @@ class Shrine
68
68
  end
69
69
 
70
70
  module AttacherMethods
71
- # Promotes cached file to permanent storage in an atomic way. It's
72
- # intended to be called from a background job.
73
- #
74
- # attacher.assign(file)
75
- # attacher.cached? #=> true
76
- #
77
- # # ... in background job ...
78
- #
79
- # attacher.atomic_promote
80
- # attacher.stored? #=> true
81
- #
82
- # It accepts `:reload` and `:persist` strategies:
83
- #
84
- # attacher.atomic_promote(reload: :lock) # uses database locking (default)
85
- # attacher.atomic_promote(reload: :fetch) # reloads with no locking
86
- # attacher.atomic_promote(reload: ->(&b){}) # custom reloader
87
- # attacher.atomic_promote(reload: false) # skips reloading
88
- #
89
- # attacher.atomic_promote(persist: :save) # persists stored file (default)
90
- # attacher.atomic_promote(persist: ->{}) # custom persister
91
- # attacher.atomic_promote(persist: false) # skips persistence
92
- def activerecord_atomic_promote(**options, &block)
93
- abstract_atomic_promote(activerecord_strategies(**options), &block)
94
- end
95
- alias atomic_promote activerecord_atomic_promote
96
-
97
- # Persist the the record only if the attachment hasn't changed.
98
- # Optionally yields reloaded attacher to the block before persisting.
99
- # It's intended to be called from a background job.
71
+ # The _persistence plugin defines the following methods:
100
72
  #
101
- # # ... in background job ...
102
- #
103
- # attacher.file.metadata["foo"] = "bar"
104
- # attacher.write
105
- #
106
- # attacher.atomic_persist
107
- def activerecord_atomic_persist(*args, **options, &block)
108
- abstract_atomic_persist(*args, activerecord_strategies(**options), &block)
109
- end
110
- alias atomic_persist activerecord_atomic_persist
111
-
112
- # Called in the `after_commit` callback after finalization.
113
- def activerecord_persist
114
- activerecord_save
115
- end
116
- alias persist activerecord_persist
117
-
73
+ # * #persist (calls #activerecord_persist and #activerecord?)
74
+ # * #atomic_persist (calls #activerecord_lock, #activerecord_persist and #activerecord?)
75
+ # * #atomic_promote (calls #activerecord_lock, #activerecord_persist and #activerecord?)
118
76
  private
119
77
 
120
- # Resolves strategies for atomic promotion and persistence.
121
- def activerecord_strategies(reload: :lock, persist: :save, **options)
122
- reload = method(:"activerecord_#{reload}") if reload.is_a?(Symbol)
123
- persist = method(:"activerecord_#{persist}") if persist.is_a?(Symbol)
124
-
125
- { reload: reload, persist: persist, **options }
126
- end
127
-
128
- # Implements the "fetch" reload strategy for #atomic_promote and
129
- # #atomic_persist.
130
- def activerecord_fetch
131
- yield record.clone.reload
132
- end
133
-
134
- # Implements the "lock" reload strategy for #atomic_promote and
135
- # #atomic_persist.
136
- def activerecord_lock
137
- record.transaction { yield record.clone.reload(lock: true) }
138
- end
139
-
140
- # Implements the "save" persist strategy for #atomic_promote and
141
- # #atomic_persist.
142
- def activerecord_save
143
- record.save(validate: false)
144
- end
145
-
146
78
  # ActiveRecord JSON column attribute needs to be assigned with a Hash.
147
79
  def serialize_column(data)
148
80
  activerecord_json_column? ? data : super
@@ -150,11 +82,29 @@ class Shrine
150
82
 
151
83
  # Returns true if the data attribute represents a JSON or JSONB column.
152
84
  def activerecord_json_column?
153
- return false unless record.is_a?(ActiveRecord::Base)
85
+ return false unless activerecord?
154
86
  return false unless column = record.class.columns_hash[attribute.to_s]
155
87
 
156
88
  [:json, :jsonb].include?(column.type)
157
89
  end
90
+
91
+ # Saves changes to the model instance, skipping validations. Used by
92
+ # the _persistence plugin.
93
+ def activerecord_persist
94
+ record.save(validate: false)
95
+ end
96
+
97
+ # Locks the database row and yields the reloaded record. Used by the
98
+ # _persistence plugin.
99
+ def activerecord_reload
100
+ record.transaction { yield record.clone.reload(lock: true) }
101
+ end
102
+
103
+ # Returns whether the record is an ActiveRecord model. Used by the
104
+ # _persistence plugin.
105
+ def activerecord?
106
+ record.is_a?(::ActiveRecord::Base)
107
+ end
158
108
  end
159
109
  end
160
110
 
@@ -14,9 +14,9 @@ class Shrine
14
14
  # the attachment hasn't changed. It raises `Shrine::AttachmentChanged`
15
15
  # exception if the attached file doesn't match.
16
16
  #
17
- # Shrine::Attacher.retrieve(model: photo, name: :image, data: data)
17
+ # Shrine::Attacher.retrieve(model: photo, name: :image, file: file_data)
18
18
  # #=> #<ImageUploader::Attacher>
19
- def retrieve(model: nil, entity: nil, name:, data:, **options)
19
+ def retrieve(model: nil, entity: nil, name:, file:, **options)
20
20
  fail ArgumentError, "either :model or :entity is required" unless model || entity
21
21
 
22
22
  record = model || entity
@@ -25,7 +25,7 @@ class Shrine
25
25
  attacher ||= from_model(record, name, **options) if model
26
26
  attacher ||= from_entity(record, name, **options) if entity
27
27
 
28
- if attacher.file != attacher.class.from_data(data).file
28
+ if attacher.file != attacher.uploaded_file(file)
29
29
  fail Shrine::AttachmentChanged, "attachment has changed"
30
30
  end
31
31
 
@@ -82,6 +82,16 @@ class Shrine
82
82
  end
83
83
  end
84
84
 
85
+ # Return only needed main file data, without the metadata. This allows
86
+ # you to avoid bloating your background job payload when you have
87
+ # derivatives or lots of metadata, by only sending data you need for
88
+ # atomic persitence.
89
+ #
90
+ # attacher.file_data #=> { "id" => "abc123.jpg", "storage" => "store" }
91
+ def file_data
92
+ file!.data.reject { |key, value| key == "metadata" }
93
+ end
94
+
85
95
  protected
86
96
 
87
97
  # Calls the reload strategy and yields a reloaded attacher from the