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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -2
- data/README.md +1 -1
- data/doc/creating_persistence_plugins.md +20 -63
- data/doc/plugins/activerecord.md +13 -106
- data/doc/plugins/add_metadata.md +31 -9
- data/doc/plugins/atomic_helpers.md +33 -9
- data/doc/plugins/cached_attachment_data.md +3 -3
- data/doc/plugins/data_uri.md +16 -19
- data/doc/plugins/default_storage.md +32 -8
- data/doc/plugins/default_url.md +21 -9
- data/doc/plugins/derivation_endpoint.md +48 -0
- data/doc/plugins/derivatives.md +74 -63
- data/doc/plugins/entity.md +27 -2
- data/doc/plugins/included.md +8 -7
- data/doc/plugins/metadata_attributes.md +20 -5
- data/doc/plugins/model.md +22 -2
- data/doc/plugins/persistence.md +89 -0
- data/doc/plugins/remote_url.md +41 -45
- data/doc/plugins/remove_attachment.md +23 -4
- data/doc/plugins/remove_invalid.md +3 -4
- data/doc/plugins/restore_cached_data.md +3 -1
- data/doc/plugins/sequel.md +13 -105
- data/doc/plugins/signature.md +3 -3
- data/lib/shrine/attachment.rb +11 -1
- data/lib/shrine/plugins/_persistence.rb +69 -0
- data/lib/shrine/plugins/activerecord.rb +31 -81
- data/lib/shrine/plugins/atomic_helpers.rb +13 -3
- data/lib/shrine/plugins/backgrounding.rb +8 -8
- data/lib/shrine/plugins/cached_attachment_data.rb +2 -6
- data/lib/shrine/plugins/data_uri.rb +2 -6
- data/lib/shrine/plugins/default_storage.rb +28 -2
- data/lib/shrine/plugins/derivation_endpoint.rb +3 -9
- data/lib/shrine/plugins/derivatives.rb +26 -17
- data/lib/shrine/plugins/entity.rb +24 -16
- data/lib/shrine/plugins/included.rb +1 -0
- data/lib/shrine/plugins/infer_extension.rb +2 -0
- data/lib/shrine/plugins/metadata_attributes.rb +18 -8
- data/lib/shrine/plugins/model.rb +35 -14
- data/lib/shrine/plugins/remote_url.rb +2 -6
- data/lib/shrine/plugins/remove_attachment.rb +2 -6
- data/lib/shrine/plugins/remove_invalid.rb +10 -6
- data/lib/shrine/plugins/sequel.rb +31 -78
- data/lib/shrine/plugins/upload_options.rb +2 -2
- data/lib/shrine/plugins/validation.rb +1 -11
- data/lib/shrine/version.rb +1 -1
- metadata +4 -2
data/doc/plugins/remote_url.md
CHANGED
@@ -7,40 +7,58 @@ location.
|
|
7
7
|
plugin :remote_url, max_size: 20*1024*1024
|
8
8
|
```
|
9
9
|
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
24
|
-
`
|
20
|
+
If you're using `Shrine::Attacher` directly, you can use
|
21
|
+
`Attacher#assign_remote_url`:
|
25
22
|
|
26
23
|
```rb
|
27
|
-
attacher.
|
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
|
33
|
+
gem. This will use the [Down::NetHttp] backend by default, which is a wrapper
|
32
34
|
around [open-uri].
|
33
35
|
|
34
|
-
|
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
|
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
|
107
|
-
extension
|
108
|
-
|
109
|
-
extension
|
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
|
-
|
11
|
-
|
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
|
-
|
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
|
4
|
-
assigned file if it was invalid
|
5
|
-
|
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
|
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
|
data/doc/plugins/sequel.md
CHANGED
@@ -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
|
-
|
117
|
+
The following persistence methods are added to the attacher:
|
118
118
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
`Attacher#
|
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
|
-
|
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
|
-
[
|
224
|
-
[backgrounding]: /doc/plugins/backgrounding.md#readme
|
132
|
+
[persistence]: /doc/plugins/persistence.md#readme
|
data/doc/plugins/signature.md
CHANGED
@@ -39,7 +39,7 @@ calculated hash.
|
|
39
39
|
```rb
|
40
40
|
plugin :add_metadata
|
41
41
|
|
42
|
-
add_metadata :md5 do |io
|
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,
|
52
|
-
calculate_signature(io, :md5) if
|
51
|
+
add_metadata :md5 do |io, action: nil, **|
|
52
|
+
calculate_signature(io, :md5) if action == :cache
|
53
53
|
end
|
54
54
|
```
|
55
55
|
|
data/lib/shrine/attachment.rb
CHANGED
@@ -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}(#{
|
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 :
|
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 =
|
27
|
+
name = @name
|
28
28
|
|
29
29
|
if shrine_class.opts[:activerecord][:validations]
|
30
|
+
# add validation plugin integration
|
30
31
|
model.validate do
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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").
|
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
|
-
#
|
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
|
-
#
|
102
|
-
#
|
103
|
-
#
|
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
|
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,
|
17
|
+
# Shrine::Attacher.retrieve(model: photo, name: :image, file: file_data)
|
18
18
|
# #=> #<ImageUploader::Attacher>
|
19
|
-
def retrieve(model: nil, entity: nil, name:,
|
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.
|
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
|