uppy-s3_multipart 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +93 -42
- data/lib/shrine/plugins/uppy_s3_multipart.rb +6 -6
- data/lib/uppy/s3_multipart/app.rb +30 -16
- data/lib/uppy/s3_multipart/client.rb +9 -1
- data/uppy-s3_multipart.gemspec +2 -2
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e799b2dfe2ad03929e1e6477382b85c4d721313708abe699673c2c3e496e98e8
|
4
|
+
data.tar.gz: 05d354cff6f1f5a9ae807faffdd91b29d5f16c62fba9abbec82038b7713a1a3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 336d352eeeb31ac132c074ddb4febd73d75f47b5fe41ed69caafb90eecf1d43494f37542d06a1df2859210fe9907207456695c0a3c965131384564f93b0abaa9
|
7
|
+
data.tar.gz: 2ae94a045d3b19e1eabc99dce49ffcaadc1d806caf77cbc87b6edf5b771e1509d58c56a3510f5e1a44b15adfdbe5d93f19fc6acf5322d66bb22791aa5d80d043
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ uploads.
|
|
10
10
|
Add the gem to your Gemfile:
|
11
11
|
|
12
12
|
```rb
|
13
|
-
gem "uppy-s3_multipart"
|
13
|
+
gem "uppy-s3_multipart", "~> 0.2"
|
14
14
|
```
|
15
15
|
|
16
16
|
## Setup
|
@@ -56,14 +56,14 @@ directly.
|
|
56
56
|
|
57
57
|
### Shrine
|
58
58
|
|
59
|
-
In
|
59
|
+
In your Shrine initializer load the `uppy_s3_multipart` plugin:
|
60
60
|
|
61
61
|
```rb
|
62
62
|
require "shrine"
|
63
63
|
require "shrine/storage/s3"
|
64
64
|
|
65
65
|
Shrine.storages = {
|
66
|
-
cache: Shrine::Storage::S3.new(...),
|
66
|
+
cache: Shrine::Storage::S3.new(prefix: "cache", ...),
|
67
67
|
store: Shrine::Storage::S3.new(...),
|
68
68
|
}
|
69
69
|
|
@@ -78,11 +78,11 @@ inside your main application:
|
|
78
78
|
```rb
|
79
79
|
# Rails (config/routes.rb)
|
80
80
|
Rails.application.routes.draw do
|
81
|
-
mount Shrine.uppy_s3_multipart(:cache) => "/s3"
|
81
|
+
mount Shrine.uppy_s3_multipart(:cache) => "/s3/multipart"
|
82
82
|
end
|
83
83
|
|
84
84
|
# Rack (config.ru)
|
85
|
-
map "/s3" do
|
85
|
+
map "/s3/multipart" do
|
86
86
|
run Shrine.uppy_s3_multipart(:cache)
|
87
87
|
end
|
88
88
|
```
|
@@ -97,14 +97,20 @@ POST /s3/multipart/:uploadId/complete
|
|
97
97
|
DELETE /s3/multipart/:uploadId
|
98
98
|
```
|
99
99
|
|
100
|
-
|
100
|
+
Now in your Uppy configuration point `serverUrl` to your app's URL:
|
101
101
|
|
102
102
|
```js
|
103
103
|
// ...
|
104
104
|
uppy.use(Uppy.AwsS3Multipart, {
|
105
105
|
serverUrl: '/',
|
106
106
|
})
|
107
|
+
```
|
108
|
+
|
109
|
+
In the `upload-success` Uppy callback you can then construct the Shrine
|
110
|
+
uploaded file data (this example assumes your temporary Shrine S3 storage has
|
111
|
+
`prefix: "cache"` set):
|
107
112
|
|
113
|
+
```js
|
108
114
|
uppy.on('upload-success', function (file, data, uploadURL) {
|
109
115
|
var uploadedFileData = JSON.stringify({
|
110
116
|
id: uploadURL.match(/\/cache\/([^\?]+)/)[1], // extract key without prefix
|
@@ -123,8 +129,22 @@ uppy.on('upload-success', function (file, data, uploadURL) {
|
|
123
129
|
Shrine. From there you can swap the `presign_endpoint` + `AwsS3` code with the
|
124
130
|
`uppy_s3_multipart` + `AwsS3Multipart` setup.**
|
125
131
|
|
126
|
-
|
127
|
-
|
132
|
+
Note that by default **Shrine won't extract metadata from directly upload
|
133
|
+
files**, instead it will just copy metadata that was extracted on the client
|
134
|
+
side. See [this section][metadata direct uploads] for the rationale and
|
135
|
+
instructions on how to opt in.
|
136
|
+
|
137
|
+
If you want to make uploads public and have public URLs without query
|
138
|
+
parameters returned, you can pass `public: true` to the Shrine storage (note
|
139
|
+
that this is supported starting from Shrine 2.13).
|
140
|
+
|
141
|
+
```rb
|
142
|
+
Shrine::Storage::S3.new(public: true, **options)
|
143
|
+
```
|
144
|
+
|
145
|
+
Both the plugin and method accept `:options` for specifying additional options
|
146
|
+
to the S3 client operations (see the [Client](#client) section for list of
|
147
|
+
operations and options they accept):
|
128
148
|
|
129
149
|
```rb
|
130
150
|
Shrine.plugin :uppy_s3_multipart, options: {
|
@@ -138,14 +158,11 @@ Shrine.uppy_s3_multipart(:cache, options: {
|
|
138
158
|
})
|
139
159
|
```
|
140
160
|
|
141
|
-
|
142
|
-
files**, instead it will just copy metadata that was extracted on the client
|
143
|
-
side. See [this section][metadata direct uploads] for the rationale and
|
144
|
-
instructions on how to opt in.
|
161
|
+
In the dynamic version the yielded object is an instance of [`Rack::Request`].
|
145
162
|
|
146
|
-
###
|
163
|
+
### App
|
147
164
|
|
148
|
-
You can also use `uppy-s3_multipart` without Shrine
|
165
|
+
You can also use `uppy-s3_multipart` without Shrine, by initializing the
|
149
166
|
`Uppy::S3Multipart::App` directly:
|
150
167
|
|
151
168
|
```rb
|
@@ -162,16 +179,16 @@ bucket = resource.bucket("my-bucket")
|
|
162
179
|
UPPY_S3_MULTIPART_APP = Uppy::S3Multipart::App.new(bucket: bucket)
|
163
180
|
```
|
164
181
|
|
165
|
-
|
182
|
+
You can mount it inside your main app in the same way:
|
166
183
|
|
167
184
|
```rb
|
168
185
|
# Rails (config/routes.rb)
|
169
186
|
Rails.application.routes.draw do
|
170
|
-
mount UPPY_S3_MULTIPART_APP => "/s3"
|
187
|
+
mount UPPY_S3_MULTIPART_APP => "/s3/multipart"
|
171
188
|
end
|
172
189
|
|
173
190
|
# Rack (config.ru)
|
174
|
-
map "/s3" do
|
191
|
+
map "/s3/multipart" do
|
175
192
|
run UPPY_S3_MULTIPART_APP
|
176
193
|
end
|
177
194
|
```
|
@@ -185,25 +202,34 @@ uppy.use(Uppy.AwsS3Multipart, {
|
|
185
202
|
})
|
186
203
|
```
|
187
204
|
|
188
|
-
|
189
|
-
|
190
|
-
|
205
|
+
If you want to make uploads public and have public URLs without query
|
206
|
+
parameters returned, you can pass `public: true` to the app.
|
207
|
+
|
208
|
+
```rb
|
209
|
+
Uppy::S3Multipart::App.new(bucket: bucket, public: true)
|
210
|
+
```
|
211
|
+
|
212
|
+
You can also pass `:options` for specifying additional options to the S3 client
|
213
|
+
operations (see the [Client](#client) section for list of operations and
|
214
|
+
options they accept):
|
191
215
|
|
192
216
|
```rb
|
193
217
|
Uppy::S3Multipart::App.new(bucket: bucket, options: {
|
194
|
-
create_multipart_upload: { acl: "public-read" }
|
218
|
+
create_multipart_upload: { acl: "public-read" } # static
|
195
219
|
})
|
196
220
|
|
197
221
|
# OR
|
198
222
|
|
199
223
|
Uppy::S3Multipart::App.new(bucket: bucket, options: {
|
200
|
-
create_multipart_upload: -> (request) { { acl: "public-read" } }
|
224
|
+
create_multipart_upload: -> (request) { { acl: "public-read" } } # dynamic
|
201
225
|
})
|
202
226
|
```
|
203
227
|
|
204
|
-
|
228
|
+
In the dynamic version the yielded object is an instance of [`Rack::Request`].
|
229
|
+
|
230
|
+
### Client
|
205
231
|
|
206
|
-
If you would rather implement the endpoints yourself, you can utilize
|
232
|
+
If you would rather implement the endpoints yourself, you can utilize the
|
207
233
|
`Uppy::S3Multipart::Client` to make S3 requests.
|
208
234
|
|
209
235
|
```rb
|
@@ -223,13 +249,13 @@ client.create_multipart_upload(key: "foo", **options)
|
|
223
249
|
|
224
250
|
Accepts:
|
225
251
|
|
226
|
-
* `:key`
|
252
|
+
* `:key` – object key
|
227
253
|
* additional options for [`Aws::S3::Client#create_multipart_upload`]
|
228
254
|
|
229
255
|
Returns:
|
230
256
|
|
231
|
-
* `:upload_id`
|
232
|
-
* `:key`
|
257
|
+
* `:upload_id` – id of the created multipart upload
|
258
|
+
* `:key` – object key
|
233
259
|
|
234
260
|
#### `#list_parts`
|
235
261
|
|
@@ -244,17 +270,17 @@ client.list_parts(upload_id: "MultipartUploadId", key: "foo", **options)
|
|
244
270
|
|
245
271
|
Accepts:
|
246
272
|
|
247
|
-
* `:upload_id`
|
248
|
-
* `:key`
|
273
|
+
* `:upload_id` – multipart upload id
|
274
|
+
* `:key` – object key
|
249
275
|
* additional options for [`Aws::S3::Client#list_parts`]
|
250
276
|
|
251
277
|
Returns:
|
252
278
|
|
253
279
|
* array of parts
|
254
280
|
|
255
|
-
- `:part_number`
|
256
|
-
- `:size`
|
257
|
-
- `:etag`
|
281
|
+
- `:part_number` – position of the part
|
282
|
+
- `:size` – filesize of the part
|
283
|
+
- `:etag` – etag of the part
|
258
284
|
|
259
285
|
#### `#prepare_upload_part`
|
260
286
|
|
@@ -267,14 +293,14 @@ client.prepare_upload_part(upload_id: "MultipartUploadId", key: "foo", part_numb
|
|
267
293
|
|
268
294
|
Accepts:
|
269
295
|
|
270
|
-
* `:upload_id`
|
271
|
-
* `:key`
|
272
|
-
* `:part_number`
|
296
|
+
* `:upload_id` – multipart upload id
|
297
|
+
* `:key` – object key
|
298
|
+
* `:part_number` – number of the next part
|
273
299
|
* additional options for [`Aws::S3::Client#upload_part`] and [`Aws::S3::Presigner#presigned_url`]
|
274
300
|
|
275
301
|
Returns:
|
276
302
|
|
277
|
-
* `:url`
|
303
|
+
* `:url` – endpoint that should be used for uploading a new multipart part via a `PUT` request
|
278
304
|
|
279
305
|
#### `#complete_multipart_upload`
|
280
306
|
|
@@ -287,14 +313,36 @@ client.complete_multipart_upload(upload_id: upload_id, key: key, parts: [{ part_
|
|
287
313
|
|
288
314
|
Accepts:
|
289
315
|
|
290
|
-
* `:upload_id`
|
291
|
-
* `:key`
|
292
|
-
* `:parts`
|
316
|
+
* `:upload_id` – multipart upload id
|
317
|
+
* `:key` – object key
|
318
|
+
* `:parts` – list of all uploaded parts, consisting of `:part_number` and `:etag`
|
293
319
|
* additional options for [`Aws::S3::Client#complete_multipart_upload`]
|
294
320
|
|
295
321
|
Returns:
|
296
322
|
|
297
|
-
* `:location`
|
323
|
+
* `:location` – URL to the uploaded object
|
324
|
+
|
325
|
+
#### `#object_url`
|
326
|
+
|
327
|
+
Generates URL to the object.
|
328
|
+
|
329
|
+
```rb
|
330
|
+
client.object_url(key: key, **options)
|
331
|
+
# => "https://my-bucket.s3.amazonaws.com/foo?..."
|
332
|
+
```
|
333
|
+
|
334
|
+
This is called after `#complete_multipart_upload` in the app and returned in
|
335
|
+
the response.
|
336
|
+
|
337
|
+
Accepts:
|
338
|
+
|
339
|
+
* `:key` – object key
|
340
|
+
* `:public` – for generating a public URL (default is presigned expiring URL)
|
341
|
+
* additional options for [`Aws::S3::Object#presigned_url`] and [`Aws::S3::Client#get_object`]
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
|
345
|
+
* URL to the object
|
298
346
|
|
299
347
|
#### `#abort_multipart_upload`
|
300
348
|
|
@@ -307,8 +355,8 @@ client.abort_multipart_upload(upload_id: upload_id, key: key, **options)
|
|
307
355
|
|
308
356
|
Accepts:
|
309
357
|
|
310
|
-
* `:upload_id`
|
311
|
-
* `:key`
|
358
|
+
* `:upload_id` – multipart upload id
|
359
|
+
* `:key` – object key
|
312
360
|
* additional options for [`Aws::S3::Client#abort_multipart_upload`]
|
313
361
|
|
314
362
|
## Contributing
|
@@ -331,10 +379,13 @@ License](https://opensource.org/licenses/MIT).
|
|
331
379
|
[AwsS3Multipart]: https://uppy.io/docs/aws-s3-multipart/
|
332
380
|
[Shrine]: https://shrinerb.com
|
333
381
|
[Adding Direct S3 Uploads]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads
|
382
|
+
[`Rack::Request`]: https://www.rubydoc.info/github/rack/rack/master/Rack/Request
|
334
383
|
[`Aws::S3::Client#create_multipart_upload`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#create_multipart_upload-instance_method
|
335
384
|
[`Aws::S3::Client#list_parts`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#list_parts-instance_method
|
336
385
|
[`Aws::S3::Client#upload_part`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#upload_part-instance_method
|
337
386
|
[`Aws::S3::Presigner#presigned_url`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Presigner.html#presigned_url-instance_method
|
338
387
|
[`Aws::S3::Client#complete_multipart_upload`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#complete_multipart_upload-instance_method
|
388
|
+
[`Aws::S3::Object#presigned_url`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_url-instance_method
|
389
|
+
[`Aws::S3::Client#get_object`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#get_object-instance_method
|
339
390
|
[`Aws::S3::Client#abort_multipart_upload`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#abort_multipart_upload-instance_method
|
340
391
|
[metadata direct uploads]: https://github.com/shrinerb/shrine/blob/master/doc/metadata.md#direct-uploads
|
@@ -15,12 +15,12 @@ class Shrine
|
|
15
15
|
fail Error, "expected storage to be a Shrine::Storage::S3, but was #{s3.inspect}"
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
)
|
18
|
+
options[:bucket] ||= s3.bucket
|
19
|
+
options[:prefix] ||= s3.prefix
|
20
|
+
options[:public] ||= s3.public if s3.respond_to?(:public)
|
21
|
+
options[:options] ||= opts[:uppy_s3_multipart_options]
|
22
|
+
|
23
|
+
::Uppy::S3Multipart::App.new(**options)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
@@ -8,10 +8,11 @@ require "cgi"
|
|
8
8
|
module Uppy
|
9
9
|
module S3Multipart
|
10
10
|
class App
|
11
|
-
def initialize(bucket:, prefix: nil, options: {})
|
11
|
+
def initialize(bucket:, prefix: nil, public: nil, options: {})
|
12
12
|
@router = Class.new(Router)
|
13
13
|
@router.opts[:client] = Client.new(bucket: bucket)
|
14
14
|
@router.opts[:prefix] = prefix
|
15
|
+
@router.opts[:public] = public
|
15
16
|
@router.opts[:options] = options
|
16
17
|
end
|
17
18
|
|
@@ -24,10 +25,14 @@ module Uppy
|
|
24
25
|
plugin :json
|
25
26
|
plugin :json_parser
|
26
27
|
plugin :halt
|
28
|
+
plugin :path_rewriter
|
29
|
+
|
30
|
+
# allow mounting on "/s3" for backwards compatibility
|
31
|
+
rewrite_path "/multipart", ""
|
27
32
|
|
28
33
|
route do |r|
|
29
|
-
# POST /multipart
|
30
|
-
r.post
|
34
|
+
# POST /s3/multipart
|
35
|
+
r.post true do
|
31
36
|
content_type = r.params["type"]
|
32
37
|
filename = r.params["filename"]
|
33
38
|
|
@@ -38,13 +43,16 @@ module Uppy
|
|
38
43
|
# CGI-escape the filename because aws-sdk's signature calculator trips on special characters
|
39
44
|
content_disposition = "inline; filename=\"#{CGI.escape(filename)}\"" if filename
|
40
45
|
|
41
|
-
|
46
|
+
options = { content_type: content_type, content_disposition: content_disposition }
|
47
|
+
options[:acl] = "public-read" if opts[:public]
|
48
|
+
|
49
|
+
result = client_call(:create_multipart_upload, key: key, **options)
|
42
50
|
|
43
51
|
{ uploadId: result.fetch(:upload_id), key: result.fetch(:key) }
|
44
52
|
end
|
45
53
|
|
46
|
-
# GET /multipart/:uploadId
|
47
|
-
r.get
|
54
|
+
# GET /s3/multipart/:uploadId
|
55
|
+
r.get String do |upload_id|
|
48
56
|
key = param!("key")
|
49
57
|
|
50
58
|
result = client_call(:list_parts, upload_id: upload_id, key: key)
|
@@ -54,8 +62,8 @@ module Uppy
|
|
54
62
|
end
|
55
63
|
end
|
56
64
|
|
57
|
-
# GET /multipart/:uploadId/:partNumber
|
58
|
-
r.get
|
65
|
+
# GET /s3/multipart/:uploadId/:partNumber
|
66
|
+
r.get String, String do |upload_id, part_number|
|
59
67
|
key = param!("key")
|
60
68
|
|
61
69
|
result = client_call(:prepare_upload_part, upload_id: upload_id, key: key, part_number: part_number)
|
@@ -63,8 +71,8 @@ module Uppy
|
|
63
71
|
{ url: result.fetch(:url) }
|
64
72
|
end
|
65
73
|
|
66
|
-
# POST /multipart/:uploadId/complete
|
67
|
-
r.post
|
74
|
+
# POST /s3/multipart/:uploadId/complete
|
75
|
+
r.post String, "complete" do |upload_id|
|
68
76
|
key = param!("key")
|
69
77
|
parts = param!("parts")
|
70
78
|
|
@@ -72,17 +80,19 @@ module Uppy
|
|
72
80
|
begin
|
73
81
|
{ part_number: part.fetch("PartNumber"), etag: part.fetch("ETag") }
|
74
82
|
rescue KeyError
|
75
|
-
|
83
|
+
error! "At least one part is missing \"PartNumber\" or \"ETag\" field"
|
76
84
|
end
|
77
85
|
end
|
78
86
|
|
79
|
-
|
87
|
+
client_call(:complete_multipart_upload, upload_id: upload_id, key: key, parts: parts)
|
88
|
+
|
89
|
+
object_url = client_call(:object_url, key: key, public: opts[:public])
|
80
90
|
|
81
|
-
{ location:
|
91
|
+
{ location: object_url }
|
82
92
|
end
|
83
93
|
|
84
|
-
# DELETE /multipart/:uploadId
|
85
|
-
r.delete
|
94
|
+
# DELETE /s3/multipart/:uploadId
|
95
|
+
r.delete String do |upload_id|
|
86
96
|
key = param!("key")
|
87
97
|
|
88
98
|
client_call(:abort_multipart_upload, upload_id: upload_id, key: key)
|
@@ -105,10 +115,14 @@ module Uppy
|
|
105
115
|
def param!(name)
|
106
116
|
value = request.params[name]
|
107
117
|
|
108
|
-
|
118
|
+
error! "Missing \"#{name}\" parameter" if value.nil?
|
109
119
|
|
110
120
|
value
|
111
121
|
end
|
122
|
+
|
123
|
+
def error!(message)
|
124
|
+
request.halt 400, { error: message }
|
125
|
+
end
|
112
126
|
end
|
113
127
|
end
|
114
128
|
end
|
@@ -43,7 +43,15 @@ module Uppy
|
|
43
43
|
**options
|
44
44
|
)
|
45
45
|
|
46
|
-
{ location:
|
46
|
+
{ location: object_url(key: key) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def object_url(key:, public: nil, **options)
|
50
|
+
if public
|
51
|
+
object(key).public_url(**options)
|
52
|
+
else
|
53
|
+
object(key).presigned_url(:get, **options)
|
54
|
+
end
|
47
55
|
end
|
48
56
|
|
49
57
|
def abort_multipart_upload(upload_id:, key:, **options)
|
data/uppy-s3_multipart.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |gem|
|
2
2
|
gem.name = "uppy-s3_multipart"
|
3
|
-
gem.version = "0.
|
3
|
+
gem.version = "0.2.0"
|
4
4
|
|
5
5
|
gem.required_ruby_version = ">= 2.2"
|
6
6
|
|
@@ -19,7 +19,7 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.add_development_dependency "rake"
|
20
20
|
gem.add_development_dependency "minitest"
|
21
21
|
gem.add_development_dependency "rack-test_app"
|
22
|
-
gem.add_development_dependency "shrine", "~> 2.
|
22
|
+
gem.add_development_dependency "shrine", "~> 2.13"
|
23
23
|
gem.add_development_dependency "shrine-memory"
|
24
24
|
gem.add_development_dependency "aws-sdk-core", "~> 3.23"
|
25
25
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: uppy-s3_multipart
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-12-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: roda
|
@@ -92,14 +92,14 @@ dependencies:
|
|
92
92
|
requirements:
|
93
93
|
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version: '2.
|
95
|
+
version: '2.13'
|
96
96
|
type: :development
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version: '2.
|
102
|
+
version: '2.13'
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: shrine-memory
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|