shrine 2.18.1 → 2.19.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

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
data/doc/attacher.md CHANGED
@@ -184,8 +184,7 @@ attacher.promote(cached_file, action: :custom_name)
184
184
  ```
185
185
 
186
186
  The `:action` parameter is optional; it can be used for triggering a certain
187
- processing block, and it is also automatically printed by the `logging` plugin
188
- to aid in debugging.
187
+ processing block, or for additional context during instrumentation.
189
188
 
190
189
  As a matter of fact, all additional options passed to `#promote` will be
191
190
  forwarded to `Shrine#upload`. So unless you're generating versions, you can do
data/doc/carrierwave.md CHANGED
@@ -613,10 +613,11 @@ Shrine.plugin :keep_files, cached: true, replaced: true
613
613
 
614
614
  #### `move_to_cache`, `move_to_store`
615
615
 
616
- Shrine brings this functionality through the `moving` plugin.
616
+ You can tell the `FileSystem` storage that it should move files by specifying
617
+ the `:move` upload option:
617
618
 
618
619
  ```rb
619
- Shrine.plugin :moving, storages: [:cache]
620
+ Shrine.plugin :upload_options, cache: { move: true }, store: { move: true }
620
621
  ```
621
622
 
622
623
  #### `validate_integrity`, `ignore_integrity_errors`
@@ -204,26 +204,6 @@ The storage can support additional options to customize how the presign will be
204
204
  generated, those can be forwarded via the `:presign_options` option on the
205
205
  `presign_endpoint` plugin.
206
206
 
207
- ## Move
208
-
209
- If your storage can move files, you can add the additional `#move` and
210
- `#movable?` methods, and they will automatically get used if the `moving`
211
- plugin is loaded.
212
-
213
- ```rb
214
- class MyStorage
215
- # ...
216
- def move(io, id, **upload_options)
217
- # does the moving of the `io` to the location `id`
218
- end
219
-
220
- def movable?(io, id)
221
- # whether the given `io` is movable to the location `id`
222
- end
223
- # ...
224
- end
225
- ```
226
-
227
207
  ## Clear
228
208
 
229
209
  While this method is not used by Shrine, it is good to give users the
data/doc/design.md CHANGED
@@ -188,7 +188,7 @@ Shrine::Attachment.new(:image).instance_methods #=> [:image=, :image, :image_url
188
188
 
189
189
  # equivalents
190
190
  Shrine::Attachment.new(:image)
191
- Shrine.attachment(:image)
191
+ Shrine::Attachment(:image)
192
192
  Shrine[:image]
193
193
  ```
194
194
 
data/doc/metadata.md CHANGED
@@ -21,9 +21,27 @@ The following metadata is extracted by default:
21
21
  | `mime_type` | extracted from `io.content_type` |
22
22
  | `size` | extracted from `io.size` |
23
23
 
24
+ You can access extracted metadata in three ways:
24
25
 
25
- Under the hood `Shrine#extract_metadata` is called, which you can also use
26
- directly to extract metadata from any IO object.
26
+ ```rb
27
+ # via methods (if they're defined)
28
+ uploaded_file.size
29
+ uploaded_file.original_filename
30
+ uploaded_file.mime_type
31
+
32
+ # via the metadata hash
33
+ uploaded_file.metadata["size"]
34
+ uploaded_file.metadata["filename"]
35
+ uploaded_file.metadata["mime_type"]
36
+
37
+ # via the #[] operator
38
+ uploaded_file["size"]
39
+ uploaded_file["filename"]
40
+ uploaded_file["mime_type"]
41
+ ```
42
+
43
+ Under the hood, `Shrine#upload` calls `Shrine#extract_metadata`, which you can
44
+ also use directly to extract metadata from any IO object:
27
45
 
28
46
  ```rb
29
47
  uploader.extract_metadata(io) #=>
@@ -34,51 +52,61 @@ uploader.extract_metadata(io) #=>
34
52
  # }
35
53
  ```
36
54
 
37
- By default these values are determined from the following attributes on the IO
38
- object:
55
+ `Shrine#upload` accepts a `:metadata` option which accepts the following values:
39
56
 
40
- * `filename` – `io.original_filename` or `io.path`
41
- * `mime_type` – `io.content_type`
42
- * `size` – `io.size`
57
+ * `Hash` – adds/overrides extracted metadata with the given hash
43
58
 
44
- Note that you can also manually add or override metadata on upload by passing
45
- the `:metadata` option to `Shrine#upload`:
59
+ ```rb
60
+ uploaded_file = uploader.upload(file, metadata: { "filename" => "Matrix[1999].mp4", "foo" => "bar" })
61
+ uploaded_file.original_filename #=> "Matrix[1999].mp4"
62
+ uploaded_file.metadata["foo"] #=> "bar"
63
+ ```
46
64
 
47
- ```rb
48
- uploaded_file = uploader.upload(file, metadata: { "filename" => "Matrix[1999].mp4", "foo" => "bar" })
49
- uploaded_file.original_filename #=> "Matrix[1999].mp4"
50
- uploaded_file.metadata["foo"] #=> "bar"
51
- ```
65
+ * `false` – skips metadata extraction (useful in tests)
66
+
67
+ ```rb
68
+ uploaded_file = uploader.upload(file, metadata: false)
69
+ uploaded_file.metadata #=> {}
70
+ ```
71
+
72
+ * `true` – forces metadata extraction when a `Shrine::UploadedFile` is being
73
+ uploaded (by default metadata is simply copied over)
74
+
75
+ ```rb
76
+ uploaded_file = uploader.upload(uploaded_file, metadata: true)
77
+ uploaded_file.metadata # re-extracted metadata
78
+ ```
52
79
 
53
80
  ## MIME type
54
81
 
55
82
  By default, the `mime_type` metadata will be copied over from the
56
- `#content_type` attribute of the input file, if present. However, since
83
+ `#content_type` attribute of the input file (if present). However, since
57
84
  `#content_type` value comes from the `Content-Type` header of the upload
58
85
  request, it's *not guaranteed* to hold the actual MIME type of the file (browser
59
86
  determines this header based on file extension). Moreover, only
60
- `ActionDispatch::Http::UploadedFile` and `Shrine::Plugins::RackFile::UploadedFile`
61
- objects have `#content_type` defined, so when uploading simple file objects
62
- `mime_type` will be nil. That makes relying on `#content_type` both a security
63
- risk and limiting.
87
+ `ActionDispatch::Http::UploadedFile`, `Shrine::Plugins::RackFile::UploadedFile`,
88
+ and `Shrine::Plugins::DataUri::DataFile` objects have `#content_type` defined,
89
+ so, when uploading simple file objects, `mime_type` will be nil. That makes
90
+ relying on `#content_type` both a security risk and limiting.
64
91
 
65
- To remedy that, Shrine comes with a `determine_mime_type` plugin which is able
66
- to extract the MIME type from IO *content*. When you load it, the `mime_type`
67
- plugin will now be determined using the UNIX [`file`] command.
92
+ To remedy that, Shrine comes with a
93
+ [`determine_mime_type`][determine_mime_type] plugin which is able to extract
94
+ the MIME type from IO *content*:
68
95
 
69
96
  ```rb
70
- Shrine.plugin :determine_mime_type
97
+ # Gemfile
98
+ gem "marcel", "~> 0.3"
99
+ ```
100
+ ```rb
101
+ Shrine.plugin :determine_mime_type, analyzer: :marcel
71
102
  ```
72
103
  ```rb
73
104
  uploaded_file = uploader.upload StringIO.new("<?php ... ?>")
74
- uploaded_file.mime_type #=> "text/x-php"
105
+ uploaded_file.mime_type #=> "application/x-php"
75
106
  ```
76
107
 
77
- The `file` command won't correctly determine the MIME type in all cases, that's
78
- why the `determine_mime_type` plugin comes with different MIME type analyzers.
79
- So, instead of the `file` command you can use gems like [MimeMagic] or
80
- [Marcel], as well as mix-and-match the analyzers to suit your needs. See the
81
- plugin documentation for more details.
108
+ You can choose different analyzers, and even mix-and-match them. See the
109
+ [`determine_mime_type`][determine_mime_type] plugin docs for more details.
82
110
 
83
111
  ## Image Dimensions
84
112
 
@@ -165,13 +193,10 @@ uploaded_file.metadata #=>
165
193
 
166
194
  The yielded `io` object will not always be an object that responds to `#path`.
167
195
  If you're using the `data_uri` plugin, the `io` will be a `StringIO` wrapper.
168
- When the `restore_cached_data` plugin is loaded, any assigned cached file will
169
- get their metadata extracted, and `io` will be a `Shrine::UploadedFile` object.
170
- If you're using a metadata analyzer that requires the source file to be on
171
- disk, you can use `Shrine.with_file` to ensure you have a file object.
172
-
173
- Also, be aware that metadata is extracted before file validation, so you'll
174
- need to handle the cases where the file is not of expected type.
196
+ With `restore_cached_data` or `refresh_metadata` plugins, `io` might be a
197
+ `Shrine::UploadedFile` object. If you're using a metadata analyzer that
198
+ requires the source file to be on disk, you can use `Shrine.with_file` to
199
+ ensure you have a file object.
175
200
 
176
201
  ## Metadata columns
177
202
 
@@ -319,3 +344,4 @@ end
319
344
  [MiniMagick]: https://github.com/minimagick/minimagick
320
345
  [ruby-vips]: https://github.com/libvips/ruby-vips
321
346
  [tus server]: https://github.com/janko/tus-ruby-server
347
+ [determine_mime_type]: /doc/plugins/determine_mime_type.md#readme
data/doc/paperclip.md CHANGED
@@ -182,16 +182,17 @@ but without the possibility of false negatives.
182
182
 
183
183
  In Paperclip you enable logging by setting `Paperclip.options[:log] = true`,
184
184
  however, this only logs ImageMagick commands. Shrine has full logging support,
185
- which measures processing, uploading and deleting individually, along with
186
- context for debugging:
185
+ which measures processing, uploading and deleting individually:
187
186
 
188
187
  ```rb
189
- Shrine.plugin :logging
188
+ Shrine.plugin :instrumentation
190
189
  ```
191
190
  ```
192
- 2015-10-09T20:06:06.676Z #25602: STORE[cache] ImageUploader[:avatar] User[29543] 1 file (0.1s)
193
- 2015-10-09T20:06:06.854Z #25602: PROCESS[store]: ImageUploader[:avatar] User[29543] 1-3 files (0.22s)
194
- 2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)
191
+ Metadata (32ms) {:storage=>:store, :io=>StringIO, :uploader=>Shrine}
192
+ Upload (1523ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :io=>StringIO, :upload_options=>{}, :uploader=>Shrine}
193
+ Exists (755ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :uploader=>Shrine}
194
+ Download (1002ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :download_options=>{}, :uploader=>Shrine}
195
+ Delete (700ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :uploader=>Shrine}
195
196
  ```
196
197
 
197
198
  ## Attachments
@@ -26,6 +26,8 @@ You can also use `#data_uri=` and `#data_uri` methods directly on the
26
26
  attacher.data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
27
27
  ```
28
28
 
29
+ ## Errors
30
+
29
31
  If the data URI wasn't correctly parsed, an error message will be added to the
30
32
  attachment column. You can change the default error message:
31
33
 
@@ -54,7 +56,9 @@ load the `infer_extension` plugin to infer it from the MIME type.
54
56
  plugin :infer_extension
55
57
  ```
56
58
 
57
- ## `Shrine.data_uri`
59
+ ## API
60
+
61
+ ### `Shrine.data_uri`
58
62
 
59
63
  If you just want to parse the data URI and create an IO object from it, you can
60
64
  do that with `Shrine.data_uri`. If the data URI cannot be parsed, a
@@ -86,10 +90,10 @@ io = Shrine.data_uri("data:,content", filename: "foo.txt")
86
90
  io.original_filename #=> "foo.txt"
87
91
  ```
88
92
 
89
- ## `UploadedFile#data_uri` and `UploadedFile#base64`
93
+ ### `UploadedFile#data_uri` and `UploadedFile#base64`
90
94
 
91
- This plugin also adds UploadedFile#data_uri method, which returns a
92
- base64-encoded data URI of the file content, and UploadedFile#base64, which
95
+ This plugin also adds `UploadedFile#data_uri` method, which returns a
96
+ base64-encoded data URI of the file content, and `UploadedFile#base64`, which
93
97
  simply returns the file content base64-encoded.
94
98
 
95
99
  ```rb
@@ -97,6 +101,48 @@ uploaded_file.data_uri #=> "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
97
101
  uploaded_file.base64 #=> "iVBORw0KGgoAAAANSUhEUgAAAAUA"
98
102
  ```
99
103
 
104
+ ## Instrumentation
105
+
106
+ If the `instrumentation` plugin has been loaded, the `data_uri` plugin adds
107
+ instrumentation around data URI parsing.
108
+
109
+ ```rb
110
+ # instrumentation plugin needs to be loaded *before* data_uri
111
+ plugin :instrumentation
112
+ plugin :data_uri
113
+ ```
114
+
115
+ Parsing data URIs will trigger a `data_uri.shrine` event with the following
116
+ payload:
117
+
118
+ | Key | Description |
119
+ | :-- | :---- |
120
+ | `:data_uri` | The data URI string |
121
+ | `:uploader` | The uploader class that sent the event |
122
+
123
+ A default log subscriber is added as well which logs these events:
124
+
125
+ ```
126
+ Data URI (5ms) – {:uploader=>Shrine}
127
+ ```
128
+
129
+ You can also use your own log subscriber:
130
+
131
+ ```rb
132
+ plugin :data_uri, log_subscriber: -> (event) {
133
+ Shrine.logger.info JSON.generate(name: event.name, duration: event.duration, uploader: event[:uploader])
134
+ }
135
+ ```
136
+ ```
137
+ {"name":"data_uri","duration":5,"uploader":"Shrine"}
138
+ ```
139
+
140
+ Or disable logging altogether:
141
+
142
+ ```rb
143
+ plugin :data_uri, log_subscriber: nil
144
+ ```
145
+
100
146
  [data_uri]: /lib/shrine/plugins/data_uri.rb
101
147
  [data URIs]: https://tools.ietf.org/html/rfc2397
102
148
  [HTML5 Canvas]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
@@ -5,6 +5,30 @@ dynamically processing uploaded files on request. This allows you to create
5
5
  URLs to files that might not have been generated yet, and have the endpoint
6
6
  process them on-the-fly.
7
7
 
8
+ ## Contents
9
+
10
+ * [Quick start](#quick-start)
11
+ * [How it works](#how-it-works)
12
+ - [Performance](#performance)
13
+ * [Derivation response](#derivation-response)
14
+ * [Dynamic settings](#dynamic-settings)
15
+ * [Host](#host)
16
+ * [Prefix](#prefix)
17
+ * [Expiration](#expiration)
18
+ * [Response headers](#response-headers)
19
+ - [Content Type](#content-type)
20
+ - [Content Disposition](#content-disposition)
21
+ - [Cache Control](#cache-control)
22
+ * [Uploading](#uploading)
23
+ - [Redirecting](#redirecting)
24
+ - [Deleting derivatives](#deleting-derivatives)
25
+ * [Cache busting](#cache-busting)
26
+ * [Accessing source file](#accessing-source-file)
27
+ * [Downloading](#downloading)
28
+ - [Skipping download](#skipping-download)
29
+ * [Derivation API](#derivation-api)
30
+ * [Plugin Options](#plugin-options)
31
+
8
32
  ## Quick start
9
33
 
10
34
  We first load the plugin, providing a secret key and a path prefix to where the
@@ -69,18 +69,60 @@ plugin :determine_mime_type, analyzer: -> (io, analyzers) do
69
69
  end
70
70
  ```
71
71
 
72
- ## Other Usage
72
+ ## API
73
73
 
74
74
  You can also use the methods for determining the MIME type directly:
75
75
 
76
76
  ```rb
77
77
  # or YourUploader.determine_mime_type(io)
78
- Shrine.determine_mime_type(io) # calls the defined analyzer
79
- #=> "image/jpeg"
78
+ Shrine.determine_mime_type(io) #=> "image/jpeg" (calls the defined analyzer)
79
+ # or just
80
+ Shrine.mime_type(io) #=> "image/jpeg" (calls the defined analyzer)
80
81
 
81
82
  # or YourUploader.mime_type_analyzers
82
- Shrine.mime_type_analyzers[:file].call(io) # calls a built-in analyzer
83
- #=> "image/jpeg"
83
+ Shrine.mime_type_analyzers[:file].call(io) #=> "image/jpeg" (calls a built-in analyzer)
84
+ ```
85
+
86
+ ## Instrumentation
87
+
88
+ If the `instrumentation` plugin has been loaded, the `determine_mime_type` plugin
89
+ adds instrumentation around MIME type analyzation.
90
+
91
+ ```rb
92
+ # instrumentation plugin needs to be loaded *before* determine_mime_type
93
+ plugin :instrumentation
94
+ plugin :determine_mime_type
95
+ ```
96
+
97
+ Analyzing MIME type will trigger a `mime_type.shrine` event with the following
98
+ payload:
99
+
100
+ | Key | Description |
101
+ | :-- | :---- |
102
+ | `:io` | The IO object |
103
+ | `:uploader` | The uploader class that sent the event |
104
+
105
+ A default log subscriber is added as well which logs these events:
106
+
107
+ ```
108
+ MIME Type (33ms) – {:io=>StringIO, :uploader=>Shrine}
109
+ ```
110
+
111
+ You can also use your own log subscriber:
112
+
113
+ ```rb
114
+ plugin :determine_mime_type, log_subscriber: -> (event) {
115
+ Shrine.logger.info JSON.generate(name: event.name, duration: event.duration, **event.payload)
116
+ }
117
+ ```
118
+ ```
119
+ {"name":"mime_type","duration":24,"io":"#<StringIO:0x00007fb7c5b08b80>","uploader":"Shrine"}
120
+ ```
121
+
122
+ Or disable logging altogether:
123
+
124
+ ```rb
125
+ plugin :determine_mime_type, log_subscriber: nil
84
126
  ```
85
127
 
86
128
  [determine_mime_type]: /lib/shrine/plugins/determine_mime_type.rb
@@ -9,15 +9,7 @@ extension might not be known.
9
9
  plugin :infer_extension
10
10
  ```
11
11
 
12
- Ordinarily, the upload location will gain the inferred extension only if it
13
- couldn't be determined from the filename. However, you can pass `force: true`
14
- to force the inferred extension to be used rather than an extension from the
15
- original filename. This can be used to canonicalize extensions (jpg, jpeg =>
16
- jpeg), or replace an incorrect original extension.
17
-
18
- ```rb
19
- plugin :infer_extension, force: true
20
- ```
12
+ ## Inferrers
21
13
 
22
14
  By default `MIME::Types` will be used for inferring the extension, but you can
23
15
  also choose a different inferrer:
@@ -43,6 +35,8 @@ plugin :infer_extension, inferrer: -> (mime_type, inferrers) do
43
35
  end
44
36
  ```
45
37
 
38
+ ## API
39
+
46
40
  You can also use methods for inferring extension directly:
47
41
 
48
42
  ```rb
@@ -53,6 +47,48 @@ Shrine.extension_inferrers[:mime_types].call("image/jpeg")
53
47
  # => ".jpeg"
54
48
  ```
55
49
 
50
+ ## Instrumentation
51
+
52
+ If the `instrumentation` plugin has been loaded, the `infer_extension` plugin
53
+ adds instrumentation around inferring extension.
54
+
55
+ ```rb
56
+ # instrumentation plugin needs to be loaded *before* infer_extension
57
+ plugin :instrumentation
58
+ plugin :infer_extension
59
+ ```
60
+
61
+ Inferring extension will trigger a `extension.shrine` event with the following
62
+ payload:
63
+
64
+ | Key | Description |
65
+ | :-- | :---- |
66
+ | `:mime_type` | MIME type to infer extension from |
67
+ | `:uploader` | The uploader class that sent the event |
68
+
69
+ A default log subscriber is added as well which logs these events:
70
+
71
+ ```
72
+ Extension (5ms) – {:mime_type=>"image/jpeg", :uploader=>Shrine}
73
+ ```
74
+
75
+ You can also use your own log subscriber:
76
+
77
+ ```rb
78
+ plugin :infer_extension, log_subscriber: -> (event) {
79
+ Shrine.logger.info JSON.generate(name: event.name, duration: event.duration, **event.payload)
80
+ }
81
+ ```
82
+ ```
83
+ {"name":"extension","duration":5,"mime_type":"image/jpeg","uploader":"Shrine"}
84
+ ```
85
+
86
+ Or disable logging altogether:
87
+
88
+ ```rb
89
+ plugin :infer_extension, log_subscriber: nil
90
+ ```
91
+
56
92
  [infer_extension]: /lib/shrine/plugins/infer_extension.rb
57
93
  [mime-types]: https://github.com/mime-types/ruby-mime-types
58
94
  [mini_mime]: https://github.com/discourse/mini_mime