shrine 3.4.0 → 3.10.0
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 +4 -4
- data/CHANGELOG.md +96 -0
- data/README.md +11 -11
- data/doc/advantages.md +1 -1
- data/doc/attacher.md +1 -1
- data/doc/carrierwave.md +2 -2
- data/doc/changing_derivatives.md +2 -1
- data/doc/changing_location.md +19 -6
- data/doc/design.md +4 -4
- data/doc/external/articles.md +16 -16
- data/doc/external/extensions.md +1 -1
- data/doc/getting_started.md +73 -30
- data/doc/multiple_files.md +57 -22
- data/doc/plugins/activerecord.md +91 -0
- data/doc/plugins/backgrounding.md +30 -4
- data/doc/plugins/data_uri.md +15 -0
- data/doc/plugins/derivation_endpoint.md +39 -1
- data/doc/plugins/derivatives.md +13 -2
- data/doc/plugins/download_endpoint.md +16 -4
- data/doc/plugins/dynamic_storage.md +15 -0
- data/doc/plugins/fallback_storage.md +90 -0
- data/doc/plugins/keep_files.md +6 -4
- data/doc/plugins/rack_response.md +9 -0
- data/doc/plugins/refresh_metadata.md +20 -0
- data/doc/plugins/sequel.md +57 -0
- data/doc/plugins/signature.md +8 -6
- data/doc/plugins/tempfile.md +14 -6
- data/doc/plugins/upload_options.md +12 -0
- data/doc/plugins/url_options.md +12 -0
- data/doc/plugins/validation_helpers.md +1 -1
- data/doc/processing.md +5 -3
- data/doc/refile.md +3 -3
- data/doc/release_notes/2.1.0.md +1 -1
- data/doc/release_notes/3.10.0.md +30 -0
- data/doc/release_notes/3.5.0.md +63 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/release_notes/3.7.0.md +75 -0
- data/doc/release_notes/3.7.1.md +31 -0
- data/doc/release_notes/3.8.0.md +37 -0
- data/doc/release_notes/3.9.0.md +42 -0
- data/doc/retrieving_uploads.md +1 -1
- data/doc/storage/s3.md +10 -0
- data/doc/testing.md +45 -17
- data/doc/upgrading_to_3.md +3 -5
- data/lib/shrine/attacher.rb +41 -29
- data/lib/shrine/attachment.rb +2 -2
- data/lib/shrine/plugins/_persistence.rb +1 -1
- data/lib/shrine/plugins/_urlsafe_serialization.rb +4 -4
- data/lib/shrine/plugins/activerecord.rb +25 -4
- data/lib/shrine/plugins/add_metadata.rb +2 -4
- data/lib/shrine/plugins/atomic_helpers.rb +7 -7
- data/lib/shrine/plugins/backgrounding.rb +10 -10
- data/lib/shrine/plugins/column.rb +7 -5
- data/lib/shrine/plugins/data_uri.rb +28 -3
- data/lib/shrine/plugins/default_url.rb +4 -4
- data/lib/shrine/plugins/delete_raw.rb +2 -2
- data/lib/shrine/plugins/derivation_endpoint.rb +65 -59
- data/lib/shrine/plugins/derivatives.rb +35 -21
- data/lib/shrine/plugins/download_endpoint.rb +72 -11
- data/lib/shrine/plugins/entity.rb +15 -9
- data/lib/shrine/plugins/fallback_storage.rb +51 -0
- data/lib/shrine/plugins/infer_extension.rb +5 -1
- data/lib/shrine/plugins/instrumentation.rb +13 -9
- data/lib/shrine/plugins/metadata_attributes.rb +1 -1
- data/lib/shrine/plugins/mirroring.rb +10 -10
- data/lib/shrine/plugins/model.rb +12 -10
- data/lib/shrine/plugins/presign_endpoint.rb +13 -10
- data/lib/shrine/plugins/pretty_location.rb +2 -2
- data/lib/shrine/plugins/processing.rb +3 -3
- data/lib/shrine/plugins/rack_file.rb +2 -2
- data/lib/shrine/plugins/rack_response.rb +12 -6
- data/lib/shrine/plugins/refresh_metadata.rb +6 -6
- data/lib/shrine/plugins/remote_url.rb +4 -4
- data/lib/shrine/plugins/remove_attachment.rb +2 -0
- data/lib/shrine/plugins/remove_invalid.rb +4 -0
- data/lib/shrine/plugins/restore_cached_data.rb +3 -3
- data/lib/shrine/plugins/sequel.rb +15 -1
- data/lib/shrine/plugins/signature.rb +2 -2
- data/lib/shrine/plugins/store_dimensions.rb +2 -2
- data/lib/shrine/plugins/tempfile.rb +0 -2
- data/lib/shrine/plugins/upload_endpoint.rb +7 -5
- data/lib/shrine/plugins/upload_options.rb +18 -4
- data/lib/shrine/plugins/url_options.rb +17 -3
- data/lib/shrine/plugins/validation.rb +8 -8
- data/lib/shrine/plugins/validation_helpers.rb +2 -2
- data/lib/shrine/plugins/versions.rb +10 -10
- data/lib/shrine/plugins.rb +6 -14
- data/lib/shrine/storage/file_system.rb +15 -19
- data/lib/shrine/storage/linter.rb +8 -8
- data/lib/shrine/storage/memory.rb +1 -3
- data/lib/shrine/storage/s3.rb +60 -40
- data/lib/shrine/uploaded_file.rb +23 -19
- data/lib/shrine/version.rb +1 -1
- data/lib/shrine.rb +24 -20
- data/shrine.gemspec +10 -8
- metadata +52 -26
data/doc/testing.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
title: Testing with Shrine
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
import Tabs from '@theme/Tabs';
|
|
6
|
+
import TabItem from '@theme/TabItem';
|
|
7
|
+
|
|
5
8
|
The goal of this guide is to provide some useful tips for testing file
|
|
6
9
|
attachments implemented with Shrine in your application.
|
|
7
10
|
|
|
@@ -137,19 +140,26 @@ module TestData
|
|
|
137
140
|
end
|
|
138
141
|
end
|
|
139
142
|
```
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
|
|
144
|
+
<Tabs>
|
|
145
|
+
<TabItem value="factory_bot" label="FactoryBot">
|
|
146
|
+
|
|
142
147
|
```rb
|
|
143
148
|
factory :photo do
|
|
144
149
|
image_data { TestData.image_data }
|
|
145
150
|
end
|
|
146
151
|
```
|
|
147
|
-
|
|
152
|
+
|
|
153
|
+
</TabItem>
|
|
154
|
+
<TabItem value="fixtures" label="Rails YAML fixtures">
|
|
155
|
+
|
|
148
156
|
```erb
|
|
149
157
|
photo:
|
|
150
158
|
image_data: <%= TestData.image_data %>
|
|
151
159
|
```
|
|
152
|
-
|
|
160
|
+
|
|
161
|
+
</TabItem>
|
|
162
|
+
</Tabs>
|
|
153
163
|
|
|
154
164
|
## Unit tests
|
|
155
165
|
|
|
@@ -182,24 +192,33 @@ end
|
|
|
182
192
|
In acceptance tests you're testing your app end-to-end, and you likely want to
|
|
183
193
|
also test file attachments here. Here are examples for some common use cases:
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
<Tabs>
|
|
196
|
+
<TabItem value="capybara" label="Capybara">
|
|
197
|
+
|
|
187
198
|
```rb
|
|
188
199
|
attach_file("#image-field", "test/files/image.jpg")
|
|
189
200
|
```
|
|
190
|
-
|
|
201
|
+
|
|
202
|
+
</TabItem>
|
|
203
|
+
<TabItem value="rack-test" label="rack-test">
|
|
204
|
+
|
|
191
205
|
```rb
|
|
192
206
|
post "/photos", photo: {
|
|
193
207
|
image: Rack::Test::UploadedFile.new("test/files/image.jpg", "image/jpeg")
|
|
194
208
|
}
|
|
195
209
|
```
|
|
196
|
-
|
|
210
|
+
|
|
211
|
+
</TabItem>
|
|
212
|
+
</Tabs>
|
|
213
|
+
|
|
214
|
+
If you want to test requests with cached attachment data, you can do so as
|
|
215
|
+
follows:
|
|
216
|
+
|
|
197
217
|
```rb
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
218
|
+
cached_file = Shrine.upload(file, :cache)
|
|
219
|
+
|
|
220
|
+
post "/photos", photo: { image: cached_file.to_json }
|
|
201
221
|
```
|
|
202
|
-
<!--END_DOCUSAURUS_CODE_TABS-->
|
|
203
222
|
|
|
204
223
|
## Background jobs
|
|
205
224
|
|
|
@@ -207,21 +226,30 @@ If you're using background jobs with Shrine, you probably want to make them
|
|
|
207
226
|
synchronous in tests. See your backgrounding library docs for how to make jobs
|
|
208
227
|
synchronous.
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
<Tabs>
|
|
230
|
+
<TabItem value="activejob" label="Active Job">
|
|
231
|
+
|
|
212
232
|
```rb
|
|
213
233
|
ActiveJob::Base.queue_adapter = :inline
|
|
214
234
|
```
|
|
215
|
-
|
|
235
|
+
|
|
236
|
+
</TabItem>
|
|
237
|
+
<TabItem value="sidekiq" label="Sidekiq">
|
|
238
|
+
|
|
216
239
|
```rb
|
|
217
240
|
require "sidekiq/testing"
|
|
218
241
|
Sidekiq::Testing.inline!
|
|
219
242
|
```
|
|
220
|
-
|
|
243
|
+
|
|
244
|
+
</TabItem>
|
|
245
|
+
<TabItem value="sucker_punch" label="SuckerPunch">
|
|
246
|
+
|
|
221
247
|
```rb
|
|
222
248
|
require "sucker_punch/testing/inline"
|
|
223
249
|
```
|
|
224
|
-
|
|
250
|
+
|
|
251
|
+
</TabItem>
|
|
252
|
+
</Tabs>
|
|
225
253
|
|
|
226
254
|
## Processing
|
|
227
255
|
|
data/doc/upgrading_to_3.md
CHANGED
|
@@ -7,9 +7,6 @@ This guide provides instructions for upgrading Shrine in your apps to version
|
|
|
7
7
|
3.x. If you're looking for a full list of changes, see the **[3.0 release
|
|
8
8
|
notes]**.
|
|
9
9
|
|
|
10
|
-
If you would like assistance with the upgrade, I'm available for consultation,
|
|
11
|
-
you can email me at <janko.marohnic@gmail.com>.
|
|
12
|
-
|
|
13
10
|
## Attacher
|
|
14
11
|
|
|
15
12
|
The `Shrine::Attacher` class has been rewritten in Shrine 3.0, though much of
|
|
@@ -447,7 +444,7 @@ class PromoteJob
|
|
|
447
444
|
attacher.create_derivatives # call derivatives processor
|
|
448
445
|
attacher.atomic_promote
|
|
449
446
|
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
|
|
450
|
-
# attachment has changed or record has
|
|
447
|
+
# attachment has changed or record has been deleted, nothing to do
|
|
451
448
|
end
|
|
452
449
|
end
|
|
453
450
|
```
|
|
@@ -649,7 +646,8 @@ attacher.copy(other_attacher)
|
|
|
649
646
|
with
|
|
650
647
|
|
|
651
648
|
```rb
|
|
652
|
-
attacher.set
|
|
649
|
+
attacher.set nil # clear original attachment
|
|
650
|
+
attacher.attach other_attacher.file, storage: other_attacher.file.storage_key
|
|
653
651
|
attacher.add_derivatives other_attacher.derivatives # if using derivatives
|
|
654
652
|
```
|
|
655
653
|
|
data/lib/shrine/attacher.rb
CHANGED
|
@@ -20,10 +20,10 @@ class Shrine
|
|
|
20
20
|
|
|
21
21
|
# Initializes the attacher from a data hash generated from `Attacher#data`.
|
|
22
22
|
#
|
|
23
|
-
# attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => {
|
|
23
|
+
# attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
24
24
|
# attacher.file #=> #<Shrine::UploadedFile>
|
|
25
|
-
def from_data(data, **
|
|
26
|
-
attacher = new(**
|
|
25
|
+
def from_data(data, **)
|
|
26
|
+
attacher = new(**)
|
|
27
27
|
attacher.load_data(data)
|
|
28
28
|
attacher
|
|
29
29
|
end
|
|
@@ -47,34 +47,34 @@ class Shrine
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# Returns the temporary storage identifier.
|
|
50
|
-
def cache_key
|
|
50
|
+
def cache_key = @cache.to_sym
|
|
51
51
|
# Returns the permanent storage identifier.
|
|
52
|
-
def store_key
|
|
52
|
+
def store_key = @store.to_sym
|
|
53
53
|
|
|
54
54
|
# Returns the uploader that is used for the temporary storage.
|
|
55
|
-
def cache
|
|
55
|
+
def cache = shrine_class.new(cache_key)
|
|
56
56
|
# Returns the uploader that is used for the permanent storage.
|
|
57
|
-
def store
|
|
57
|
+
def store = shrine_class.new(store_key)
|
|
58
58
|
|
|
59
59
|
# Calls #attach_cached, but skips if value is an empty string (this is
|
|
60
60
|
# useful when the uploaded file comes from form fields). Forwards any
|
|
61
61
|
# additional options to #attach_cached.
|
|
62
62
|
#
|
|
63
|
-
# attacher.assign(File.open(
|
|
64
|
-
# attacher.assign(File.open(
|
|
63
|
+
# attacher.assign(File.open(path))
|
|
64
|
+
# attacher.assign(File.open(path), metadata: { "foo" => "bar" })
|
|
65
65
|
# attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
|
|
66
66
|
# attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
67
67
|
#
|
|
68
68
|
# # ignores the assignment when a blank string is given
|
|
69
69
|
# attacher.assign("")
|
|
70
|
-
def assign(value, **
|
|
70
|
+
def assign(value, **)
|
|
71
71
|
return if value == "" # skip empty hidden field
|
|
72
72
|
|
|
73
73
|
if value.is_a?(Hash) || value.is_a?(String)
|
|
74
74
|
return if uploaded_file(value) == file # skip assignment for current file
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
attach_cached(value, **
|
|
77
|
+
attach_cached(value, **)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# Sets an existing cached file, or uploads an IO object to temporary
|
|
@@ -82,21 +82,21 @@ class Shrine
|
|
|
82
82
|
# #attach.
|
|
83
83
|
#
|
|
84
84
|
# # upload file to temporary storage and set the uploaded file.
|
|
85
|
-
# attacher.attach_cached(File.open(
|
|
85
|
+
# attacher.attach_cached(File.open(path))
|
|
86
86
|
#
|
|
87
87
|
# # foward additional options to the uploader
|
|
88
|
-
# attacher.attach_cached(File.open(
|
|
88
|
+
# attacher.attach_cached(File.open(path), metadata: { "foo" => "bar" })
|
|
89
89
|
#
|
|
90
90
|
# # sets an existing cached file from JSON data
|
|
91
91
|
# attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')
|
|
92
92
|
#
|
|
93
93
|
# # sets an existing cached file from Hash data
|
|
94
94
|
# attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
95
|
-
def attach_cached(value, **
|
|
95
|
+
def attach_cached(value, **)
|
|
96
96
|
if value.is_a?(String) || value.is_a?(Hash)
|
|
97
|
-
change(cached(value, **
|
|
97
|
+
change(cached(value, **))
|
|
98
98
|
else
|
|
99
|
-
attach(value, storage: cache_key, action: :cache, **
|
|
99
|
+
attach(value, storage: cache_key, action: :cache, **)
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -113,8 +113,8 @@ class Shrine
|
|
|
113
113
|
#
|
|
114
114
|
# # removes the attachment
|
|
115
115
|
# attacher.attach(nil)
|
|
116
|
-
def attach(io, storage: store_key, **
|
|
117
|
-
file = upload(io, storage, **
|
|
116
|
+
def attach(io, storage: store_key, **)
|
|
117
|
+
file = upload(io, storage, **) if io
|
|
118
118
|
|
|
119
119
|
change(file)
|
|
120
120
|
end
|
|
@@ -158,8 +158,8 @@ class Shrine
|
|
|
158
158
|
# attacher.cached? #=> true
|
|
159
159
|
# attacher.promote_cached
|
|
160
160
|
# attacher.stored? #=> true
|
|
161
|
-
def promote_cached(**
|
|
162
|
-
promote(**
|
|
161
|
+
def promote_cached(**)
|
|
162
|
+
promote(**) if promote?
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
# Uploads current file to permanent storage and sets the stored file.
|
|
@@ -167,8 +167,8 @@ class Shrine
|
|
|
167
167
|
# attacher.cached? #=> true
|
|
168
168
|
# attacher.promote
|
|
169
169
|
# attacher.stored? #=> true
|
|
170
|
-
def promote(storage: store_key, **
|
|
171
|
-
set upload(file, storage, action: :store, **
|
|
170
|
+
def promote(storage: store_key, **)
|
|
171
|
+
set upload(file, storage, action: :store, **)
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
# Delegates to `Shrine.upload`, passing the #context.
|
|
@@ -178,8 +178,8 @@ class Shrine
|
|
|
178
178
|
#
|
|
179
179
|
# # pass additional options for the uploader
|
|
180
180
|
# attacher.upload(io, :store, metadata: { "foo" => "bar" })
|
|
181
|
-
def upload(io, storage = store_key, **
|
|
182
|
-
shrine_class.upload(io, storage, **context, **
|
|
181
|
+
def upload(io, storage = store_key, **)
|
|
182
|
+
shrine_class.upload(io, storage, **context, **)
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
# If a new file was attached, deletes previously attached file if any.
|
|
@@ -189,7 +189,7 @@ class Shrine
|
|
|
189
189
|
# attacher.destroy_previous
|
|
190
190
|
# previous_file.exists? #=> false
|
|
191
191
|
def destroy_previous
|
|
192
|
-
@previous.destroy_attached if changed?
|
|
192
|
+
@previous.destroy_attached if changed? && destroy_previous?
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
# Destroys the attached file if it exists and is uploaded to permanent
|
|
@@ -249,8 +249,8 @@ class Shrine
|
|
|
249
249
|
#
|
|
250
250
|
# attacher.file = nil
|
|
251
251
|
# attacher.url #=> nil
|
|
252
|
-
def url(**
|
|
253
|
-
file&.url(**
|
|
252
|
+
def url(**)
|
|
253
|
+
file&.url(**)
|
|
254
254
|
end
|
|
255
255
|
|
|
256
256
|
# Returns whether the attachment has changed.
|
|
@@ -299,7 +299,7 @@ class Shrine
|
|
|
299
299
|
# Loads the uploaded file from data generated by `Attacher#data`.
|
|
300
300
|
#
|
|
301
301
|
# attacher.file #=> nil
|
|
302
|
-
# attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => {
|
|
302
|
+
# attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
303
303
|
# attacher.file #=> #<Shrine::UploadedFile>
|
|
304
304
|
def load_data(data)
|
|
305
305
|
@file = data && uploaded_file(data)
|
|
@@ -341,6 +341,13 @@ class Shrine
|
|
|
341
341
|
|
|
342
342
|
private
|
|
343
343
|
|
|
344
|
+
# The copy constructor that's called on #dup and #clone
|
|
345
|
+
# We need to duplicate the context to prevent it from being shared
|
|
346
|
+
def initialize_copy(other)
|
|
347
|
+
super
|
|
348
|
+
@context = @context.dup
|
|
349
|
+
end
|
|
350
|
+
|
|
344
351
|
# Converts a String or Hash value into an UploadedFile object and ensures
|
|
345
352
|
# it's uploaded to temporary storage.
|
|
346
353
|
#
|
|
@@ -349,7 +356,7 @@ class Shrine
|
|
|
349
356
|
# #=> #<Shrine::UploadedFile>
|
|
350
357
|
#
|
|
351
358
|
# # from Hash data
|
|
352
|
-
# attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => {
|
|
359
|
+
# attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
|
|
353
360
|
# #=> #<Shrine::UploadedFile>
|
|
354
361
|
def cached(value, **)
|
|
355
362
|
uploaded_file = uploaded_file(value)
|
|
@@ -373,6 +380,11 @@ class Shrine
|
|
|
373
380
|
attached? && !cached?
|
|
374
381
|
end
|
|
375
382
|
|
|
383
|
+
# Whether the previous attached file should be deleted.
|
|
384
|
+
def destroy_previous?
|
|
385
|
+
true
|
|
386
|
+
end
|
|
387
|
+
|
|
376
388
|
# Whether assigning the given file is considered a change.
|
|
377
389
|
def change?(file)
|
|
378
390
|
@file != file
|
data/lib/shrine/attachment.rb
CHANGED
|
@@ -23,8 +23,8 @@ class Shrine
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
module FileMethods
|
|
26
|
-
def urlsafe_dump(**
|
|
27
|
-
self.class.urlsafe_dump(self, **
|
|
26
|
+
def urlsafe_dump(**)
|
|
27
|
+
self.class.urlsafe_dump(self, **)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def urlsafe_data(metadata: [])
|
|
@@ -45,8 +45,8 @@ class Shrine
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
module FileClassMethods
|
|
48
|
-
def urlsafe_dump(file, **
|
|
49
|
-
data = file.urlsafe_data(**
|
|
48
|
+
def urlsafe_dump(file, **)
|
|
49
|
+
data = file.urlsafe_data(**)
|
|
50
50
|
|
|
51
51
|
shrine_class.urlsafe_serialize(data)
|
|
52
52
|
end
|
|
@@ -12,7 +12,7 @@ class Shrine
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def self.configure(uploader, **opts)
|
|
15
|
-
uploader.opts[:activerecord] ||= { callbacks: true, validations: true }
|
|
15
|
+
uploader.opts[:activerecord] ||= { callbacks: true, validations: true, attribute_types: false }
|
|
16
16
|
uploader.opts[:activerecord].merge!(opts)
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -108,12 +108,22 @@ class Shrine
|
|
|
108
108
|
record.transaction { yield record.clone.reload(lock: true) }
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
# Returns true if the data attribute represents a JSON or JSONB
|
|
111
|
+
# Returns true if the data attribute represents a JSON or JSONB type.
|
|
112
112
|
# Used by the _persistence plugin to determine whether serialization
|
|
113
113
|
# should be skipped.
|
|
114
|
+
#
|
|
115
|
+
# By default only real database columns are checked. When the
|
|
116
|
+
# `:attribute_types` plugin option is enabled, the check uses
|
|
117
|
+
# `type_for_attribute`, so attributes declared via the Attributes API
|
|
118
|
+
# are recognized as well.
|
|
114
119
|
def activerecord_hash_attribute?
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
if shrine_class.opts[:activerecord][:attribute_types]
|
|
121
|
+
attribute_type = record.class.type_for_attribute(attribute.to_s)
|
|
122
|
+
attribute_type && [:json, :jsonb].include?(attribute_type.type)
|
|
123
|
+
else
|
|
124
|
+
column = record.class.columns_hash[attribute.to_s]
|
|
125
|
+
column && [:json, :jsonb].include?(column.type)
|
|
126
|
+
end
|
|
117
127
|
end
|
|
118
128
|
|
|
119
129
|
# Returns whether the record is an ActiveRecord model. Used by the
|
|
@@ -121,6 +131,17 @@ class Shrine
|
|
|
121
131
|
def activerecord?
|
|
122
132
|
record.is_a?(::ActiveRecord::Base)
|
|
123
133
|
end
|
|
134
|
+
|
|
135
|
+
# Prevents the previous file from being destroyed when this save
|
|
136
|
+
# created the record (as opposed to updating an existing one), since
|
|
137
|
+
# a record being created can't yet have its own confirmed attachment
|
|
138
|
+
# to safely replace (e.g. when it was duplicated via `#dup` from
|
|
139
|
+
# another, still-persisted record).
|
|
140
|
+
def destroy_previous?
|
|
141
|
+
return super unless activerecord?
|
|
142
|
+
|
|
143
|
+
super && !record.previously_new_record?
|
|
144
|
+
end
|
|
124
145
|
end
|
|
125
146
|
end
|
|
126
147
|
|
|
@@ -29,11 +29,9 @@ class Shrine
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
module InstanceMethods
|
|
32
|
-
def extract_metadata(io, **
|
|
32
|
+
def extract_metadata(io, **)
|
|
33
33
|
metadata = super
|
|
34
|
-
|
|
35
|
-
extract_custom_metadata(io, **options, metadata: metadata)
|
|
36
|
-
|
|
34
|
+
extract_custom_metadata(io, **, metadata:)
|
|
37
35
|
metadata
|
|
38
36
|
end
|
|
39
37
|
|
|
@@ -14,14 +14,14 @@ class Shrine
|
|
|
14
14
|
#
|
|
15
15
|
# Shrine::Attacher.retrieve(model: photo, name: :image, file: file_data)
|
|
16
16
|
# #=> #<ImageUploader::Attacher>
|
|
17
|
-
def retrieve(model: nil, entity: nil, name:, file:, **
|
|
17
|
+
def retrieve(model: nil, entity: nil, name:, file:, **)
|
|
18
18
|
fail ArgumentError, "either :model or :entity is required" unless model || entity
|
|
19
19
|
|
|
20
20
|
record = model || entity
|
|
21
21
|
|
|
22
|
-
attacher = record.send(:"#{name}_attacher", **
|
|
23
|
-
attacher ||= from_model(record, name, **
|
|
24
|
-
attacher ||= from_entity(record, name, **
|
|
22
|
+
attacher = record.send(:"#{name}_attacher", **) if record.respond_to?(:"#{name}_attacher")
|
|
23
|
+
attacher ||= from_model(record, name, **) if model
|
|
24
|
+
attacher ||= from_entity(record, name, **) if entity
|
|
25
25
|
|
|
26
26
|
if attacher.file != attacher.uploaded_file(file)
|
|
27
27
|
fail Shrine::AttachmentChanged, "attachment has changed"
|
|
@@ -43,13 +43,13 @@ class Shrine
|
|
|
43
43
|
#
|
|
44
44
|
# This more convenient to use with concrete persistence plugins, which
|
|
45
45
|
# provide defaults for reloading and persistence.
|
|
46
|
-
def abstract_atomic_promote(reload:, persist:,
|
|
46
|
+
def abstract_atomic_promote(reload:, persist:, **, &block)
|
|
47
47
|
original_file = file
|
|
48
48
|
|
|
49
|
-
result = promote(**
|
|
49
|
+
result = promote(**)
|
|
50
50
|
|
|
51
51
|
begin
|
|
52
|
-
abstract_atomic_persist(original_file, reload
|
|
52
|
+
abstract_atomic_persist(original_file, reload:, persist:, &block)
|
|
53
53
|
result
|
|
54
54
|
rescue Shrine::AttachmentChanged
|
|
55
55
|
destroy_attached
|
|
@@ -47,7 +47,7 @@ class Shrine
|
|
|
47
47
|
# attacher.promote_block do |attacher|
|
|
48
48
|
# Attachment::PromoteJob.perform_async(
|
|
49
49
|
# attacher.record,
|
|
50
|
-
# attacher.name
|
|
50
|
+
# attacher.name,
|
|
51
51
|
# attacher.file_data,
|
|
52
52
|
# )
|
|
53
53
|
# end
|
|
@@ -67,19 +67,19 @@ class Shrine
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Does a background promote if promote block was registered.
|
|
70
|
-
def promote_cached(**
|
|
70
|
+
def promote_cached(**)
|
|
71
71
|
if promote? && promote_block
|
|
72
|
-
promote_background
|
|
72
|
+
promote_background(**)
|
|
73
73
|
else
|
|
74
74
|
super
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# Calls the registered promote block.
|
|
79
|
-
def promote_background(**
|
|
79
|
+
def promote_background(**)
|
|
80
80
|
fail Error, "promote block is not registered" unless promote_block
|
|
81
81
|
|
|
82
|
-
background_block(promote_block, **
|
|
82
|
+
background_block(promote_block, **)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
# Does a background destroy if destroy block was registered.
|
|
@@ -92,19 +92,19 @@ class Shrine
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Calls the registered destroy block.
|
|
95
|
-
def destroy_background(**
|
|
95
|
+
def destroy_background(**)
|
|
96
96
|
fail Error, "destroy block is not registered" unless destroy_block
|
|
97
97
|
|
|
98
|
-
background_block(destroy_block, **
|
|
98
|
+
background_block(destroy_block, **)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
private
|
|
102
102
|
|
|
103
|
-
def background_block(block, **
|
|
103
|
+
def background_block(block, **)
|
|
104
104
|
if block.arity == 1
|
|
105
|
-
block.call(self, **
|
|
105
|
+
block.call(self, **)
|
|
106
106
|
else
|
|
107
|
-
instance_exec(
|
|
107
|
+
instance_exec(**, &block)
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
end
|
|
@@ -16,8 +16,8 @@ class Shrine
|
|
|
16
16
|
# from a database record column.
|
|
17
17
|
#
|
|
18
18
|
# Attacher.from_column('{"id":"...","storage":"...","metadata":{...}}')
|
|
19
|
-
def from_column(data, **
|
|
20
|
-
attacher = new(**
|
|
19
|
+
def from_column(data, **)
|
|
20
|
+
attacher = new(**)
|
|
21
21
|
attacher.load_column(data)
|
|
22
22
|
attacher
|
|
23
23
|
end
|
|
@@ -28,8 +28,8 @@ class Shrine
|
|
|
28
28
|
attr_reader :column_serializer
|
|
29
29
|
|
|
30
30
|
# Allows overriding the default column serializer.
|
|
31
|
-
def initialize(column_serializer: shrine_class.opts[:column][:serializer], **
|
|
32
|
-
super(**
|
|
31
|
+
def initialize(column_serializer: shrine_class.opts[:column][:serializer], **)
|
|
32
|
+
super(**)
|
|
33
33
|
@column_serializer = column_serializer
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -54,7 +54,7 @@ class Shrine
|
|
|
54
54
|
# Converts the column data hash into a string (generates JSON by
|
|
55
55
|
# default).
|
|
56
56
|
#
|
|
57
|
-
# Attacher.serialize_column({ "id" => "...", "storage" => "...", "metadata" => {
|
|
57
|
+
# Attacher.serialize_column({ "id" => "...", "storage" => "...", "metadata" => {} })
|
|
58
58
|
# #=> '{"id":"...","storage":"...","metadata":{...}}'
|
|
59
59
|
#
|
|
60
60
|
# Attacher.serialize_column(nil)
|
|
@@ -75,6 +75,8 @@ class Shrine
|
|
|
75
75
|
# Attacher.deserialize_column(nil)
|
|
76
76
|
# #=> nil
|
|
77
77
|
def deserialize_column(data)
|
|
78
|
+
return nil if data == ""
|
|
79
|
+
|
|
78
80
|
if column_serializer && data && !data.is_a?(Hash)
|
|
79
81
|
column_serializer.load(data)
|
|
80
82
|
else
|
|
@@ -17,6 +17,7 @@ class Shrine
|
|
|
17
17
|
BASE64_REGEXP = /;base64/
|
|
18
18
|
CONTENT_SEPARATOR = /,/
|
|
19
19
|
DEFAULT_CONTENT_TYPE = "text/plain"
|
|
20
|
+
BASE64_ALPHABET = "A-Za-z0-9+/"
|
|
20
21
|
|
|
21
22
|
LOG_SUBSCRIBER = -> (event) do
|
|
22
23
|
Shrine.logger.info "Data URI (#{event.duration}ms) – #{{
|
|
@@ -24,7 +25,7 @@ class Shrine
|
|
|
24
25
|
}.inspect}"
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def self.load_dependencies(uploader,
|
|
28
|
+
def self.load_dependencies(uploader, **)
|
|
28
29
|
uploader.plugin :validation
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -78,13 +79,37 @@ class Shrine
|
|
|
78
79
|
data_file
|
|
79
80
|
end
|
|
80
81
|
|
|
81
|
-
#
|
|
82
|
+
# Raises `ParseError` if the content exceeds the `:max_size` option.
|
|
83
|
+
def verify_data_size!(uri, offset, base64)
|
|
84
|
+
max_size = opts[:data_uri][:max_size]
|
|
85
|
+
return unless max_size
|
|
86
|
+
|
|
87
|
+
size = data_size(uri, offset, base64)
|
|
88
|
+
|
|
89
|
+
raise ParseError, "data URI is too large" if size > max_size
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the size the content will have once decoded. It's exact for
|
|
93
|
+
# base64 and an upper bound for percent-encoded content.
|
|
94
|
+
def data_size(uri, offset, base64)
|
|
95
|
+
return uri.bytesize - offset unless base64
|
|
96
|
+
|
|
97
|
+
header_characters = uri.byteslice(0, offset).count(BASE64_ALPHABET)
|
|
98
|
+
content_characters = uri.count(BASE64_ALPHABET) - header_characters
|
|
99
|
+
|
|
100
|
+
content_characters * 3 / 4 # 4 characters decode into 3 bytes
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parses the data URI string, verifies content size, and returns parts.
|
|
82
104
|
def parse_data_uri(uri)
|
|
83
105
|
scanner = StringScanner.new(uri)
|
|
84
106
|
scanner.scan(DATA_REGEXP) or raise ParseError, "data URI has invalid format"
|
|
85
107
|
media_type = scanner.scan(MEDIA_TYPE_REGEXP)
|
|
86
108
|
base64 = scanner.scan(BASE64_REGEXP)
|
|
87
109
|
scanner.scan(CONTENT_SEPARATOR) or raise ParseError, "data URI has invalid format"
|
|
110
|
+
|
|
111
|
+
verify_data_size!(scanner.string, scanner.pos, base64)
|
|
112
|
+
|
|
88
113
|
content = scanner.post_match
|
|
89
114
|
|
|
90
115
|
{ content_type: media_type, base64: !!base64, data: content }
|
|
@@ -129,7 +154,7 @@ class Shrine
|
|
|
129
154
|
# Generates an error message for failed data URI parse.
|
|
130
155
|
def data_uri_error_messsage(uri, error)
|
|
131
156
|
message = shrine_class.opts[:data_uri][:error_message]
|
|
132
|
-
message = message.call(uri) if message.respond_to?(:call)
|
|
157
|
+
message = message.call(*[uri, error].take(message.arity.abs)) if message.respond_to?(:call)
|
|
133
158
|
message || error.message
|
|
134
159
|
end
|
|
135
160
|
end
|
|
@@ -16,16 +16,16 @@ class Shrine
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
module AttacherMethods
|
|
19
|
-
def url(**
|
|
20
|
-
super || default_url(**
|
|
19
|
+
def url(**)
|
|
20
|
+
super || default_url(**)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
-
def default_url(**
|
|
25
|
+
def default_url(**)
|
|
26
26
|
return unless default_url_block
|
|
27
27
|
|
|
28
|
-
url = instance_exec(
|
|
28
|
+
url = instance_exec(**, &default_url_block)
|
|
29
29
|
|
|
30
30
|
[*default_url_host, url].join
|
|
31
31
|
end
|