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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/README.md +11 -11
  4. data/doc/advantages.md +1 -1
  5. data/doc/attacher.md +1 -1
  6. data/doc/carrierwave.md +2 -2
  7. data/doc/changing_derivatives.md +2 -1
  8. data/doc/changing_location.md +19 -6
  9. data/doc/design.md +4 -4
  10. data/doc/external/articles.md +16 -16
  11. data/doc/external/extensions.md +1 -1
  12. data/doc/getting_started.md +73 -30
  13. data/doc/multiple_files.md +57 -22
  14. data/doc/plugins/activerecord.md +91 -0
  15. data/doc/plugins/backgrounding.md +30 -4
  16. data/doc/plugins/data_uri.md +15 -0
  17. data/doc/plugins/derivation_endpoint.md +39 -1
  18. data/doc/plugins/derivatives.md +13 -2
  19. data/doc/plugins/download_endpoint.md +16 -4
  20. data/doc/plugins/dynamic_storage.md +15 -0
  21. data/doc/plugins/fallback_storage.md +90 -0
  22. data/doc/plugins/keep_files.md +6 -4
  23. data/doc/plugins/rack_response.md +9 -0
  24. data/doc/plugins/refresh_metadata.md +20 -0
  25. data/doc/plugins/sequel.md +57 -0
  26. data/doc/plugins/signature.md +8 -6
  27. data/doc/plugins/tempfile.md +14 -6
  28. data/doc/plugins/upload_options.md +12 -0
  29. data/doc/plugins/url_options.md +12 -0
  30. data/doc/plugins/validation_helpers.md +1 -1
  31. data/doc/processing.md +5 -3
  32. data/doc/refile.md +3 -3
  33. data/doc/release_notes/2.1.0.md +1 -1
  34. data/doc/release_notes/3.10.0.md +30 -0
  35. data/doc/release_notes/3.5.0.md +63 -0
  36. data/doc/release_notes/3.6.0.md +23 -0
  37. data/doc/release_notes/3.7.0.md +75 -0
  38. data/doc/release_notes/3.7.1.md +31 -0
  39. data/doc/release_notes/3.8.0.md +37 -0
  40. data/doc/release_notes/3.9.0.md +42 -0
  41. data/doc/retrieving_uploads.md +1 -1
  42. data/doc/storage/s3.md +10 -0
  43. data/doc/testing.md +45 -17
  44. data/doc/upgrading_to_3.md +3 -5
  45. data/lib/shrine/attacher.rb +41 -29
  46. data/lib/shrine/attachment.rb +2 -2
  47. data/lib/shrine/plugins/_persistence.rb +1 -1
  48. data/lib/shrine/plugins/_urlsafe_serialization.rb +4 -4
  49. data/lib/shrine/plugins/activerecord.rb +25 -4
  50. data/lib/shrine/plugins/add_metadata.rb +2 -4
  51. data/lib/shrine/plugins/atomic_helpers.rb +7 -7
  52. data/lib/shrine/plugins/backgrounding.rb +10 -10
  53. data/lib/shrine/plugins/column.rb +7 -5
  54. data/lib/shrine/plugins/data_uri.rb +28 -3
  55. data/lib/shrine/plugins/default_url.rb +4 -4
  56. data/lib/shrine/plugins/delete_raw.rb +2 -2
  57. data/lib/shrine/plugins/derivation_endpoint.rb +65 -59
  58. data/lib/shrine/plugins/derivatives.rb +35 -21
  59. data/lib/shrine/plugins/download_endpoint.rb +72 -11
  60. data/lib/shrine/plugins/entity.rb +15 -9
  61. data/lib/shrine/plugins/fallback_storage.rb +51 -0
  62. data/lib/shrine/plugins/infer_extension.rb +5 -1
  63. data/lib/shrine/plugins/instrumentation.rb +13 -9
  64. data/lib/shrine/plugins/metadata_attributes.rb +1 -1
  65. data/lib/shrine/plugins/mirroring.rb +10 -10
  66. data/lib/shrine/plugins/model.rb +12 -10
  67. data/lib/shrine/plugins/presign_endpoint.rb +13 -10
  68. data/lib/shrine/plugins/pretty_location.rb +2 -2
  69. data/lib/shrine/plugins/processing.rb +3 -3
  70. data/lib/shrine/plugins/rack_file.rb +2 -2
  71. data/lib/shrine/plugins/rack_response.rb +12 -6
  72. data/lib/shrine/plugins/refresh_metadata.rb +6 -6
  73. data/lib/shrine/plugins/remote_url.rb +4 -4
  74. data/lib/shrine/plugins/remove_attachment.rb +2 -0
  75. data/lib/shrine/plugins/remove_invalid.rb +4 -0
  76. data/lib/shrine/plugins/restore_cached_data.rb +3 -3
  77. data/lib/shrine/plugins/sequel.rb +15 -1
  78. data/lib/shrine/plugins/signature.rb +2 -2
  79. data/lib/shrine/plugins/store_dimensions.rb +2 -2
  80. data/lib/shrine/plugins/tempfile.rb +0 -2
  81. data/lib/shrine/plugins/upload_endpoint.rb +7 -5
  82. data/lib/shrine/plugins/upload_options.rb +18 -4
  83. data/lib/shrine/plugins/url_options.rb +17 -3
  84. data/lib/shrine/plugins/validation.rb +8 -8
  85. data/lib/shrine/plugins/validation_helpers.rb +2 -2
  86. data/lib/shrine/plugins/versions.rb +10 -10
  87. data/lib/shrine/plugins.rb +6 -14
  88. data/lib/shrine/storage/file_system.rb +15 -19
  89. data/lib/shrine/storage/linter.rb +8 -8
  90. data/lib/shrine/storage/memory.rb +1 -3
  91. data/lib/shrine/storage/s3.rb +60 -40
  92. data/lib/shrine/uploaded_file.rb +23 -19
  93. data/lib/shrine/version.rb +1 -1
  94. data/lib/shrine.rb +24 -20
  95. data/shrine.gemspec +10 -8
  96. 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
- <!--DOCUSAURUS_CODE_TABS-->
141
- <!--FactoryBot-->
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
- <!--Rails YAML fixtures-->
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
- <!--END_DOCUSAURUS_CODE_TABS-->
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
- <!--DOCUSAURUS_CODE_TABS-->
186
- <!--Capybara-->
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
- <!--Rack::Test-->
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
- <!--Rack::TestApp-->
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
- app.post "/photos", multipart: {
199
- "photo[image]" => File.open("test/files/image.jpg")
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
- <!--DOCUSAURUS_CODE_TABS-->
211
- <!--ActiveJob-->
229
+ <Tabs>
230
+ <TabItem value="activejob" label="Active Job">
231
+
212
232
  ```rb
213
233
  ActiveJob::Base.queue_adapter = :inline
214
234
  ```
215
- <!--Sidekiq-->
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
- <!--SuckerPunch-->
243
+
244
+ </TabItem>
245
+ <TabItem value="sucker_punch" label="SuckerPunch">
246
+
221
247
  ```rb
222
248
  require "sucker_punch/testing/inline"
223
249
  ```
224
- <!--END_DOCUSAURUS_CODE_TABS-->
250
+
251
+ </TabItem>
252
+ </Tabs>
225
253
 
226
254
  ## Processing
227
255
 
@@ -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 beeen deleted, nothing to do
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 attacher.upload(other_attacher.file)
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
 
@@ -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, **options)
26
- attacher = new(**options)
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; @cache.to_sym; end
50
+ def cache_key = @cache.to_sym
51
51
  # Returns the permanent storage identifier.
52
- def store_key; @store.to_sym; end
52
+ def store_key = @store.to_sym
53
53
 
54
54
  # Returns the uploader that is used for the temporary storage.
55
- def cache; shrine_class.new(cache_key); end
55
+ def cache = shrine_class.new(cache_key)
56
56
  # Returns the uploader that is used for the permanent storage.
57
- def store; shrine_class.new(store_key); end
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(...), metadata: { "foo" => "bar" })
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, **options)
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, **options)
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(...), metadata: { "foo" => "bar" })
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, **options)
95
+ def attach_cached(value, **)
96
96
  if value.is_a?(String) || value.is_a?(Hash)
97
- change(cached(value, **options))
97
+ change(cached(value, **))
98
98
  else
99
- attach(value, storage: cache_key, action: :cache, **options)
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, **options)
117
- file = upload(io, storage, **options) if io
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(**options)
162
- promote(**options) if 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, **options)
171
- set upload(file, storage, action: :store, **options)
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, **options)
182
- shrine_class.upload(io, storage, **context, **options)
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(**options)
253
- file&.url(**options)
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
@@ -22,8 +22,8 @@ class Shrine
22
22
  # Shorthand for `Attachment.new`.
23
23
  #
24
24
  # Shrine::Attachment[:image]
25
- def [](*args, **options)
26
- new(*args, **options)
25
+ def [](*, **)
26
+ new(*, **)
27
27
  end
28
28
  end
29
29
 
@@ -7,7 +7,7 @@ class Shrine
7
7
  #
8
8
  # plugin :_persistence, plugin: MyPlugin
9
9
  module Persistence
10
- def self.load_dependencies(uploader, *)
10
+ def self.load_dependencies(uploader, **)
11
11
  uploader.plugin :atomic_helpers
12
12
  uploader.plugin :entity
13
13
  end
@@ -23,8 +23,8 @@ class Shrine
23
23
  end
24
24
 
25
25
  module FileMethods
26
- def urlsafe_dump(**options)
27
- self.class.urlsafe_dump(self, **options)
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, **options)
49
- data = file.urlsafe_data(**options)
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 column.
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
- column = record.class.columns_hash[attribute.to_s]
116
- column && [:json, :jsonb].include?(column.type)
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, **options)
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:, **options)
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", **options) if record.respond_to?(:"#{name}_attacher")
23
- attacher ||= from_model(record, name, **options) if model
24
- attacher ||= from_entity(record, name, **options) if entity
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:, **options, &block)
46
+ def abstract_atomic_promote(reload:, persist:, **, &block)
47
47
  original_file = file
48
48
 
49
- result = promote(**options)
49
+ result = promote(**)
50
50
 
51
51
  begin
52
- abstract_atomic_persist(original_file, reload: reload, persist: persist, &block)
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(**options)
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(**options)
79
+ def promote_background(**)
80
80
  fail Error, "promote block is not registered" unless promote_block
81
81
 
82
- background_block(promote_block, **options)
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(**options)
95
+ def destroy_background(**)
96
96
  fail Error, "destroy block is not registered" unless destroy_block
97
97
 
98
- background_block(destroy_block, **options)
98
+ background_block(destroy_block, **)
99
99
  end
100
100
 
101
101
  private
102
102
 
103
- def background_block(block, **options)
103
+ def background_block(block, **)
104
104
  if block.arity == 1
105
- block.call(self, **options)
105
+ block.call(self, **)
106
106
  else
107
- instance_exec(**options, &block)
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, **options)
20
- attacher = new(**options)
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], **options)
32
- super(**options)
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
- # Parses the data URI string and returns parts.
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(**options)
20
- super || default_url(**options)
19
+ def url(**)
20
+ super || default_url(**)
21
21
  end
22
22
 
23
23
  private
24
24
 
25
- def default_url(**options)
25
+ def default_url(**)
26
26
  return unless default_url_block
27
27
 
28
- url = instance_exec(**options, &default_url_block)
28
+ url = instance_exec(**, &default_url_block)
29
29
 
30
30
  [*default_url_host, url].join
31
31
  end