shrine 3.3.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +14 -12
  4. data/doc/carrierwave.md +2 -2
  5. data/doc/changing_derivatives.md +1 -1
  6. data/doc/changing_location.md +8 -6
  7. data/doc/design.md +5 -5
  8. data/doc/direct_s3.md +25 -0
  9. data/doc/external/articles.md +16 -16
  10. data/doc/external/extensions.md +1 -1
  11. data/doc/getting_started.md +79 -27
  12. data/doc/metadata.md +1 -1
  13. data/doc/multiple_files.md +57 -22
  14. data/doc/paperclip.md +1 -0
  15. data/doc/plugins/backgrounding.md +4 -4
  16. data/doc/plugins/derivation_endpoint.md +24 -0
  17. data/doc/plugins/derivatives.md +11 -1
  18. data/doc/plugins/entity.md +12 -4
  19. data/doc/plugins/instrumentation.md +1 -1
  20. data/doc/plugins/keep_files.md +6 -4
  21. data/doc/plugins/model.md +8 -3
  22. data/doc/plugins/sequel.md +1 -1
  23. data/doc/plugins/validation_helpers.md +1 -1
  24. data/doc/processing.md +3 -2
  25. data/doc/refile.md +3 -3
  26. data/doc/release_notes/2.1.0.md +1 -1
  27. data/doc/release_notes/3.4.0.md +35 -0
  28. data/doc/release_notes/3.5.0.md +63 -0
  29. data/doc/retrieving_uploads.md +1 -1
  30. data/doc/testing.md +55 -17
  31. data/doc/upgrading_to_3.md +6 -8
  32. data/lib/shrine/plugins/activerecord.rb +3 -3
  33. data/lib/shrine/plugins/derivation_endpoint.rb +31 -30
  34. data/lib/shrine/plugins/derivatives.rb +25 -19
  35. data/lib/shrine/plugins/determine_mime_type.rb +2 -0
  36. data/lib/shrine/plugins/download_endpoint.rb +7 -0
  37. data/lib/shrine/plugins/entity.rb +14 -7
  38. data/lib/shrine/plugins/infer_extension.rb +4 -0
  39. data/lib/shrine/plugins/instrumentation.rb +17 -19
  40. data/lib/shrine/plugins/model.rb +3 -1
  41. data/lib/shrine/plugins/remove_attachment.rb +2 -0
  42. data/lib/shrine/plugins/sequel.rb +1 -1
  43. data/lib/shrine/plugins/validation_helpers.rb +1 -1
  44. data/lib/shrine/storage/s3.rb +17 -7
  45. data/lib/shrine/uploaded_file.rb +3 -2
  46. data/lib/shrine/version.rb +1 -1
  47. data/lib/shrine.rb +3 -3
  48. data/shrine.gemspec +3 -2
  49. metadata +26 -10
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
 
@@ -251,6 +279,16 @@ TestMode.disable_processing(Photo.image_attacher) do
251
279
  end
252
280
  ```
253
281
 
282
+ ## Testing direct upload
283
+
284
+ If you'd like to unit-test direct upload on the server side, you can
285
+ emulate it by uploading a file to `cache` and then assigning it to the record.
286
+
287
+ ```rb
288
+ cached_file = Shrine.upload(some_file, :cache)
289
+ record.attachment = cached_file.to_json
290
+ ```
291
+
254
292
  [DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
255
293
  [`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
256
294
  [aws-sdk-ruby stubs]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html
@@ -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
@@ -237,7 +234,7 @@ class PromoteJob
237
234
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
238
235
  # attachment has changed or record has been deleted, nothing to do
239
236
  end
240
- and
237
+ end
241
238
  ```
242
239
  ```rb
243
240
  class DestroyJob
@@ -258,7 +255,7 @@ class DestroyJob
258
255
  attacher = attacher_class.from_data(data)
259
256
  attacher.destroy
260
257
  end
261
- and
258
+ end
262
259
  ```
263
260
 
264
261
  ### Attacher backgrounding
@@ -439,7 +436,7 @@ creation in the `PromoteJob` instead of the controller:
439
436
  class PromoteJob
440
437
  include Sidekiq::Worker
441
438
 
442
- def perform(attacher_class, record_class, record.id, name, file_data)
439
+ def perform(attacher_class, record_class, record_id, name, file_data)
443
440
  attacher_class = Object.const_get(attacher_class)
444
441
  record = Object.const_get(record_class).find(record_id) # if using Active Record
445
442
 
@@ -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
 
@@ -55,7 +55,7 @@ class Shrine
55
55
  # reload the attacher on record reload
56
56
  define_method :reload do |*args|
57
57
  result = super(*args)
58
- send(:"#{name}_attacher").reload
58
+ send(:"#{name}_attacher").reload if instance_variable_defined?(:"@#{name}_attacher")
59
59
  result
60
60
  end
61
61
  end
@@ -75,8 +75,8 @@ class Shrine
75
75
  def activerecord_validate
76
76
  return unless respond_to?(:errors)
77
77
 
78
- errors.each do |message|
79
- record.errors.add(name, *message)
78
+ errors.each do |(type, options)|
79
+ record.errors.add(name, type, **options.to_h)
80
80
  end
81
81
  end
82
82
 
@@ -28,8 +28,8 @@ class Shrine
28
28
  uploader.opts[:derivation_endpoint] ||= { options: {}, derivations: {} }
29
29
  uploader.opts[:derivation_endpoint][:options].merge!(opts)
30
30
 
31
- unless uploader.opts[:derivation_endpoint][:options][:secret_key]
32
- fail Error, "must provide :secret_key option to derivation_endpoint plugin"
31
+ if !uploader.opts[:derivation_endpoint][:options][:secret_key] && !uploader.opts[:derivation_endpoint][:options][:signer]
32
+ fail Error, "must provide :secret_key option to derivation_endpoint plugin when no custom signer is set"
33
33
  end
34
34
 
35
35
  # instrumentation plugin integration
@@ -197,6 +197,7 @@ class Shrine
197
197
  option :metadata, default: -> { [] }
198
198
  option :prefix
199
199
  option :secret_key
200
+ option :signer
200
201
  option :type
201
202
  option :upload, default: -> { false }
202
203
  option :upload_location, default: -> { default_upload_location }, result: -> (o) { upload_location(o) }
@@ -216,7 +217,7 @@ class Shrine
216
217
  option_definition = self.class.options.fetch(name)
217
218
 
218
219
  value = options.fetch(name) { shrine_class.derivation_options[name] }
219
- value = instance_exec(&value) if value.is_a?(Proc)
220
+ value = instance_exec(&value) if value.is_a?(Proc) && value.arity == 0
220
221
 
221
222
  if value.nil?
222
223
  default = option_definition[:default]
@@ -300,20 +301,36 @@ class Shrine
300
301
  end
301
302
 
302
303
  class Derivation::Url < Derivation::Command
303
- delegate :name, :args, :source, :secret_key
304
+ delegate :name, :args, :source, :secret_key, :signer
304
305
 
305
306
  def call(host: nil, prefix: nil, **options)
306
- [host, *prefix, identifier(**options)].join("/")
307
+ base_url = [host, *prefix].join("/")
308
+ path = path_identifier(metadata: options.delete(:metadata))
309
+
310
+ if signer
311
+ url = [base_url, path].join("/")
312
+ signer.call(url, **options)
313
+ else
314
+ signed_part = signed_url("#{path}?#{query(**options)}")
315
+ [base_url, signed_part].join("/")
316
+ end
307
317
  end
308
318
 
309
319
  private
310
320
 
311
- def identifier(expires_in: nil,
312
- version: nil,
313
- type: nil,
314
- filename: nil,
315
- disposition: nil,
316
- metadata: [])
321
+ def path_identifier(metadata: [])
322
+ [
323
+ name,
324
+ *args,
325
+ source.urlsafe_dump(metadata: metadata)
326
+ ].map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
327
+ end
328
+
329
+ def query(expires_in: nil,
330
+ type: nil,
331
+ filename: nil,
332
+ disposition: nil,
333
+ version: nil)
317
334
 
318
335
  params = {}
319
336
  params[:expires_at] = (Time.now + expires_in).to_i if expires_in
@@ -322,23 +339,7 @@ class Shrine
322
339
  params[:filename] = filename if filename
323
340
  params[:disposition] = disposition if disposition
324
341
 
325
- # serializes the source uploaded file into an URL-safe format
326
- source_component = source.urlsafe_dump(metadata: metadata)
327
-
328
- # generate plain URL
329
- url = plain_url(name, *args, source_component, params)
330
-
331
- # generate signed URL
332
- signed_url(url)
333
- end
334
-
335
- def plain_url(*components, params)
336
- # When using Rack < 2, Rack::Utils#escape_path will escape '/'.
337
- # Escape each component and then join them together.
338
- path = components.map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
339
- query = Rack::Utils.build_query(params)
340
-
341
- "#{path}?#{query}"
342
+ Rack::Utils.build_query(params)
342
343
  end
343
344
 
344
345
  def signed_url(url)
@@ -379,7 +380,7 @@ class Shrine
379
380
  # Returns "404 Not Found" if derivation block is not defined, or if source
380
381
  # file was not found on the storage.
381
382
  def handle_request(request)
382
- verify_signature!(request)
383
+ verify_signature!(request) if secret_key
383
384
  check_expiry!(request)
384
385
 
385
386
  name, *args, serialized_file = request.path_info.split("/")[1..-1]
@@ -603,7 +604,7 @@ class Shrine
603
604
  def instrument_derivation(&block)
604
605
  return yield unless shrine_class.respond_to?(:instrument)
605
606
 
606
- shrine_class.instrument(:derivation, derivation: derivation, &block)
607
+ shrine_class.instrument(:derivation, { derivation: derivation }, &block)
607
608
  end
608
609
 
609
610
  # Massages the derivation result, ensuring it's opened in binary mode,
@@ -12,18 +12,18 @@ class Shrine
12
12
  }.inspect}"
13
13
  end
14
14
 
15
- def self.load_dependencies(uploader, versions_compatibility: false, **)
15
+ def self.load_dependencies(uploader, **)
16
16
  uploader.plugin :default_url
17
-
18
- AttacherMethods.prepend(VersionsCompatibility) if versions_compatibility
19
17
  end
20
18
 
21
- def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
22
- uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key } }
19
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, versions_compatibility: false, **opts)
20
+ uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key }, mutex: true }
23
21
  uploader.opts[:derivatives].merge!(opts)
24
22
 
25
23
  # instrumentation plugin integration
26
24
  uploader.subscribe(:derivatives, &log_subscriber) if uploader.respond_to?(:subscribe)
25
+
26
+ uploader::Attacher.include(VersionsCompatibility) if versions_compatibility
27
27
  end
28
28
 
29
29
  module AttachmentMethods
@@ -109,7 +109,7 @@ class Shrine
109
109
  super(**options)
110
110
 
111
111
  @derivatives = derivatives
112
- @derivatives_mutex = Mutex.new
112
+ @derivatives_mutex = Mutex.new if shrine_class.derivatives_options[:mutex]
113
113
  end
114
114
 
115
115
  # Convenience method for accessing derivatives.
@@ -139,7 +139,7 @@ class Shrine
139
139
  # Allows generating a URL to the derivative by passing the derivative
140
140
  # name.
141
141
  #
142
- # attacher.add_derivatives(thumb: thumb)
142
+ # attacher.add_derivatives({ thumb: thumb })
143
143
  # attacher.url(:thumb) #=> "https://example.org/thumb.jpg"
144
144
  def url(*path, **options)
145
145
  return super if path.empty?
@@ -180,7 +180,7 @@ class Shrine
180
180
 
181
181
  # In addition to deleting the main file it also deletes any derivatives.
182
182
  #
183
- # attacher.add_derivatives(thumb: thumb)
183
+ # attacher.add_derivatives({ thumb: thumb })
184
184
  # attacher.derivatives[:thumb].exists? #=> true
185
185
  # attacher.destroy
186
186
  # attacher.derivatives[:thumb].exists? #=> false
@@ -208,7 +208,7 @@ class Shrine
208
208
  # # {
209
209
  # # thumb: #<Shrine::UploadedFile>,
210
210
  # # }
211
- # attacher.add_derivatives(cropped: cropped)
211
+ # attacher.add_derivatives({ cropped: cropped })
212
212
  # attacher.derivatives #=>
213
213
  # # {
214
214
  # # thumb: #<Shrine::UploadedFile>,
@@ -239,7 +239,7 @@ class Shrine
239
239
 
240
240
  # Uploads given hash of files.
241
241
  #
242
- # hash = attacher.upload_derivatives(thumb: thumb)
242
+ # hash = attacher.upload_derivatives({ thumb: thumb })
243
243
  # hash[:thumb] #=> #<Shrine::UploadedFile>
244
244
  def upload_derivatives(files, **options)
245
245
  map_derivative(files) do |path, file|
@@ -298,10 +298,10 @@ class Shrine
298
298
  # Deep merges given uploaded derivatives with current derivatives.
299
299
  #
300
300
  # attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
301
- # attacher.merge_derivatives(two: uploaded_file)
301
+ # attacher.merge_derivatives({ two: uploaded_file })
302
302
  # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
303
303
  def merge_derivatives(new_derivatives)
304
- @derivatives_mutex.synchronize do
304
+ derivatives_synchronize do
305
305
  merged_derivatives = deep_merge_derivatives(derivatives, new_derivatives)
306
306
  set_derivatives(merged_derivatives)
307
307
  end
@@ -379,7 +379,7 @@ class Shrine
379
379
 
380
380
  # Deletes given hash of uploaded files.
381
381
  #
382
- # attacher.delete_derivatives(thumb: uploaded_file)
382
+ # attacher.delete_derivatives({ thumb: uploaded_file })
383
383
  # uploaded_file.exists? #=> false
384
384
  def delete_derivatives(derivatives = self.derivatives)
385
385
  map_derivative(derivatives) { |_, derivative| derivative.delete }
@@ -387,7 +387,7 @@ class Shrine
387
387
 
388
388
  # Sets the given hash of uploaded files as derivatives.
389
389
  #
390
- # attacher.set_derivatives(thumb: uploaded_file)
390
+ # attacher.set_derivatives({ thumb: uploaded_file })
391
391
  # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
392
392
  def set_derivatives(derivatives)
393
393
  self.derivatives = derivatives
@@ -398,7 +398,7 @@ class Shrine
398
398
  # Adds derivative data into the hash.
399
399
  #
400
400
  # attacher.attach(io)
401
- # attacher.add_derivatives(thumb: thumb)
401
+ # attacher.add_derivatives({ thumb: thumb })
402
402
  # attacher.data
403
403
  # #=>
404
404
  # # {
@@ -507,14 +507,12 @@ class Shrine
507
507
  def instrument_derivatives(processor_name, source, processor_options, &block)
508
508
  return yield unless shrine_class.respond_to?(:instrument)
509
509
 
510
- shrine_class.instrument(
511
- :derivatives,
510
+ shrine_class.instrument(:derivatives, {
512
511
  processor: processor_name,
513
512
  processor_options: processor_options,
514
513
  io: source,
515
514
  attacher: self,
516
- &block
517
- )
515
+ }, &block)
518
516
  end
519
517
 
520
518
  # Returns symbolized array or single key.
@@ -546,6 +544,14 @@ class Shrine
546
544
  def create_derivatives_on_promote?
547
545
  shrine_class.derivatives_options[:create_on_promote]
548
546
  end
547
+
548
+ def derivatives_synchronize
549
+ if @derivatives_mutex
550
+ @derivatives_mutex.synchronize { yield }
551
+ else
552
+ yield
553
+ end
554
+ end
549
555
  end
550
556
 
551
557
  module ClassMethods
@@ -124,6 +124,8 @@ class Shrine
124
124
  require "fastimage"
125
125
 
126
126
  type = FastImage.type(io)
127
+ return 'image/svg+xml' if type == :svg
128
+
127
129
  "image/#{type}" if type
128
130
  end
129
131
 
@@ -182,12 +182,19 @@ class Shrine
182
182
  @shrine_class::UploadedFile.urlsafe_load(serialized)
183
183
  rescue Shrine::Error # storage not found
184
184
  not_found!
185
+ rescue JSON::ParserError, ArgumentError => error # invalid serialized component
186
+ raise if error.is_a?(ArgumentError) && error.message != "invalid base64"
187
+ bad_request!("Invalid serialized file")
185
188
  end
186
189
 
187
190
  def not_found!
188
191
  error!(404, "File Not Found")
189
192
  end
190
193
 
194
+ def bad_request!(message)
195
+ error!(400, message)
196
+ end
197
+
191
198
  # Halts the request with the error message.
192
199
  def error!(status, message)
193
200
  throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
@@ -36,8 +36,14 @@ class Shrine
36
36
  attachment = self
37
37
 
38
38
  # Returns the attached file.
39
- define_method :"#{name}" do |*args|
40
- send(:"#{name}_attacher").get(*args)
39
+ if shrine_class::Attacher.instance_method(:get).arity == 0
40
+ define_method :"#{name}" do
41
+ send(:"#{name}_attacher").get
42
+ end
43
+ else # derivatives
44
+ define_method :"#{name}" do |*args|
45
+ send(:"#{name}_attacher").get(*args)
46
+ end
41
47
  end
42
48
 
43
49
  # Returns the URL to the attached file.
@@ -112,9 +118,15 @@ class Shrine
112
118
  # attacher.file #=> #<Shrine::UploadedFile>
113
119
  def reload
114
120
  read
121
+ @previous = nil
115
122
  self
116
123
  end
117
124
 
125
+ # Loads attachment from the entity attribute.
126
+ def read
127
+ load_column(read_attribute)
128
+ end
129
+
118
130
  # Returns a hash with entity attribute name and column data.
119
131
  #
120
132
  # attacher.column_values
@@ -134,11 +146,6 @@ class Shrine
134
146
 
135
147
  private
136
148
 
137
- # Loads attachment from the entity attribute.
138
- def read
139
- load_column(read_attribute)
140
- end
141
-
142
149
  # Reads value from the entity attribute.
143
150
  def read_attribute
144
151
  record.public_send(attribute)
@@ -51,6 +51,10 @@ class Shrine
51
51
  end
52
52
 
53
53
  module InstanceMethods
54
+ def infer_extension(mime_type)
55
+ self.class.infer_extension(mime_type)
56
+ end
57
+
54
58
  private
55
59
 
56
60
  def basic_location(io, metadata:)
@@ -12,7 +12,11 @@ class Shrine
12
12
  def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
13
13
  uploader.opts[:instrumentation] ||= { log_events: EVENTS, subscribers: {} }
14
14
  uploader.opts[:instrumentation].merge!(opts)
15
- uploader.opts[:instrumentation][:notifications] ||= ::ActiveSupport::Notifications
15
+ begin
16
+ uploader.opts[:instrumentation][:notifications] ||= ::ActiveSupport::Notifications
17
+ rescue NameError
18
+ fail Error, "default notifications library is ActiveSupport::Notifications, but activesupport gem is not installed"
19
+ end
16
20
 
17
21
  uploader.opts[:instrumentation][:log_events].each do |event_name|
18
22
  uploader.subscribe(event_name, &log_subscriber)
@@ -69,27 +73,25 @@ class Shrine
69
73
 
70
74
  # Sends a `upload.shrine` event.
71
75
  def _upload(io, location:, metadata:, upload_options: {}, **options)
72
- self.class.instrument(
73
- :upload,
76
+ self.class.instrument(:upload, {
74
77
  storage: storage_key,
75
78
  location: location,
76
79
  io: io,
77
80
  upload_options: upload_options,
78
81
  metadata: metadata,
79
82
  options: options,
80
- ) { super }
83
+ }) { super }
81
84
  end
82
85
 
83
86
  # Sends a `metadata.shrine` event.
84
87
  def get_metadata(io, metadata: nil, **options)
85
88
  return super if io.is_a?(UploadedFile) && metadata != true || metadata == false
86
89
 
87
- self.class.instrument(
88
- :metadata,
90
+ self.class.instrument(:metadata, {
89
91
  storage: storage_key,
90
92
  io: io,
91
93
  options: options,
92
- ) { super }
94
+ }) { super }
93
95
  end
94
96
  end
95
97
 
@@ -98,30 +100,27 @@ class Shrine
98
100
  def stream(destination, **options)
99
101
  return super if opened?
100
102
 
101
- shrine_class.instrument(
102
- :download,
103
+ shrine_class.instrument(:download, {
103
104
  storage: storage_key,
104
105
  location: id,
105
106
  download_options: options,
106
- ) { super(destination, **options, instrument: false) }
107
+ }) { super(destination, **options, instrument: false) }
107
108
  end
108
109
 
109
110
  # Sends a `exists.shrine` event.
110
111
  def exists?
111
- shrine_class.instrument(
112
- :exists,
112
+ shrine_class.instrument(:exists, {
113
113
  storage: storage_key,
114
114
  location: id,
115
- ) { super }
115
+ }) { super }
116
116
  end
117
117
 
118
118
  # Sends a `delete.shrine` event.
119
119
  def delete
120
- shrine_class.instrument(
121
- :delete,
120
+ shrine_class.instrument(:delete, {
122
121
  storage: storage_key,
123
122
  location: id,
124
- ) { super }
123
+ }) { super }
125
124
  end
126
125
 
127
126
  private
@@ -130,12 +129,11 @@ class Shrine
130
129
  def _open(instrument: true, **options)
131
130
  return super(**options) unless instrument
132
131
 
133
- shrine_class.instrument(
134
- :open,
132
+ shrine_class.instrument(:open, {
135
133
  storage: storage_key,
136
134
  location: id,
137
135
  download_options: options,
138
- ) { super(**options) }
136
+ }) { super(**options) }
139
137
  end
140
138
  end
141
139
 
@@ -48,7 +48,9 @@ class Shrine
48
48
  # The copy constructor that's called on #dup and #clone.
49
49
  define_method :initialize_copy do |other|
50
50
  super(other)
51
- instance_variable_set(:"@#{name}_attacher", instance_variable_get(:"@#{name}_attacher")&.dup)
51
+ attacher_copy = instance_variable_get(:"@#{name}_attacher")&.dup
52
+ attacher_copy.set_entity(self, name) if attacher_copy
53
+ instance_variable_set(:"@#{name}_attacher", attacher_copy)
52
54
  self
53
55
  end
54
56
  private :initialize_copy
@@ -39,6 +39,8 @@ class Shrine
39
39
 
40
40
  # Rails sends "0" or "false" if the checkbox hasn't been ticked.
41
41
  def remove?
42
+ return remove if [true, false].include?(remove)
43
+
42
44
  remove && remove != "" && remove !~ /\A(0|false)\z/
43
45
  end
44
46
  end
@@ -62,7 +62,7 @@ class Shrine
62
62
  # reload the attacher on record reload
63
63
  define_method :_refresh do |*args|
64
64
  result = super(*args)
65
- send(:"#{name}_attacher").reload
65
+ send(:"#{name}_attacher").reload if instance_variable_defined?(:"@#{name}_attacher")
66
66
  result
67
67
  end
68
68
  private :_refresh