shrine 3.0.0.beta2 → 3.0.0.beta3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -1
- data/README.md +100 -106
- data/doc/advantages.md +90 -88
- data/doc/attacher.md +322 -152
- data/doc/carrierwave.md +105 -113
- data/doc/changing_derivatives.md +308 -0
- data/doc/changing_location.md +92 -21
- data/doc/changing_storage.md +107 -0
- data/doc/creating_plugins.md +1 -1
- data/doc/design.md +8 -9
- data/doc/direct_s3.md +3 -2
- data/doc/metadata.md +97 -78
- data/doc/multiple_files.md +3 -3
- data/doc/paperclip.md +89 -88
- data/doc/plugins/activerecord.md +3 -12
- data/doc/plugins/backgrounding.md +126 -100
- data/doc/plugins/derivation_endpoint.md +4 -5
- data/doc/plugins/derivatives.md +63 -32
- data/doc/plugins/download_endpoint.md +54 -1
- data/doc/plugins/entity.md +1 -0
- data/doc/plugins/form_assign.md +53 -0
- data/doc/plugins/mirroring.md +37 -16
- data/doc/plugins/multi_cache.md +22 -0
- data/doc/plugins/presign_endpoint.md +1 -1
- data/doc/plugins/remote_url.md +19 -4
- data/doc/plugins/validation.md +83 -0
- data/doc/processing.md +149 -133
- data/doc/refile.md +68 -63
- data/doc/release_notes/3.0.0.md +835 -0
- data/doc/securing_uploads.md +56 -36
- data/doc/storage/s3.md +2 -2
- data/doc/testing.md +104 -120
- data/doc/upgrading_to_3.md +538 -0
- data/doc/validation.md +48 -87
- data/lib/shrine.rb +7 -4
- data/lib/shrine/attacher.rb +16 -6
- data/lib/shrine/plugins/activerecord.rb +33 -14
- data/lib/shrine/plugins/atomic_helpers.rb +1 -1
- data/lib/shrine/plugins/backgrounding.rb +23 -89
- data/lib/shrine/plugins/data_uri.rb +13 -2
- data/lib/shrine/plugins/derivation_endpoint.rb +7 -11
- data/lib/shrine/plugins/derivatives.rb +44 -20
- data/lib/shrine/plugins/download_endpoint.rb +26 -0
- data/lib/shrine/plugins/form_assign.rb +6 -3
- data/lib/shrine/plugins/keep_files.rb +2 -2
- data/lib/shrine/plugins/mirroring.rb +62 -22
- data/lib/shrine/plugins/model.rb +2 -2
- data/lib/shrine/plugins/multi_cache.rb +27 -0
- data/lib/shrine/plugins/remote_url.rb +25 -10
- data/lib/shrine/plugins/remove_invalid.rb +1 -1
- data/lib/shrine/plugins/sequel.rb +39 -20
- data/lib/shrine/plugins/validation.rb +3 -0
- data/lib/shrine/storage/s3.rb +16 -1
- data/lib/shrine/uploaded_file.rb +1 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +1 -1
- metadata +12 -7
- data/doc/migrating_storage.md +0 -76
- data/doc/regenerating_versions.md +0 -143
- data/lib/shrine/plugins/attacher_options.rb +0 -55
data/doc/securing_uploads.md
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
Shrine does a lot to make your file uploads secure, but there are still a lot
|
4
4
|
of security measures that could be added by the user on the application's side.
|
5
|
-
This guide will try to cover
|
6
|
-
|
5
|
+
This guide will try to cover some well-known security issues, ranging from the
|
6
|
+
obvious ones to not-so-obvious ones, and try to provide solutions.
|
7
7
|
|
8
8
|
## Validate file type
|
9
9
|
|
@@ -12,17 +12,21 @@ idea to create a whitelist (or a blacklist) of extensions and MIME types.
|
|
12
12
|
|
13
13
|
By default Shrine stores the MIME type derived from the extension, which means
|
14
14
|
it's not guaranteed to hold the actual MIME type of the the file. However, you
|
15
|
-
can load the `determine_mime_type` plugin
|
16
|
-
|
15
|
+
can load the `determine_mime_type` plugin to determine MIME type from magic
|
16
|
+
file headers.
|
17
17
|
|
18
|
+
```rb
|
19
|
+
# Gemfile
|
20
|
+
gem "marcel", "~> 0.3"
|
21
|
+
```
|
18
22
|
```rb
|
19
23
|
class MyUploader < Shrine
|
24
|
+
plugin :determine_mime_type, analyzer: :marcel
|
20
25
|
plugin :validation_helpers
|
21
|
-
plugin :determine_mime_type
|
22
26
|
|
23
27
|
Attacher.validate do
|
24
|
-
|
25
|
-
|
28
|
+
validate_extension %w[jpg jpeg png webp]
|
29
|
+
validate_mime_type %w[image/jpeg image/png image/webp]
|
26
30
|
end
|
27
31
|
end
|
28
32
|
```
|
@@ -31,22 +35,23 @@ end
|
|
31
35
|
|
32
36
|
It's a good idea to generally limit the filesize of uploaded files, so that
|
33
37
|
attackers cannot easily flood your storage. There are various layers at which
|
34
|
-
you can apply filesize limits, depending on how you're accepting uploads.
|
35
|
-
|
36
|
-
|
38
|
+
you can apply filesize limits, depending on how you're accepting uploads. For
|
39
|
+
starters you can add a filesize validation to prevent large files from being
|
40
|
+
uploaded to `:store`:
|
37
41
|
|
38
42
|
```rb
|
39
43
|
class MyUploader < Shrine
|
40
44
|
plugin :validation_helpers
|
41
45
|
|
42
46
|
Attacher.validate do
|
43
|
-
validate_max_size
|
47
|
+
validate_max_size 100*1024*1024 # 100 MB
|
44
48
|
end
|
45
49
|
end
|
46
50
|
```
|
47
51
|
|
48
|
-
In the following sections we talk about various strategies to prevent files
|
49
|
-
being uploaded to
|
52
|
+
In the following sections we talk about various strategies to prevent files
|
53
|
+
from being uploaded to Shrine's temporary storage and the system's temporary
|
54
|
+
directory.
|
50
55
|
|
51
56
|
### Limiting filesize in direct uploads
|
52
57
|
|
@@ -55,7 +60,7 @@ in the `:max_size` option to reject files that are larger than the specified
|
|
55
60
|
limit:
|
56
61
|
|
57
62
|
```rb
|
58
|
-
plugin :upload_endpoint, max_size:
|
63
|
+
plugin :upload_endpoint, max_size: 100*1024*1024 # 20 MB
|
59
64
|
```
|
60
65
|
|
61
66
|
If you're doing direct uploads to Amazon S3 using the `presign_endpoint`
|
@@ -63,7 +68,7 @@ plugin, you can pass in the `:content_length_range` presign option:
|
|
63
68
|
|
64
69
|
```rb
|
65
70
|
plugin :presign_endpoint, presign_options: -> (request) do
|
66
|
-
{ content_length_range: 0..
|
71
|
+
{ content_length_range: 0..100*1024*1024 }
|
67
72
|
end
|
68
73
|
```
|
69
74
|
|
@@ -92,20 +97,17 @@ loading the `remove_invalid` plugin.
|
|
92
97
|
plugin :remove_invalid
|
93
98
|
```
|
94
99
|
|
95
|
-
###
|
100
|
+
### Failsafe filesize limiting
|
96
101
|
|
97
|
-
If you want to make sure that no large files ever get to your storages, and
|
98
|
-
|
99
|
-
and raise an error:
|
102
|
+
If you want to make sure that no large files ever get to your storages, and you
|
103
|
+
don't really care about the error message, you can override `Shrine#upload`:
|
100
104
|
|
101
105
|
```rb
|
102
|
-
class MyUploader
|
103
|
-
|
106
|
+
class MyUploader < Shrine
|
107
|
+
def upload(io, **options)
|
108
|
+
fail FileTooLarge if io.size >= 100*1024*1024
|
104
109
|
|
105
|
-
|
106
|
-
if io.respond_to?(:read)
|
107
|
-
raise FileTooLarge if io.size >= 20*1024*1024
|
108
|
-
end
|
110
|
+
super
|
109
111
|
end
|
110
112
|
end
|
111
113
|
```
|
@@ -118,24 +120,43 @@ image processing, since processing them can take a lot of time and memory. This
|
|
118
120
|
makes it trivial to DoS the application which doesn't have any protection
|
119
121
|
against them.
|
120
122
|
|
121
|
-
|
122
|
-
|
123
|
-
you still need to prevent those files from being attached and processed:
|
123
|
+
So, in addition to validating filesize, we should also validate image
|
124
|
+
dimensions:
|
124
125
|
|
125
126
|
```rb
|
126
|
-
|
127
|
+
# Gemfile
|
128
|
+
gem "fastimage"
|
129
|
+
```
|
130
|
+
```rb
|
131
|
+
class ImageUploader < Shrine
|
127
132
|
plugin :store_dimensions
|
128
133
|
plugin :validation_helpers
|
129
134
|
|
130
135
|
Attacher.validate do
|
131
|
-
|
132
|
-
|
136
|
+
validate_max_size 100*1024*1024
|
137
|
+
|
138
|
+
if validate_mime_type %w[image/jpeg image/png image/webp]
|
139
|
+
validate_max_dimensions [5000, 5000]
|
140
|
+
end
|
133
141
|
end
|
134
142
|
end
|
135
143
|
```
|
136
144
|
|
137
|
-
If you
|
138
|
-
|
145
|
+
If you want to be extra safe, you can add a failsafe before performing
|
146
|
+
processing:
|
147
|
+
|
148
|
+
```rb
|
149
|
+
class ImageUploader < Shrine
|
150
|
+
# ...
|
151
|
+
Attacher.derivatives_processor do |original|
|
152
|
+
width, height = Shrine.dimensions(original)
|
153
|
+
|
154
|
+
fail ImageBombError if width > 5000 || height > 5000
|
155
|
+
|
156
|
+
# ...
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
139
160
|
|
140
161
|
## Prevent metadata tampering
|
141
162
|
|
@@ -153,7 +174,7 @@ app. To guard yourself from such attacks, you can load the
|
|
153
174
|
cached files on assignment and override the received metadata.
|
154
175
|
|
155
176
|
```rb
|
156
|
-
plugin :restore_cached_data
|
177
|
+
Shrine.plugin :restore_cached_data
|
157
178
|
```
|
158
179
|
|
159
180
|
## Limit number of files
|
@@ -172,6 +193,7 @@ class MyUploader < Shrine
|
|
172
193
|
|
173
194
|
Attacher.validate do
|
174
195
|
validate_min_size 10*1024 # 10 KB
|
196
|
+
# ...
|
175
197
|
end
|
176
198
|
end
|
177
199
|
```
|
@@ -183,6 +205,4 @@ end
|
|
183
205
|
* [AppSec: 8 Basic Rules to Implement Secure File Uploads](https://software-security.sans.org/blog/2009/12/28/8-basic-rules-to-implement-secure-file-uploads/)
|
184
206
|
|
185
207
|
[image bombs]: https://www.bamsoftware.com/hacks/deflate.html
|
186
|
-
[fastimage]: https://github.com/sdsykes/fastimage
|
187
|
-
[file]: http://linux.die.net/man/1/file
|
188
208
|
[rack-attack]: https://github.com/kickstarter/rack-attack
|
data/doc/storage/s3.md
CHANGED
@@ -79,8 +79,8 @@ plugin
|
|
79
79
|
|
80
80
|
```rb
|
81
81
|
class MyUploader < Shrine
|
82
|
-
plugin :upload_options, store: -> (io,
|
83
|
-
if
|
82
|
+
plugin :upload_options, store: -> (io, derivative: nil, **) do
|
83
|
+
if derivative == :thumb
|
84
84
|
{ acl: "public-read" }
|
85
85
|
else
|
86
86
|
{ acl: "private" }
|
data/doc/testing.md
CHANGED
@@ -6,9 +6,9 @@ attachments implemented with Shrine in your application.
|
|
6
6
|
## Callbacks
|
7
7
|
|
8
8
|
When you first try to test file attachments, you might experience that files
|
9
|
-
are
|
10
|
-
|
11
|
-
|
9
|
+
are not being promoted to permanent storage. This is because your tests are
|
10
|
+
likely setup to be wrapped inside database transactions, and that doesn't work
|
11
|
+
with Shrine callbacks.
|
12
12
|
|
13
13
|
Specifically, Shrine uses "after commit" callbacks for promoting and deleting
|
14
14
|
attached files. This means that if your tests are wrapped inside transactions,
|
@@ -18,7 +18,7 @@ happens only after the test has already finished.
|
|
18
18
|
```rb
|
19
19
|
# Promoting will happen only after the test transaction commits
|
20
20
|
it "can attach images" do
|
21
|
-
photo = Photo.create(image:
|
21
|
+
photo = Photo.create(image: file)
|
22
22
|
photo.image.storage_key #=> :cache (we expected it to be promoted to permanent storage)
|
23
23
|
end
|
24
24
|
```
|
@@ -37,15 +37,10 @@ end
|
|
37
37
|
## Storage
|
38
38
|
|
39
39
|
If you're using FileSystem storage and your tests run in a single process,
|
40
|
-
you can switch to
|
41
|
-
|
40
|
+
you can switch to `Shrine::Storage::Memory`, which is both faster and doesn't
|
41
|
+
require you to clean up anything between tests.
|
42
42
|
|
43
43
|
```rb
|
44
|
-
# Gemfile
|
45
|
-
gem "shrine-memory"
|
46
|
-
```
|
47
|
-
```rb
|
48
|
-
# test/test_helper.rb
|
49
44
|
require "shrine/storage/memory"
|
50
45
|
|
51
46
|
Shrine.storages = {
|
@@ -100,41 +95,88 @@ subdomains when generating URLs.
|
|
100
95
|
|
101
96
|
## Test data
|
102
97
|
|
103
|
-
|
104
|
-
|
98
|
+
We want to keep our tests fast, so when we're setting up files for tests, we
|
99
|
+
want to avoid expensive operations such as file processing and metadata
|
100
|
+
extraction.
|
101
|
+
|
102
|
+
We can start by creating a method which would generate fake attachment data:
|
105
103
|
|
106
104
|
```rb
|
107
|
-
|
108
|
-
|
109
|
-
end
|
110
|
-
```
|
105
|
+
module TestData
|
106
|
+
module_function
|
111
107
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
allow you to specify Shrine attributes - you can only assign to columns
|
116
|
-
directly.
|
108
|
+
def image_data
|
109
|
+
attacher = Shrine::Attacher.new
|
110
|
+
attacher.set(uploaded_image)
|
117
111
|
|
118
|
-
|
112
|
+
# if you're processing derivatives
|
113
|
+
attacher.set_derivatives(
|
114
|
+
large: uploaded_image,
|
115
|
+
medium: uploaded_image,
|
116
|
+
small: uploaded_image,
|
117
|
+
)
|
119
118
|
|
120
|
-
|
121
|
-
|
122
|
-
support this, examples:
|
119
|
+
attacher.column_data
|
120
|
+
end
|
123
121
|
|
122
|
+
def uploaded_image
|
123
|
+
file = File.open("test/files/image.jpg", binmode: true)
|
124
|
+
|
125
|
+
# for performance we skip metadata extraction and assign test metadata
|
126
|
+
uploaded_file = Shrine.upload(file, :store, metadata: false)
|
127
|
+
uploaded_file.metadata.merge!(
|
128
|
+
"size" => file.size,
|
129
|
+
"mime_type" => "image/jpeg",
|
130
|
+
"filename" => "test.jpg",
|
131
|
+
)
|
132
|
+
|
133
|
+
uploaded_file
|
134
|
+
end
|
135
|
+
end
|
136
|
+
```
|
124
137
|
```rb
|
125
|
-
|
126
|
-
require "sidekiq/testing"
|
127
|
-
Sidekiq::Testing.inline!
|
138
|
+
TestData.image_data #=> '{"id":"...","storage":"...","metadata":{...},"derivatives":{...}}'
|
128
139
|
```
|
129
140
|
|
141
|
+
With [factory_bot] you can then assign the test attachment data like this:
|
142
|
+
|
130
143
|
```rb
|
131
|
-
|
132
|
-
|
144
|
+
factory :photo do
|
145
|
+
image_data { TestData.image_data }
|
146
|
+
end
|
133
147
|
```
|
134
148
|
|
149
|
+
With [Rails' YAML fixtures][fixtures] it would look like this:
|
150
|
+
|
151
|
+
```erb
|
152
|
+
photo:
|
153
|
+
image_data: <%= TestData.image_data %>
|
154
|
+
```
|
155
|
+
|
156
|
+
## Unit tests
|
157
|
+
|
158
|
+
For testing attachment in your unit tests, you can assign plain `File` objects:
|
159
|
+
|
135
160
|
```rb
|
136
|
-
|
137
|
-
|
161
|
+
RSpec.describe ImageUploader do
|
162
|
+
let(:image) { photo.image }
|
163
|
+
let(:derivatives) { photo.image_derivatives }
|
164
|
+
let(:photo) { Photo.create(image: File.open("test/files/image.png", "rb")) }
|
165
|
+
|
166
|
+
it "extracts metadata" do
|
167
|
+
expect(image.mime_type).to eq("image/png")
|
168
|
+
expect(image.extension).to eq("png")
|
169
|
+
expect(image.size).to be_instance_of(Integer)
|
170
|
+
expect(image.width).to be_instance_of(Integer)
|
171
|
+
expect(image.height).to be_instance_of(Integer)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "generates derivatives" do
|
175
|
+
expect(derivatives[:small]).to be_kind_of(Shrine::UploadedFile)
|
176
|
+
expect(derivatives[:medium]).to be_kind_of(Shrine::UploadedFile)
|
177
|
+
expect(derivatives[:large]).to be_kind_of(Shrine::UploadedFile)
|
178
|
+
end
|
179
|
+
end
|
138
180
|
```
|
139
181
|
|
140
182
|
## Acceptance tests
|
@@ -170,119 +212,61 @@ With [Rack::TestApp] you can create multipart file upload requests by using the
|
|
170
212
|
http.post "/photos", multipart: {"photo[image]" => File.open("test/files/image.jpg")}
|
171
213
|
```
|
172
214
|
|
173
|
-
##
|
174
|
-
|
175
|
-
Even though all the file attachment logic is usually encapsulated in your
|
176
|
-
uploader classes, in general it's still best to test this logic through models.
|
215
|
+
## Background jobs
|
177
216
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
objects.
|
217
|
+
If you're using background jobs with Shrine, you probably want to make them
|
218
|
+
synchronous in tests. See your backgrounding library docs for how to make jobs
|
219
|
+
synchronous.
|
182
220
|
|
183
221
|
```rb
|
184
|
-
|
185
|
-
|
186
|
-
photo = Photo.create(image: File.open("test/files/image.png"))
|
187
|
-
assert_equal [:small, :medium, :large], photo.image.keys
|
188
|
-
end
|
189
|
-
end
|
222
|
+
# ActiveJob
|
223
|
+
ActiveJob::Base.queue_adapter = :inline
|
190
224
|
```
|
191
|
-
|
192
|
-
If you want test with an IO object that closely resembles the kind of IO that
|
193
|
-
is assigned by your web framework, you can use this:
|
194
|
-
|
195
225
|
```rb
|
196
|
-
|
197
|
-
require "
|
198
|
-
|
199
|
-
class FakeIO
|
200
|
-
attr_reader :original_filename, :content_type
|
201
|
-
|
202
|
-
def initialize(content, filename: nil, content_type: nil)
|
203
|
-
@io = StringIO.new(content)
|
204
|
-
@original_filename = filename
|
205
|
-
@content_type = content_type
|
206
|
-
end
|
207
|
-
|
208
|
-
extend Forwardable
|
209
|
-
delegate %i[read rewind eof? close size] => :@io
|
210
|
-
end
|
226
|
+
# Sidekiq
|
227
|
+
require "sidekiq/testing"
|
228
|
+
Sidekiq::Testing.inline!
|
211
229
|
```
|
212
|
-
|
213
230
|
```rb
|
214
|
-
|
215
|
-
|
216
|
-
photo = Photo.create(image: FakeIO.new(File.read("test/files/image.png")))
|
217
|
-
assert_equal [:small, :medium, :large], photo.image.keys
|
218
|
-
end
|
219
|
-
end
|
231
|
+
# SuckerPunch
|
232
|
+
require "sucker_punch/testing/inline"
|
220
233
|
```
|
221
234
|
|
222
235
|
## Processing
|
223
236
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
If you're processing only single files, you can override the `Shrine#process`
|
228
|
-
method in tests to return nil:
|
237
|
+
If you're testing your attachment flow which includes processing [derivatives],
|
238
|
+
you might want to disable the processing for certain tests. You can do this by
|
239
|
+
temporarily overriding the processor:
|
229
240
|
|
230
241
|
```rb
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
```rb
|
242
|
-
class ImageUploader
|
243
|
-
def process(io, context)
|
244
|
-
if context[:action] == :store
|
245
|
-
{small: io.download, medium: io.download, large: io.download}
|
242
|
+
module TestMode
|
243
|
+
module_function
|
244
|
+
|
245
|
+
def disable_processing(attacher, processor_name = :default)
|
246
|
+
attacher.class.instance_exec do
|
247
|
+
original_processor = derivatives_processor
|
248
|
+
derivatives_processor(processor_name) { Hash.new }
|
249
|
+
yield
|
250
|
+
derivatives_processor(processor_name, &original_processor)
|
246
251
|
end
|
247
252
|
end
|
248
253
|
end
|
249
254
|
```
|
250
|
-
|
251
|
-
However, it's even better to design your processing code in such a way that
|
252
|
-
it's easier to swap out in tests. In your *application* code you could extract
|
253
|
-
processing into a single `#call`-able object, and register it inside uploader
|
254
|
-
generic `opts` hash.
|
255
|
-
|
256
|
-
```rb
|
257
|
-
class ImageUploader < Shrine
|
258
|
-
opts[:processor] = ImageThumbnailsGenerator
|
259
|
-
|
260
|
-
process(:store) do |io, context|
|
261
|
-
opts[:processor].call(io, context)
|
262
|
-
end
|
263
|
-
end
|
264
|
-
```
|
265
|
-
|
266
|
-
Now in your tests you can easily swap out `ImageThumbnailsGenerator` with
|
267
|
-
"fake" processing, which just returns the result in correct format (single file
|
268
|
-
or hash of versions). Since the only requirement of the processor is that it
|
269
|
-
responds to `#call`, we can just swap it out for a proc or a lambda:
|
270
|
-
|
271
255
|
```rb
|
272
|
-
|
273
|
-
|
256
|
+
TestMode.disable_processing(Photo.image_attacher) do
|
257
|
+
photo = Photo.new
|
258
|
+
photo.file = File.open("test/files/image.png", "rb")
|
259
|
+
photo.save
|
274
260
|
end
|
275
261
|
```
|
276
262
|
|
277
|
-
This also has the benefit of allowing you to test `ImageThumbnailsGenerator` in
|
278
|
-
isolation.
|
279
|
-
|
280
263
|
[DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
|
281
|
-
[shrine-memory]: https://github.com/shrinerb/shrine-memory
|
282
264
|
[factory_bot]: https://github.com/thoughtbot/factory_bot
|
265
|
+
[fixtures]: https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures
|
283
266
|
[Capybara]: https://github.com/jnicklas/capybara
|
284
267
|
[`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
|
285
268
|
[Rack::Test]: https://github.com/brynary/rack-test
|
286
269
|
[Rack::TestApp]: https://github.com/kwatch/rack-test_app
|
287
270
|
[aws-sdk-ruby stubs]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html
|
288
271
|
[MinIO]: https://min.io/
|
272
|
+
[derivatives]: /doc/plugins/derivatives.md#readme
|