shrine 2.18.1 → 2.19.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.

Potentially problematic release.


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

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -1
  3. data/README.md +96 -137
  4. data/doc/advantages.md +4 -4
  5. data/doc/attacher.md +1 -2
  6. data/doc/carrierwave.md +3 -2
  7. data/doc/creating_storages.md +0 -20
  8. data/doc/design.md +1 -1
  9. data/doc/metadata.md +62 -36
  10. data/doc/paperclip.md +7 -6
  11. data/doc/plugins/data_uri.md +50 -4
  12. data/doc/plugins/derivation_endpoint.md +24 -0
  13. data/doc/plugins/determine_mime_type.md +47 -5
  14. data/doc/plugins/infer_extension.md +45 -9
  15. data/doc/plugins/instrumentation.md +170 -0
  16. data/doc/plugins/presign_endpoint.md +1 -1
  17. data/doc/plugins/pretty_location.md +23 -0
  18. data/doc/plugins/remote_url.md +59 -8
  19. data/doc/plugins/signature.md +54 -7
  20. data/doc/plugins/store_dimensions.md +69 -4
  21. data/doc/plugins/upload_endpoint.md +2 -2
  22. data/doc/plugins/validation_helpers.md +71 -29
  23. data/doc/refile.md +1 -1
  24. data/doc/release_notes/2.18.0.md +2 -2
  25. data/doc/release_notes/2.19.0.md +263 -0
  26. data/doc/storage/file_system.md +26 -8
  27. data/doc/testing.md +10 -10
  28. data/lib/shrine.rb +32 -16
  29. data/lib/shrine/attacher.rb +3 -0
  30. data/lib/shrine/attachment.rb +3 -0
  31. data/lib/shrine/plugins/add_metadata.rb +12 -16
  32. data/lib/shrine/plugins/backup.rb +2 -0
  33. data/lib/shrine/plugins/copy.rb +2 -0
  34. data/lib/shrine/plugins/data_uri.rb +56 -28
  35. data/lib/shrine/plugins/derivation_endpoint.rb +61 -27
  36. data/lib/shrine/plugins/determine_mime_type.rb +27 -5
  37. data/lib/shrine/plugins/infer_extension.rb +26 -5
  38. data/lib/shrine/plugins/instrumentation.rb +300 -0
  39. data/lib/shrine/plugins/logging.rb +2 -0
  40. data/lib/shrine/plugins/moving.rb +2 -0
  41. data/lib/shrine/plugins/pretty_location.rb +21 -12
  42. data/lib/shrine/plugins/rack_file.rb +23 -18
  43. data/lib/shrine/plugins/refresh_metadata.rb +4 -4
  44. data/lib/shrine/plugins/remote_url.rb +42 -23
  45. data/lib/shrine/plugins/signature.rb +32 -1
  46. data/lib/shrine/plugins/store_dimensions.rb +54 -9
  47. data/lib/shrine/plugins/validation_helpers.rb +148 -47
  48. data/lib/shrine/storage/file_system.rb +32 -15
  49. data/lib/shrine/storage/linter.rb +0 -13
  50. data/lib/shrine/storage/s3.rb +2 -5
  51. data/lib/shrine/uploaded_file.rb +8 -0
  52. data/lib/shrine/version.rb +2 -2
  53. data/shrine.gemspec +18 -3
  54. metadata +58 -27
@@ -12,13 +12,21 @@ class Shrine
12
12
  #
13
13
  # [doc/plugins/derivation_endpoint.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/derivation_endpoint.md
14
14
  module DerivationEndpoint
15
+ LOG_SUBSCRIBER = -> (event) do
16
+ Shrine.logger.info "Derivation (#{event.duration}ms) – #{{
17
+ name: event[:name],
18
+ args: event[:args],
19
+ uploader: event[:uploader],
20
+ }.inspect}"
21
+ end
22
+
15
23
  def self.load_dependencies(uploader, opts = {})
16
24
  uploader.plugin :rack_response
17
25
  uploader.plugin :_urlsafe_serialization
18
26
  end
19
27
 
20
28
  def self.configure(uploader, opts = {})
21
- uploader.opts[:derivation_endpoint_options] ||= {}
29
+ uploader.opts[:derivation_endpoint_options] ||= { log_subscriber: LOG_SUBSCRIBER }
22
30
  uploader.opts[:derivation_endpoint_options].merge!(opts)
23
31
 
24
32
  uploader.opts[:derivation_endpoint_derivations] ||= {}
@@ -26,6 +34,11 @@ class Shrine
26
34
  unless uploader.opts[:derivation_endpoint_options][:secret_key]
27
35
  fail Error, "must provide :secret_key option to derivation_endpoint plugin"
28
36
  end
37
+
38
+ # instrumentation plugin integration
39
+ if uploader.respond_to?(:subscribe)
40
+ uploader.subscribe(:derivation, &uploader.opts[:derivation_endpoint_options][:log_subscriber])
41
+ end
29
42
  end
30
43
 
31
44
  module ClassMethods
@@ -192,7 +205,7 @@ class Shrine
192
205
  option :upload_options, default: -> { {} }
193
206
  option :upload_redirect, default: -> { false }
194
207
  option :upload_redirect_url_options, default: -> { {} }
195
- option :upload_storage, default: -> { source.storage_key.to_sym }
208
+ option :upload_storage, default: -> { default_upload_storage }
196
209
  option :version
197
210
 
198
211
  # Retrieves the value of a derivation option.
@@ -245,6 +258,11 @@ class Shrine
245
258
  [directory, filename].join("/")
246
259
  end
247
260
 
261
+ # The source uploaded file storage is the default derivative storage.
262
+ def default_upload_storage
263
+ source.storage_key.to_sym
264
+ end
265
+
248
266
  # Allows caching for 1 year or until the URL expires.
249
267
  def default_cache_control
250
268
  if option(:expires_in)
@@ -504,7 +522,7 @@ class Shrine
504
522
 
505
523
  [302, { "Location" => redirect_url }, []]
506
524
  else
507
- if derivative
525
+ if derivative && File.exist?(derivative.path)
508
526
  file_response(derivative, env)
509
527
  else
510
528
  uploaded_file.open(**upload_open_options)
@@ -526,7 +544,6 @@ class Shrine
526
544
  if Rack.release > "2"
527
545
  server.serving(Rack::Request.new(env), path)
528
546
  else
529
- server = server.dup
530
547
  server.path = path
531
548
  server.serving(env)
532
549
  end
@@ -567,23 +584,43 @@ class Shrine
567
584
 
568
585
  private
569
586
 
570
- # Calls the derivation block with the source file and derivation arguments.
571
- # If a file object is given, passes that as the source file, otherwise
572
- # downloads the source uploaded file.
587
+ # Determines how to call the derivation block. If a file object is given,
588
+ # passes that as the source file, otherwise downloads the source uploaded
589
+ # file.
573
590
  def generate(file)
574
591
  if download
575
592
  with_downloaded(file) do |file|
576
593
  if include_uploaded_file
577
- uploader.instance_exec(file, source, *args, &derivation_block)
594
+ derive(file, source, *args)
578
595
  else
579
- uploader.instance_exec(file, *args, &derivation_block)
596
+ derive(file, *args)
580
597
  end
581
598
  end
582
599
  else
583
- uploader.instance_exec(source, *args, &derivation_block)
600
+ derive(source, *args)
584
601
  end
585
602
  end
586
603
 
604
+ # Calls the derivation block.
605
+ def derive(*args)
606
+ instrument_derivation do
607
+ uploader.instance_exec(*args, &derivation_block)
608
+ end
609
+ end
610
+
611
+ # Sends a `derivation.shrine` event for instrumentation plugin.
612
+ def instrument_derivation(&block)
613
+ return yield unless shrine_class.respond_to?(:instrument)
614
+
615
+ shrine_class.instrument(
616
+ :derivation,
617
+ name: derivation.name,
618
+ args: derivation.args,
619
+ derivation: derivation,
620
+ &block
621
+ )
622
+ end
623
+
587
624
  # Massages the derivation result, ensuring it's opened in binary mode,
588
625
  # rewinded and flushed to disk.
589
626
  def normalize(derivative)
@@ -644,9 +681,7 @@ class Shrine
644
681
  with_derivative(derivative) do |uploadable|
645
682
  uploader.upload uploadable,
646
683
  location: upload_location,
647
- upload_options: upload_options,
648
- delete: false, # disable delete_raw plugin
649
- move: false # disable moving plugin
684
+ upload_options: upload_options
650
685
  end
651
686
  end
652
687
 
@@ -654,9 +689,14 @@ class Shrine
654
689
 
655
690
  def with_derivative(derivative)
656
691
  if derivative
657
- # we want to keep the provided file open and rewinded
658
- File.open(derivative.path, binmode: true) do |file|
659
- yield file
692
+ begin
693
+ # we want to keep the provided file open and rewinded
694
+ File.open(derivative.path, binmode: true) do |file|
695
+ yield file
696
+ end
697
+ ensure
698
+ # close the file handler if the file was deleted during upload
699
+ derivative.close if !File.exist?(derivative.path)
660
700
  end
661
701
  else
662
702
  # generate the derivative and delete it afterwards
@@ -681,18 +721,12 @@ class Shrine
681
721
  # Returns a Shrine::UploadedFile object pointing to the uploaded derivation
682
722
  # result it exists on the storage.
683
723
  def call
684
- if storage.exists?(upload_location)
685
- shrine_class::UploadedFile.new(
686
- "storage" => upload_storage.to_s,
687
- "id" => upload_location,
688
- )
689
- end
690
- end
691
-
692
- private
724
+ uploaded_file = shrine_class::UploadedFile.new(
725
+ "storage" => upload_storage.to_s,
726
+ "id" => upload_location,
727
+ )
693
728
 
694
- def storage
695
- shrine_class.find_storage(upload_storage)
729
+ uploaded_file if uploaded_file.exists?
696
730
  end
697
731
  end
698
732
 
@@ -6,34 +6,47 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/determine_mime_type.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/determine_mime_type.md
8
8
  module DetermineMimeType
9
+ LOG_SUBSCRIBER = -> (event) do
10
+ Shrine.logger.info "MIME Type (#{event.duration}ms) – #{{
11
+ io: event[:io].class,
12
+ uploader: event[:uploader],
13
+ }.inspect}"
14
+ end
15
+
9
16
  def self.configure(uploader, opts = {})
10
17
  if opts[:analyzer] == :default
11
18
  Shrine.deprecation("The :default analyzer of the determine_mime_type plugin has been renamed to :content_type. The :default alias will not be supported in Shrine 3.")
12
19
  opts = opts.merge(analyzer: :content_type)
13
20
  end
14
21
 
15
- uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
16
- uploader.opts[:mime_type_analyzer_options] = opts.fetch(:analyzer_options, uploader.opts.fetch(:mime_type_analyzer_options, {}))
22
+ uploader.opts[:determine_mime_type] ||= { analyzer: :file, analyzer_options: {}, log_subscriber: LOG_SUBSCRIBER }
23
+ uploader.opts[:determine_mime_type].merge!(opts)
24
+
25
+ # instrumentation plugin integration
26
+ if uploader.respond_to?(:subscribe)
27
+ uploader.subscribe(:mime_type, &uploader.opts[:determine_mime_type][:log_subscriber])
28
+ end
17
29
  end
18
30
 
19
31
  module ClassMethods
20
32
  # Determines the MIME type of the IO object by calling the specified
21
33
  # analyzer.
22
34
  def determine_mime_type(io)
23
- analyzer = opts[:mime_type_analyzer]
35
+ analyzer = opts[:determine_mime_type][:analyzer]
24
36
 
25
37
  analyzer = mime_type_analyzer(analyzer) if analyzer.is_a?(Symbol)
26
38
  args = if analyzer.is_a?(Proc)
27
39
  [io, mime_type_analyzers].take(analyzer.arity.abs)
28
40
  else
29
- [io, opts[:mime_type_analyzer_options]]
41
+ [io, opts[:determine_mime_type][:analyzer_options]]
30
42
  end
31
43
 
32
- mime_type = analyzer.call(*args)
44
+ mime_type = instrument_mime_type(io) { analyzer.call(*args) }
33
45
  io.rewind
34
46
 
35
47
  mime_type
36
48
  end
49
+ alias mime_type determine_mime_type
37
50
 
38
51
  # Returns a hash of built-in MIME type analyzers, where keys are
39
52
  # analyzer names and values are `#call`-able objects which accepts the
@@ -48,6 +61,15 @@ class Shrine
48
61
  def mime_type_analyzer(name)
49
62
  MimeTypeAnalyzer.new(name)
50
63
  end
64
+
65
+ private
66
+
67
+ # Sends a `mime_type.shrine` event for instrumentation plugin.
68
+ def instrument_mime_type(io, &block)
69
+ return yield unless respond_to?(:instrument)
70
+
71
+ instrument(:mime_type, io: io, &block)
72
+ end
51
73
  end
52
74
 
53
75
  module InstanceMethods
@@ -6,18 +6,30 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/infer_extension.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/infer_extension.md
8
8
  module InferExtension
9
+ LOG_SUBSCRIBER = -> (event) do
10
+ Shrine.logger.info "Extension (#{event.duration}ms) – #{{
11
+ mime_type: event[:mime_type],
12
+ uploader: event[:uploader],
13
+ }.inspect}"
14
+ end
15
+
9
16
  def self.configure(uploader, opts = {})
10
- uploader.opts[:infer_extension_inferrer] = opts.fetch(:inferrer, uploader.opts.fetch(:infer_extension_inferrer, :mime_types))
11
- uploader.opts[:infer_extension_force] = opts.fetch(:force, uploader.opts.fetch(:infer_extension_force, false))
17
+ uploader.opts[:infer_extension] ||= { inferrer: :mime_types, force: false, log_subscriber: LOG_SUBSCRIBER }
18
+ uploader.opts[:infer_extension].merge!(opts)
19
+
20
+ # instrumentation plugin integration
21
+ if uploader.respond_to?(:subscribe)
22
+ uploader.subscribe(:extension, &uploader.opts[:infer_extension][:log_subscriber])
23
+ end
12
24
  end
13
25
 
14
26
  module ClassMethods
15
27
  def infer_extension(mime_type)
16
- inferrer = opts[:infer_extension_inferrer]
28
+ inferrer = opts[:infer_extension][:inferrer]
17
29
  inferrer = extension_inferrer(inferrer) if inferrer.is_a?(Symbol)
18
30
  args = [mime_type, extension_inferrers].take(inferrer.arity.abs)
19
31
 
20
- inferrer.call(*args)
32
+ instrument_extension(mime_type) { inferrer.call(*args) }
21
33
  end
22
34
 
23
35
  def extension_inferrers
@@ -29,6 +41,15 @@ class Shrine
29
41
  def extension_inferrer(name)
30
42
  ExtensionInferrer.new(name).method(:call)
31
43
  end
44
+
45
+ private
46
+
47
+ # Sends a `extension.shrine` event for instrumentation plugin.
48
+ def instrument_extension(mime_type, &block)
49
+ return yield unless respond_to?(:instrument)
50
+
51
+ instrument(:extension, mime_type: mime_type, &block)
52
+ end
32
53
  end
33
54
 
34
55
  module InstanceMethods
@@ -38,7 +59,7 @@ class Shrine
38
59
  location = super
39
60
  current_extension = File.extname(location)
40
61
 
41
- if current_extension.empty? || opts[:infer_extension_force]
62
+ if current_extension.empty? || opts[:infer_extension][:force]
42
63
  inferred_extension = infer_extension(mime_type)
43
64
  location = location.chomp(current_extension) << inferred_extension unless inferred_extension.empty?
44
65
  end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation lives in [doc/plugins/instrumentation.md] on GitHub.
6
+ #
7
+ # [doc/plugins/instrumentation.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/instrumentation.md
8
+ module Instrumentation
9
+ EVENTS = %i[upload download exists delete metadata].freeze
10
+
11
+ # We use a proc in order to be able identify listeners.
12
+ LOG_SUBSCRIBER = -> (event) { LogSubscriber.call(event) }
13
+
14
+ def self.configure(uploader, opts = {})
15
+ uploader.opts[:instrumentation] ||= { log_subscriber: LOG_SUBSCRIBER, log_events: EVENTS }
16
+ uploader.opts[:instrumentation].merge!(opts)
17
+ uploader.opts[:instrumentation][:notifications] ||= ::ActiveSupport::Notifications
18
+
19
+ # we assign it to the top-level so that it's duplicated on subclassing
20
+ uploader.opts[:instrumentation_subscribers] ||= Hash.new { |h, k| h[k] = [] }
21
+
22
+ uploader.opts[:instrumentation][:log_events].each do |event_name|
23
+ uploader.subscribe(event_name, &uploader.opts[:instrumentation][:log_subscriber])
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ # Sends a `*.shrine` event.
29
+ #
30
+ # # sends a `my_event.shrine` event
31
+ # Shrine.instrument(:my_event) do
32
+ # # work
33
+ # end
34
+ def instrument(event_name, payload = {}, &block)
35
+ payload[:uploader] = self
36
+
37
+ notifications.instrument("#{event_name}.shrine", payload, &block)
38
+ end
39
+
40
+ # Subscribes to a `*.shrine` event. It rejects duplicate subscribers.
41
+ #
42
+ # # subscribes to the `storage_upload.shrine` event
43
+ # Shrine.subscribe(:storage_upload) do |event|
44
+ # event.name #=> :storage_upload
45
+ # event.payload #=> { location: "...", ... }
46
+ # event[:location] #=> "..."
47
+ # event.duration #=> 50 (in milliseconds)
48
+ # end
49
+ def subscribe(event_name, &subscriber)
50
+ return if subscriber.nil?
51
+ return if subscribers[event_name].include?(subscriber)
52
+
53
+ notifications.subscribe("#{event_name}.shrine") do |event|
54
+ subscriber.call(event) if event[:uploader] <= self
55
+ end
56
+
57
+ subscribers[event_name] << subscriber
58
+ end
59
+
60
+ private
61
+
62
+ def notifications
63
+ Notifications.new(opts[:instrumentation][:notifications])
64
+ end
65
+
66
+ def log_subscriber
67
+ opts[:instrumentation][:log_subscriber]
68
+ end
69
+
70
+ def subscribers
71
+ opts[:instrumentation_subscribers]
72
+ end
73
+ end
74
+
75
+ module InstanceMethods
76
+ private
77
+
78
+ # Sends a `upload.shrine` event.
79
+ def copy(io, context)
80
+ self.class.instrument(
81
+ :upload,
82
+ storage: storage_key,
83
+ location: context[:location],
84
+ io: io,
85
+ upload_options: context[:upload_options] || {},
86
+ options: context,
87
+ ) { super }
88
+ end
89
+
90
+ # Sends a `metadata.shrine` event.
91
+ def get_metadata(io, context)
92
+ return super if io.is_a?(UploadedFile) && context[:metadata] != true || context[:metadata] == false
93
+
94
+ self.class.instrument(
95
+ :metadata,
96
+ storage: storage_key,
97
+ io: io,
98
+ options: context,
99
+ ) { super }
100
+ end
101
+ end
102
+
103
+ module FileMethods
104
+ # Sends a `download.shrine` event.
105
+ def open(**options)
106
+ shrine_class.instrument(
107
+ :download,
108
+ storage: storage_key.to_sym,
109
+ location: id,
110
+ download_options: options,
111
+ ) { super }
112
+ end
113
+
114
+ # Sends a `exists.shrine` event.
115
+ def exists?
116
+ shrine_class.instrument(
117
+ :exists,
118
+ storage: storage_key.to_sym,
119
+ location: id,
120
+ ) { super }
121
+ end
122
+
123
+ # Sends a `delete.shrine` event.
124
+ def delete
125
+ shrine_class.instrument(
126
+ :delete,
127
+ storage: storage_key.to_sym,
128
+ location: id,
129
+ ) { super }
130
+ end
131
+ end
132
+
133
+ # Abstracts away different types of notifications objects
134
+ # (`ActiveSupport::Notifications` and `Dry::Monitor::Notifications`).
135
+ class Notifications
136
+ attr_reader :notifications
137
+
138
+ def initialize(notifications)
139
+ @notifications = notifications
140
+ end
141
+
142
+ def subscribe(event_name, &block)
143
+ library_send(:subscribe, event_name) do |event|
144
+ yield Event.new(event)
145
+ end
146
+ end
147
+
148
+ def instrument(event_name, payload, &block)
149
+ notifications.instrument(event_name, payload, &block)
150
+ end
151
+
152
+ private
153
+
154
+ def dry_monitor_subscribe(event_name, &block)
155
+ notifications.register_event(event_name)
156
+ notifications.subscribe(event_name, &block)
157
+ end
158
+
159
+ def active_support_subscribe(event_name, &block)
160
+ notifications.subscribe(event_name) do |*args|
161
+ yield ActiveSupport::Notifications::Event.new(*args)
162
+ end
163
+ end
164
+
165
+ def library_send(method_name, *args, &block)
166
+ case notifications.to_s
167
+ when /Dry::Monitor::Notifications/
168
+ send(:"dry_monitor_#{method_name}", *args, &block)
169
+ when /ActiveSupport::Notifications/
170
+ send(:"active_support_#{method_name}", *args, &block)
171
+ else
172
+ notifications.send(method_name, *args, &block)
173
+ end
174
+ end
175
+ end
176
+
177
+ # Abstracts away different kind of event objects
178
+ # (`ActiveSupport::Notifications::Event` and `Dry::Events::Event`).
179
+ class Event
180
+ attr_reader :event
181
+
182
+ def initialize(event)
183
+ @event = event
184
+ end
185
+
186
+ def name
187
+ library_send(:name).chomp(".shrine").to_sym
188
+ end
189
+
190
+ def payload
191
+ event.payload
192
+ end
193
+
194
+ def [](name)
195
+ event.payload.fetch(name)
196
+ end
197
+
198
+ def duration
199
+ library_send(:duration)
200
+ end
201
+
202
+ private
203
+
204
+ def dry_events_name
205
+ event.id
206
+ end
207
+
208
+ def active_support_name
209
+ event.name
210
+ end
211
+
212
+ def dry_events_duration
213
+ event[:time]
214
+ end
215
+
216
+ def active_support_duration
217
+ event.duration.to_i
218
+ end
219
+
220
+ def library_send(method_name, *args, &block)
221
+ case event.class.name
222
+ when "ActiveSupport::Notifications::Event"
223
+ send(:"active_support_#{method_name}", *args, &block)
224
+ when "Dry::Events::Event"
225
+ send(:"dry_events_#{method_name}", *args, &block)
226
+ else
227
+ event.send(method_name, *args, &block)
228
+ end
229
+ end
230
+ end
231
+
232
+ # Logs received events.
233
+ class LogSubscriber
234
+ # Entry point for logging.
235
+ def self.call(event)
236
+ new.public_send(:"on_#{event.name}", event)
237
+ end
238
+
239
+ def on_upload(event)
240
+ log "Upload (#{event.duration}ms) – #{format(
241
+ storage: event[:storage],
242
+ location: event[:location],
243
+ io: event[:io].class,
244
+ upload_options: event[:upload_options],
245
+ uploader: event[:uploader],
246
+ )}"
247
+ end
248
+
249
+ def on_download(event)
250
+ log "Download (#{event.duration}ms) – #{format(
251
+ storage: event[:storage],
252
+ location: event[:location],
253
+ download_options: event[:download_options],
254
+ uploader: event[:uploader],
255
+ )}"
256
+ end
257
+
258
+ def on_exists(event)
259
+ log "Exists (#{event.duration}ms) – #{format(
260
+ storage: event[:storage],
261
+ location: event[:location],
262
+ uploader: event[:uploader],
263
+ )}"
264
+ end
265
+
266
+ def on_delete(event)
267
+ log "Delete (#{event.duration}ms) – #{format(
268
+ storage: event[:storage],
269
+ location: event[:location],
270
+ uploader: event[:uploader],
271
+ )}"
272
+ end
273
+
274
+ def on_metadata(event)
275
+ log "Metadata (#{event.duration}ms) – #{format(
276
+ storage: event[:storage],
277
+ io: event[:io].class,
278
+ uploader: event[:uploader],
279
+ )}"
280
+ end
281
+
282
+ private
283
+
284
+ def format(properties = {})
285
+ properties.inspect
286
+ end
287
+
288
+ def log(message)
289
+ logger.info(message)
290
+ end
291
+
292
+ def logger
293
+ Shrine.logger
294
+ end
295
+ end
296
+ end
297
+
298
+ register_plugin(:instrumentation, Instrumentation)
299
+ end
300
+ end