shrine 3.0.0.beta3 → 3.0.0.rc

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad93860597c6a720f0159063d8cbb95a6aa0e93ea89947803342500f1193eb64
4
- data.tar.gz: 79801e74ecd993986099f0f4679fcb37c2b778157c4df5bf6bb80064f2338a44
3
+ metadata.gz: 99f93b258aea07a618b1f35bf08f6b8394dea5d0c6301ed2f96cc72ec12371a4
4
+ data.tar.gz: ac05d107593d54ce6260d5159c8484f03c24ea0e9df4026b615f79fd5bc04a92
5
5
  SHA512:
6
- metadata.gz: 12337c687f57272f9af85b8b8831de448fea258e25e6b7191b95131a60e705374807a87efb7c5e4e7c3db0c6f11a8c3dc87dfe21255d24785e908a93cc94be7e
7
- data.tar.gz: 04561d887c0612ed193c31c470fcdd9ca5ed466794923ceecf979f78049ec3c9c46cf0161776121c72419bcc5f77babfc565ceaf6892a06a547bd93f9c1cccce
6
+ metadata.gz: 640d3a4f07816d7f4a20d385ac1249cd5cdbc46f320a6b8d377338b4ceaa724f5ce1e84782391fd2af6be1419aecd61474a36f03bcffb41c6600f6d999b0be0f
7
+ data.tar.gz: fa93fe25894f9a1125a852e438ca5ecd20518e206418d464afbcf59d8576959e373a3084ff133ef2c8aff062e1a4ba93d64841e19cc14fdc256f87d8871dcb4c
@@ -1,3 +1,17 @@
1
+ ## 3.0.0.rc (2019-09-28)
2
+
3
+ * `core` – Add `Storage#delete_prefixed` method for deleting all files in specified directory (@jrochkind)
4
+
5
+ * `linter` – Return `true` in `Storage::Linter#call` so that it can be used with `assert` (@jrochkind)
6
+
7
+ * `linter` – Allow `Storage::Linter` to accept a key that will be used for testing nonexistent file (@janko)
8
+
9
+ * `core` – Infer file extension from `filename` metadata (@janko)
10
+
11
+ * `pretty_location` – Add `:class_underscore` option for underscoring class name (@Uysim)
12
+
13
+ * Update `down` dependency to `~> 5.0` (@janko)
14
+
1
15
  ## 3.0.0.beta3 (2019-09-25)
2
16
 
3
17
  * `multi_cache` – Add new plugin for whitelisting additional temporary storages (@janko, @jrochkind)
data/README.md CHANGED
@@ -34,6 +34,7 @@ If you're curious how it compares to other file attachment libraries, see the [A
34
34
  - [IO abstraction](#io-abstraction)
35
35
  * [Uploaded file](#uploaded-file)
36
36
  * [Attachment](#attachment)
37
+ - [Temporary storage](#temporary-storage)
37
38
  * [Attacher](#attacher)
38
39
  * [Plugin system](#plugin-system)
39
40
  * [Metadata](#metadata)
@@ -421,6 +422,20 @@ photo.update(image: new_file) # changes the attachment and deletes previous
421
422
  photo.update(image: nil) # removes the attachment and deletes previous
422
423
  ```
423
424
 
425
+ ### Temporary storage
426
+
427
+ Shrine uses temporary storage to enable retaining uploaded files across form
428
+ redisplays and for [direct uploads](#direct-uploads), but you can disable this
429
+ behaviour and have files go straight to permanent storage:
430
+
431
+ ```rb
432
+ Shrine.plugin :model, cache: false
433
+ ```
434
+ ```rb
435
+ photo.image = File.open("waterfall.jpg")
436
+ photo.image.storage_key #=> :store
437
+ ```
438
+
424
439
  ## Attacher
425
440
 
426
441
  The model attachment attributes and callbacks added by `Shrine::Attachment`
@@ -848,12 +863,21 @@ choice][Backgrounding Libraries]:
848
863
 
849
864
  ```rb
850
865
  Shrine.plugin :backgrounding
851
- Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
852
- Shrine::Attacher.destroy_block { DestroyJob.perform_later(self.class, data) }
866
+ Shrine::Attacher.promote_block do
867
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
868
+ end
869
+ Shrine::Attacher.destroy_block do
870
+ DestroyJob.perform_async(self.class.name, data)
871
+ end
853
872
  ```
854
873
  ```rb
855
- class PromoteJob < ActiveJob::Base
856
- def perform(attacher_class, record, name, file_data)
874
+ class PromoteJob
875
+ include Sidekiq::Worker
876
+
877
+ def perform(attacher_class, record_class, record.id, name, file_data)
878
+ attacher_class = Object.const_get(attacher_class)
879
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
880
+
857
881
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
858
882
  attacher.atomic_promote
859
883
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
@@ -862,8 +886,12 @@ class PromoteJob < ActiveJob::Base
862
886
  end
863
887
  ```
864
888
  ```rb
865
- class DestroyJob < ActiveJob::Base
889
+ class DestroyJob
890
+ include Sidekiq::Worker
891
+
866
892
  def perform(attacher_class, data)
893
+ attacher_class = Object.const_get(attacher_class)
894
+
867
895
  attacher = attacher_class.from_data(data)
868
896
  attacher.destroy
869
897
  end
@@ -310,12 +310,17 @@ library][backgrounding libraries].
310
310
 
311
311
  ```rb
312
312
  Shrine::Attacher.promote_block do
313
- PromoteJob.perform_later(self.class, record, name, file_data)
313
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
314
314
  end
315
315
  ```
316
316
  ```rb
317
- class PromoteJob < ActiveJob::Base
318
- def perform(attacher_class, record, name, file_data)
317
+ class PromoteJob
318
+ include Sidekiq::Worker
319
+
320
+ def perform(attacher_class, record_class, record_id, name, file_data)
321
+ attacher_class = Object.const_get(attacher_class)
322
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
323
+
319
324
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
320
325
  attacher.create_derivatives # perform processing
321
326
  attacher.atomic_promote
@@ -288,17 +288,23 @@ Photo.find_each do |photo|
288
288
 
289
289
  next unless attacher.stored?
290
290
 
291
- MakeChangeJob.perform_later(
292
- attacher.class,
293
- attacher.record,
291
+ MakeChangeJob.perform_async(
292
+ attacher.class.name,
293
+ attacher.record.class.name,
294
+ attacher.record.id,
294
295
  attacher.name,
295
296
  attacher.file_data,
296
297
  )
297
298
  end
298
299
  ```
299
300
  ```rb
300
- class MakeChangeJob < ActiveJob::Base
301
- def perform(attacher_class, record, name, file_data)
301
+ class MakeChangeJob
302
+ include Sidekiq::Worker
303
+
304
+ def perform(attacher_class, record_class, record_id, name, file_data)
305
+ attacher_class = Object.const_get(attacher_class)
306
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
307
+
302
308
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
303
309
  # ... make our change ...
304
310
  end
@@ -76,17 +76,23 @@ Photo.find_each do |photo|
76
76
 
77
77
  next unless attacher.stored? # move only attachments uploaded to permanent storage
78
78
 
79
- MoveFilesJob.perform_later(
80
- attacher.class,
81
- attacher.record,
79
+ MoveFilesJob.perform_async(
80
+ attacher.class.name,
81
+ attacher.record.class.name,
82
+ attacher.record.id,
82
83
  attacher.name,
83
84
  attacher.file_data,
84
85
  )
85
86
  end
86
87
  ```
87
88
  ```rb
88
- class MoveFilesJob < ActiveJob::Base
89
- def perform(attacher_class, record, name, file_data)
89
+ class MoveFilesJob
90
+ include Sidekiq::Worker
91
+
92
+ def perform(attacher_class, record_class, record_id, name, file_data)
93
+ attacher_class = Object.const_get(attacher_class)
94
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
95
+
90
96
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
91
97
  old_attacher = attacher.dup
92
98
 
@@ -206,15 +206,24 @@ The storage can support additional options to customize how the presign will be
206
206
  generated, those can be forwarded via the `:presign_options` option on the
207
207
  `presign_endpoint` plugin.
208
208
 
209
- ## Clear
209
+ ## Delete Prefixed and Clear
210
210
 
211
- While this method is not used by Shrine, it is good to give users the
212
- possibility to delete all files in a storage, and the conventional name for
213
- this method is `#clear!`.
211
+ There are two methods that are not currently used by shrine, but which it's good
212
+ for storages to provide to allow client code to delete files from storage. If
213
+ storages provide these conventional methods, then clients can delete files using
214
+ consistent API for any storage.
215
+
216
+ `#clear!` deletes all files from storage, and `#delete_prefixed` will delete all
217
+ files in a given directory/prefix/path. While not strictly required for shrine storage
218
+ service functionality, storages should usually implement if possible.
214
219
 
215
220
  ```rb
216
221
  class MyStorage
217
222
  # ...
223
+ def delete_prefixed(prefix_path)
224
+ # deletes all files under the supplied argument prefix
225
+ end
226
+
218
227
  def clear!
219
228
  # deletes all files in the storage
220
229
  end
@@ -310,7 +310,7 @@ Shrine.plugin :backgrounding
310
310
 
311
311
  Shrine::Attacher.promote_block do
312
312
  # tells a Sidekiq worker to perform in 3 seconds
313
- PromoteJob.perform_in(3, self.class, record.class, record.id, name, file_data)
313
+ PromoteJob.perform_in(3, self.class.name, record.class.name, record.id, name, file_data)
314
314
  end
315
315
  ```
316
316
 
@@ -255,11 +255,19 @@ plugin uses internally):
255
255
  ```rb
256
256
  Shrine.plugin :refresh_metadata # allow re-extracting metadata
257
257
  Shrine.plugin :backgrounding
258
- Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
258
+
259
+ Shrine::Attacher.promote_block do
260
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
261
+ end
259
262
  ```
260
263
  ```rb
261
- class PromoteJob < ActiveJob::Base
262
- def perform(attacher_class, record, name, file_data)
264
+ class PromoteJob
265
+ include Sidekiq::Worker
266
+
267
+ def perform(attacher_class, record_class, record_id, name, file_data)
268
+ attacher_class = Object.const_get(attacher_class)
269
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
270
+
263
271
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
264
272
  attacher.refresh_metadata!
265
273
  attacher.atomic_promote
@@ -270,16 +278,22 @@ end
270
278
  You can also extract metadata in the background separately from promotion:
271
279
 
272
280
  ```rb
273
- MetadataJob.perform_later(
274
- attacher.class,
275
- attacher.record,
281
+ MetadataJob.perform_async(
282
+ attacher.class.name,
283
+ attacher.record.class.name,
284
+ attacher.record.id,
276
285
  attacher.name,
277
286
  attacher.file_data,
278
287
  )
279
288
  ```
280
289
  ```rb
281
- class MetadataJob < ActiveJob::Base
282
- def perform(attacher_class, record, name, file_data)
290
+ class MetadataJob
291
+ include Sidekiq::Worker
292
+
293
+ def perform(attacher_class, record_class, record_id, name, file_data)
294
+ attacher_class = Object.const_get(attacher_class)
295
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
296
+
283
297
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
284
298
  attacher.refresh_metadata!
285
299
  attacher.atomic_persist
@@ -308,9 +322,14 @@ class MyUploader < Shrine
308
322
  end
309
323
  ```
310
324
  ```rb
311
- class MetadataJob < ActiveJob::Base
312
- def perform(record, name, file_data)
313
- attacher = Shrine::Attacher.retrieve(model: record, name: name, file: file_data)
325
+ class MetadataJob
326
+ include Sidekiq::Worker
327
+
328
+ def perform(attacher_class, record_class, record_id, name, file_data)
329
+ attacher_class = Object.const_get(attacher_class)
330
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
331
+
332
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
314
333
  attacher.refresh_metadata!(background: true)
315
334
  attacher.atomic_persist
316
335
  end
@@ -324,9 +343,14 @@ promotion, you can wrap both in an `UploadedFile#open` block to make
324
343
  sure the file content is retrieved from the storage only once.
325
344
 
326
345
  ```rb
327
- class PromoteJob < ActiveJob::Base
328
- def perform(record, name, file_data)
329
- attacher = Shrine::Attacher.retrieve(model: record, name: name, file: file_data)
346
+ class PromoteJob
347
+ include Sidekiq::Worker
348
+
349
+ def perform(attacher_class, record_class, record_id, name, file_data)
350
+ attacher_class = Object.const_get(attacher_class)
351
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
352
+
353
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
330
354
 
331
355
  attacher.file.open do
332
356
  attacher.refresh_metadata!
@@ -15,12 +15,21 @@ jobs:
15
15
 
16
16
  ```rb
17
17
  # register backgrounding blocks for all uploaders
18
- Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
19
- Shrine::Attacher.destroy_block { DestroyJob.perform_later(self.class, data) }
18
+ Shrine::Attacher.promote_block do
19
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
20
+ end
21
+ Shrine::Attacher.destroy_block do
22
+ DestroyJob.perform_async(self.class.name, data)
23
+ end
20
24
  ```
21
25
  ```rb
22
- class PromoteJob < ActiveJob::Base
23
- def perform(attacher_class, record, name, file_data)
26
+ class PromoteJob
27
+ include Sidekiq::Worker
28
+
29
+ def perform(attacher_class, record_class, record_id, name, file_data)
30
+ attacher_class = Object.const_get(attacher_class)
31
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
32
+
24
33
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
25
34
  attacher.atomic_promote
26
35
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
@@ -29,8 +38,12 @@ class PromoteJob < ActiveJob::Base
29
38
  end
30
39
  ```
31
40
  ```rb
32
- class DestroyJob < ActiveJob::Base
41
+ class DestroyJob
42
+ include Sidekiq::Worker
43
+
33
44
  def perform(attacher_class, data)
45
+ attacher_class = Object.const_get(attacher_class)
46
+
34
47
  attacher = attacher_class.from_data(data)
35
48
  attacher.destroy
36
49
  end
@@ -43,8 +56,12 @@ backgrounding blocks only for a specific uploader:
43
56
  ```rb
44
57
  class MyUploader < Shrine
45
58
  # register backgrounding blocks only for this uploader
46
- Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
47
- Attacher.destroy_block { DestroyJob.perform_later(self.class, data) }
59
+ Attacher.promote_block do
60
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
61
+ end
62
+ Attacher.destroy_block do
63
+ DestroyJob.perform_async(self.class.name, data)
64
+ end
48
65
  end
49
66
  ```
50
67
 
@@ -101,17 +118,18 @@ also use the explicit version by declaring an attacher argument:
101
118
 
102
119
  ```rb
103
120
  Shrine::Attacher.promote_block do |attacher|
104
- PromoteJob.perform_later(
105
- attacher.class,
106
- attacher.record,
121
+ PromoteJob.perform_async(
122
+ attacher.class.name,
123
+ attacher.record.class.name,
124
+ attacher.record.id,
107
125
  attacher.name,
108
126
  attacher.file_data,
109
127
  )
110
128
  end
111
129
 
112
130
  Shrine::Attacher.destroy_block do |attacher|
113
- DestroyJob.perform_later(
114
- attacher.class,
131
+ DestroyJob.perform_async(
132
+ attacher.class.name,
115
133
  attacher.data,
116
134
  )
117
135
  end
@@ -122,9 +140,10 @@ flexibility:
122
140
 
123
141
  ```rb
124
142
  photo.image_attacher.promote_block do |attacher|
125
- PromoteJob.perform_later(
126
- attacher.class,
127
- attacher.record,
143
+ PromoteJob.perform_async(
144
+ attacher.class.name,
145
+ attacher.record.class.name,
146
+ attacher.record.id,
128
147
  attacher.name,
129
148
  attacher.file_data,
130
149
  current_user.id, # pass arguments known at the controller level
@@ -135,42 +154,6 @@ photo.image = file
135
154
  photo.save # executes the promote block above
136
155
  ```
137
156
 
138
- ## Other backgrounding libraries
139
-
140
- If you're not using Active Job for backgrounding, you might need to retrieve
141
- records and resolve constants from job arguments manually:
142
-
143
- ```rb
144
- # register backgrounding blocks for all uploaders
145
- Shrine::Attacher.promote_block { PromoteJob.perform_async(self.class, record.class, record.id, name, file_data) }
146
- Shrine::Attacher.destroy_block { DestroyJob.perform_async(self.class, data) }
147
- ```
148
- ```rb
149
- class PromoteJob
150
- include Sidekiq::Worker
151
-
152
- def perform(attacher_class, record_class, record_id, name, file_data)
153
- attacher_class = Object.const_get(attacher_class)
154
- record = Object.const_get(record_class).find(record_id) # if using Active Record
155
-
156
- attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
157
- attacher.atomic_promote
158
- rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
159
- end
160
- end
161
-
162
- class DestroyJob
163
- include Sidekiq::Worker
164
-
165
- def perform(attacher_class, data)
166
- attacher_class = Object.const_get(attacher_class)
167
-
168
- attacher = attacher_class.from_data(data)
169
- attacher.destroy
170
- end
171
- end
172
- ```
173
-
174
157
  [backgrounding]: /lib/shrine/plugins/backgrounding.rb
175
158
  [derivatives]: /doc/plugins/derivatives.md#readme
176
159
  [atomic_helpers]: /doc/plugins/atomic_helpers.md#readme
@@ -49,24 +49,32 @@ You can have mirroring performed in a background job:
49
49
 
50
50
  ```rb
51
51
  Shrine.mirror_upload_block do |file|
52
- MirrorUploadJob.perform_later(file.shrine_class, file.data)
52
+ MirrorUploadJob.perform_async(file.shrine_class.name, file.data)
53
53
  end
54
54
 
55
55
  Shrine.mirror_delete_block do |file|
56
- MirrorDeleteJob.perform_later(file.shrine_class, file.data)
56
+ MirrorDeleteJob.perform_async(file.shrine_class.name, file.data)
57
57
  end
58
58
  ```
59
59
  ```rb
60
- class MirrorUploadJob < ActiveJob::Base
60
+ class MirrorUploadJob
61
+ include Sidekiq::Worker
62
+
61
63
  def perform(shrine_class, file_data)
64
+ shrine_class = Object.const_get(shrine_class)
65
+
62
66
  file = shrine_class.uploaded_file(file_data)
63
67
  file.mirror_upload
64
68
  end
65
69
  end
66
70
  ```
67
71
  ```rb
68
- class MirrorDeleteJob < ActiveJob::Base
72
+ class MirrorDeleteJob
73
+ include Sidekiq::Worker
74
+
69
75
  def perform(shrine_class, file_data)
76
+ shrine_class = Object.const_get(shrine_class)
77
+
70
78
  file = shrine_class.uploaded_file(file_data)
71
79
  file.mirror_delete
72
80
  end
@@ -40,6 +40,17 @@ plugin :pretty_location, identifier: :email
40
40
  # "user/foo@bar.com/profile_picture/493g82jf23.jpg"
41
41
  ```
42
42
 
43
+ By default, the class name will be only downcased. We can also have the class
44
+ name underscored with the `:class_underscore` option:
45
+
46
+ ```ruby
47
+ plugin :pretty_location
48
+ # "blogpost/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
49
+
50
+ plugin :pretty_location, class_underscore: :true
51
+ # "blog_post/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
52
+ ```
53
+
43
54
  For a more custom identifier logic, you can overwrite the method
44
55
  `#generate_location` and call `#pretty_location` with the identifier you have
45
56
  calculated.
@@ -82,11 +82,18 @@ promotion:
82
82
 
83
83
  ```rb
84
84
  Shrine.plugin :backgrounding
85
- Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
85
+ Shrine::Attacher.promote_block do
86
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
87
+ end
86
88
  ```
87
89
  ```rb
88
- class PromoteJob < ActiveJob::Base
89
- def perform(attacher_class, record, name, file_data)
90
+ class PromoteJob
91
+ include Sidekiq::Worker
92
+
93
+ def perform(attacher_class, record_class, record_id, name, file_data)
94
+ attacher_class = Object.const_get(attacher_class)
95
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
96
+
90
97
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
91
98
  attacher.create_derivatives # calls derivatives processor
92
99
  attacher.atomic_promote
@@ -102,16 +109,22 @@ Derivatives don't need to be created as part of the attachment flow, you can
102
109
  create them at any point after promotion:
103
110
 
104
111
  ```rb
105
- DerivativesJob.perform_later(
106
- attacher.class,
107
- attacher.record,
112
+ DerivativesJob.perform_async(
113
+ attacher.class.name,
114
+ attacher.record.class.name,
115
+ attacher.record.id,
108
116
  attacher.name,
109
117
  attacher.file_data,
110
118
  )
111
119
  ```
112
120
  ```rb
113
- class DerivativesJob < ActiveJob::Base
114
- def perform(attacher_class, record, name, file_data)
121
+ class DerivativesJob
122
+ include Sidekiq::Worker
123
+
124
+ def perform(attacher_class, record_class, record_id, name, file_data)
125
+ attacher_class = Object.const_get(attacher_class)
126
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
127
+
115
128
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
116
129
  attacher.create_derivatives # calls derivatives processor
117
130
  attacher.atomic_persist
@@ -144,9 +157,10 @@ end
144
157
  ```
145
158
  ```rb
146
159
  ImageUploader::THUMBNAILS.each_key do |derivative_name|
147
- DerivativeJob.perform_later(
148
- attacher.class,
149
- attacher.record,
160
+ DerivativeJob.perform_async(
161
+ attacher.class.name,
162
+ attacher.record.class.name,
163
+ attacher.record.id,
150
164
  attacher.name,
151
165
  attacher.file_data,
152
166
  derivative_name,
@@ -154,8 +168,13 @@ ImageUploader::THUMBNAILS.each_key do |derivative_name|
154
168
  end
155
169
  ```
156
170
  ```rb
157
- class DerivativeJob < ActiveJob::Base
158
- def perform(attacher_class, record, name, file_data, derivative_name)
171
+ class DerivativeJob
172
+ include Sidekiq::Worker
173
+
174
+ def perform(attacher_class, record_class, record_id, name, file_data, derivative_name)
175
+ attacher_class = Object.const_get(attacher_class)
176
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
177
+
159
178
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
160
179
  attacher.create_derivatives(name: derivative_name)
161
180
  attacher.atomic_persist do |reloaded_attacher|
@@ -135,22 +135,35 @@ how to upgrade.
135
135
 
136
136
  ```rb
137
137
  Shrine.plugin :backgrounding
138
- Shrine::Attacher.promote_block { PromoteJob.promote_later(self.class, record, name, file_data) }
139
- Shrine::Attacher.destroy_block { DestroyJob.promote_later(self.class, data) }
138
+ Shrine::Attacher.promote_block do
139
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
140
+ end
141
+ Shrine::Attacher.destroy_block do
142
+ DestroyJob.perform_async(self.class.name, data)
143
+ end
140
144
  ```
141
145
  ```rb
142
- class PromoteJob < ActiveJob::Base
143
- def perform(attacher_class, record, name, file_data)
146
+ class PromoteJob
147
+ include Sidekiq::Worker
148
+
149
+ def perform(attacher_class, record_class, record.id, name, file_data)
150
+ attacher_class = Object.const_get(attacher_class)
151
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
152
+
144
153
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
145
- attacher.atomic_promote # promote if attachment hasn't changed
154
+ attacher.atomic_promote
146
155
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
147
- # attachment has changed or record has been deleted, nothing to do
156
+ # attachment has changed or the record has been deleted, nothing to do
148
157
  end
149
158
  end
150
159
  ```
151
160
  ```rb
152
- class DestroyJob < ActiveJob::Base
161
+ class DestroyJob
162
+ include Sidekiq::Worker
163
+
153
164
  def perform(attacher_class, data)
165
+ attacher_class = Object.const_get(attacher_class)
166
+
154
167
  attacher = attacher_class.from_data(data)
155
168
  attacher.destroy
156
169
  end
@@ -170,9 +183,10 @@ how to upgrade.
170
183
  photo = Photo.new(photo_params)
171
184
 
172
185
  photo.image_attacher.promote_block do |attacher|
173
- PromoteJob.perform_later(
174
- attacher.class,
175
- attacher.record,
186
+ PromoteJob.perform_async(
187
+ attacher.class.name,
188
+ attacher.record.class.name,
189
+ attacher.record.id,
176
190
  attacher.name,
177
191
  attacher.file_data,
178
192
  current_user.id, # <== parameters from the controller
@@ -196,16 +210,22 @@ how to upgrade.
196
210
  implement metadata extraction in the background in a concurrency-safe way:
197
211
 
198
212
  ```rb
199
- MetadataJob.perform_later(
200
- attacher.class,
201
- attacher.record,
213
+ MetadataJob.perform_async(
214
+ attacher.class.name,
215
+ attacher.record.class.name,
216
+ attacher.record.id,
202
217
  attacher.name,
203
218
  attacher.file_data,
204
219
  )
205
220
  ```
206
221
  ```rb
207
- class MetadataJob < ActiveJob::Base
208
- def perform(attacher_class, record, name, file_data)
222
+ class MetadataJob
223
+ include Sidekiq::Worker
224
+
225
+ def perform(attacher_class, record_class, record_id, name, file_data)
226
+ attacher_class = Object.const_get(attacher_class)
227
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
228
+
209
229
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
210
230
  attacher.refresh_metadata! # extract metadata
211
231
  attacher.atomic_persist # persist if attachment hasn't changed
@@ -215,7 +235,7 @@ how to upgrade.
215
235
  end
216
236
  ```
217
237
 
218
- ## Other features
238
+ ## Other new plugins
219
239
 
220
240
  * The new [`mirroring`][mirroring] plugin has been added for replicating
221
241
  uploads and deletes to other storages.
@@ -272,6 +292,8 @@ how to upgrade.
272
292
  photo.image.storage_key #=> :store (permanent storage)
273
293
  ```
274
294
 
295
+ ## Other features
296
+
275
297
  * New `Shrine.download_response` method has been added to the
276
298
  `download_endpoint` plugin for generating file response from the controller.
277
299
 
@@ -314,10 +336,11 @@ how to upgrade.
314
336
  ```rb
315
337
  class Photo
316
338
  include ImageUploader::Attachment(:image)
317
-
318
- image_attacher #=> #<ImageUploader::Attacher ...>
319
339
  end
320
340
  ```
341
+ ```rb
342
+ Photo.image_attacher #=> #<ImageUploader::Attacher ...>
343
+ ```
321
344
 
322
345
  * The attachment data serializer is now configurable (by default `JSON`
323
346
  standard library is used):
@@ -382,6 +405,17 @@ how to upgrade.
382
405
 
383
406
  ## Other Improvements
384
407
 
408
+ ### Core improvements
409
+
410
+ * Shrine now works again with MRI 2.3.
411
+
412
+ * The memory storage from the [shrine-memory] gem has been merged into core.
413
+
414
+ ```rb
415
+ # Gemfile
416
+ gem "shrine-memory" # this can be removed
417
+ ```
418
+
385
419
  * The `Attacher#assign` method now accepts cached file data as a Hash.
386
420
 
387
421
  ```rb
@@ -396,15 +430,36 @@ how to upgrade.
396
430
 
397
431
  * The temporary storage doesn't need to be defined anymore if it's not used.
398
432
 
399
- * The memory storage from the [shrine-memory] gem has been merged into core.
433
+ * Any changes to `Shrine.storages` will now be applied to existing `Shrine` and
434
+ `Attacher` instances.
435
+
436
+ * When copying the S3 object to another location, any specified upload options
437
+ will now be applied.
438
+
439
+ * Deprecation of passing unknown options to `FileSystem#open` has been
440
+ reverted. This allows users to continue using `FileSystem` storage in tests
441
+ as a mock storage.
442
+
443
+ * The `Shrine#upload` method now infers file extension from `filename` metadata,
444
+ making possible to use `filename` to specify file extension.
400
445
 
401
446
  ```rb
402
- # Gemfile
403
- gem "shrine-memory" # this can be removed
447
+ file = uploader.upload(StringIO.new("some text"), metadata: { "filename" => "file.txt" })
448
+ file.id #=> "2a2467ee6acbc5cb.txt"
404
449
  ```
405
450
 
406
- * When copying the S3 object to another location, any specified upload options
407
- will now be applied.
451
+ * The `Shrine.opts` hash is now deep-copied on subclassing. This allows plugins
452
+ to freely mutate hashes and arrays in `Shrine.opts`, knowing they won't be
453
+ shared across subclasses.
454
+
455
+ * The `down` dependency has been updated to `~> 5.0`.
456
+
457
+ ### Plugin improvements
458
+
459
+ * The `activerecord` plugin now works with Active Record 3.
460
+
461
+ * Callback code from `activerecord` and `sequel` plugin has been moved into
462
+ attacher methods, allowing the user to override them.
408
463
 
409
464
  * The `url_options` plugin now allows you to override URL options by deleting
410
465
  them.
@@ -448,6 +503,9 @@ how to upgrade.
448
503
  * The `Derivation#upload` method from `derivation_endpoint` plugin now accepts
449
504
  any IO-like object.
450
505
 
506
+ * The `derivation_endpoint` plugin doesn't re-open `File` objects returned in
507
+ derivation block anymore.
508
+
451
509
  * The `instrumentation` plugin now instruments `UploadedFile#open` calls as a
452
510
  new `open.shrine` event. `UploadedFile#download` is still instrumented as
453
511
  `download.shrine`.
@@ -462,29 +520,19 @@ how to upgrade.
462
520
  * Any options passed to `Attacher#attach_cached` are now forwarded to metadata
463
521
  extraction when `restore_cached_data` plugin is loaded.
464
522
 
465
- * Deprecation of passing unknown options to `FileSystem#open` has been
466
- reverted. This allows users to continue using `FileSystem` storage in tests
467
- as a mock storage.
468
-
469
- * The `activerecord` plugin now works with Active Record 3.
470
-
471
- * Shrine now works again with MRI 2.3.
472
-
473
523
  * The `infer_extension` plugin now works correctly with `pretty_location`
474
524
  plugin when `pretty_location` was loaded after `infer_extension`.
475
525
 
476
- * The `derivation_endpoint` plugin doesn't re-open `File` objects returned in
477
- derivation block anymore.
478
-
479
- * Any changes to `Shrine.storages` will now be applied to existing `Shrine` and
480
- `Attacher` instances.
526
+ * The `pretty_location` plugin now accepts `:class_underscore` option for
527
+ underscoring class names.
481
528
 
482
- * Callback code from `activerecord` and `sequel` plugin has been moved into
483
- attacher methods, allowing the user to override them.
529
+ ```rb
530
+ plugin :pretty_location
531
+ # "blogpost/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
484
532
 
485
- * The `Shrine.opts` hash is now deep-copied on subclassing. This allows plugins
486
- to freely mutate hashes and arrays in `Shrine.opts`, knowing they won't be
487
- shared across subclasses.
533
+ plugin :pretty_location, class_underscore: :true
534
+ # "blog_post/aa357797-5845-451b-8662-08eecdc9f762/image/493g82jf23.jpg"
535
+ ```
488
536
 
489
537
  ## Backwards compatibility
490
538
 
@@ -636,8 +684,7 @@ how to upgrade.
636
684
 
637
685
  ### S3 API
638
686
 
639
- * The support for `aws-sdk` 2.x and `aws-sdk-s3` lower than 1.14 has been
640
- removed.
687
+ * The support for `aws-sdk` 2.x and `aws-sdk-s3` < 1.14 has been removed.
641
688
 
642
689
  * `S3#open` now raises `Shrine::FileNotFound` exception when S3 object doesn't
643
690
  exist on the bucket.
@@ -74,6 +74,15 @@ You can retrieve path to the file using `#path`:
74
74
  storage.path("image.jpg") #=> #<Pathname:public/image.jpg>
75
75
  ```
76
76
 
77
+ ## Deleting prefixed
78
+
79
+ If you want to delete all files in some directory, you can use
80
+ `FileSystem#delete_prefixed`:
81
+
82
+ ```rb
83
+ storage.delete_prefixed("some_directory") # deletes all files in "some_directory/"
84
+ ```
85
+
77
86
  ## Clearing cache
78
87
 
79
88
  If you're using FileSystem as cache, you will probably want to periodically
@@ -259,6 +259,15 @@ To use Amazon S3's [Transfer Acceleration] feature, set
259
259
  Shrine::Storage::S3.new(use_accelerate_endpoint: true, **other_options)
260
260
  ```
261
261
 
262
+ ## Deleting prefixed
263
+
264
+ If you want to delete all objects in some prefix, you can use
265
+ `S3#delete_prefixed`:
266
+
267
+ ```rb
268
+ s3.delete_prefixed("some_prefix") # deletes all objects in "some_prefix/"
269
+ ```
270
+
262
271
  ## Clearing cache
263
272
 
264
273
  If you're using S3 as a cache, you will probably want to periodically delete
@@ -161,12 +161,21 @@ The `backgrounding` plugin has been rewritten in Shrine 3.0 and has a new API.
161
161
 
162
162
  ```rb
163
163
  Shrine.plugin :backgrounding
164
- Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
165
- Shrine::Attacher.destroy_block { DestroyJob.perform_later(self.class, data) }
164
+ Shrine::Attacher.promote_block do
165
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
166
+ end
167
+ Shrine::Attacher.destroy_block do
168
+ DestroyJob.perform_async(self.class.name, data)
169
+ end
166
170
  ```
167
171
  ```rb
168
- class PromoteJob < ActiveJob::Base
169
- def perform(attacher_class, record, name, file_data)
172
+ class PromoteJob
173
+ include Sidekiq::Worker
174
+
175
+ def perform(attacher_class, record_class, record_id, name, file_data)
176
+ attacher_class = Object.const_get(attacher_class)
177
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
178
+
170
179
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
171
180
  attacher.atomic_promote
172
181
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
@@ -175,8 +184,12 @@ class PromoteJob < ActiveJob::Base
175
184
  end
176
185
  ```
177
186
  ```rb
178
- class DestroyJob < ActiveJob::Base
187
+ class DestroyJob
188
+ include Sidekiq::Worker
189
+
179
190
  def perform(attacher_class, data)
191
+ attacher_class = Object.const_get(attacher_class)
192
+
180
193
  attacher = attacher_class.from_data(data)
181
194
  attacher.destroy
182
195
  end
@@ -191,16 +204,21 @@ both argument formats, and then switch to the new one once the jobs with old
191
204
  format have been drained.
192
205
 
193
206
  ```rb
194
- class PromoteJob < ActiveJob::Base
207
+ class PromoteJob
208
+ include Sidekiq::Worker
209
+
195
210
  def perform(*args)
196
211
  if args.one?
197
212
  file_data, (record_class, record_id), name, shrine_class =
198
213
  args.first.values_at("attachment", "record", "name", "shrine_class")
199
214
 
200
- record = Object.const_get(record_class).find(record_id)
215
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
201
216
  attacher_class = Object.const_get(shrine_class)::Attacher
202
217
  else
203
- attacher_class, record, name, file_data = args
218
+ attacher_class, record_class, record_id, name, file_data = args
219
+
220
+ attacher_class = Object.const_get(attacher_class)
221
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
204
222
  end
205
223
 
206
224
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
@@ -211,7 +229,9 @@ class PromoteJob < ActiveJob::Base
211
229
  and
212
230
  ```
213
231
  ```rb
214
- class DestroyJob < ActiveJob::Base
232
+ class DestroyJob
233
+ include Sidekiq::Worker
234
+
215
235
  def perform(*args)
216
236
  if args.one?
217
237
  data, shrine_class = args.first.values_at("attachment", "shrine_class")
@@ -220,6 +240,8 @@ class DestroyJob < ActiveJob::Base
220
240
  attacher_class = Object.const_get(shrine_class)::Attacher
221
241
  else
222
242
  attacher_class, data = args
243
+
244
+ attacher_class = Object.const_get(attacher_class)
223
245
  end
224
246
 
225
247
  attacher = attacher_class.from_data(data)
@@ -393,8 +415,13 @@ If you're using the `backgrounding` plugin, you can trigger derivatives
393
415
  creation in the `PromoteJob` instead of the controller:
394
416
 
395
417
  ```rb
396
- class PromoteJob < ActiveJob::Base
397
- def perform(attacher_class, record, name, file_data)
418
+ class PromoteJob
419
+ include Sidekiq::Worker
420
+
421
+ def perform(attacher_class, record_class, record.id, name, file_data)
422
+ attacher_class = Object.const_get(attacher_class)
423
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
424
+
398
425
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
399
426
  attacher.create_derivatives # call derivatives processor
400
427
  attacher.atomic_promote
@@ -273,7 +273,7 @@ class Shrine
273
273
  # Generates a basic location for an uploaded file
274
274
  def basic_location(io, metadata:)
275
275
  extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
276
- extension ||= File.extname(extract_filename(io).to_s).downcase
276
+ extension ||= File.extname(metadata["filename"].to_s).downcase
277
277
  basename = generate_uid(io)
278
278
 
279
279
  basename + extension
@@ -34,9 +34,17 @@ class Shrine
34
34
  record.public_send(opts[:pretty_location][:identifier])
35
35
  end
36
36
 
37
+ def transform_class_name(class_name)
38
+ if opts[:pretty_location][:class_underscore]
39
+ class_name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
40
+ else
41
+ class_name.downcase
42
+ end
43
+ end
44
+
37
45
  def record_namespace(record)
38
46
  class_name = record.class.name or return
39
- parts = class_name.downcase.split("::")
47
+ parts = transform_class_name(class_name).split("::")
40
48
 
41
49
  if separator = opts[:pretty_location][:namespace]
42
50
  parts.join(separator)
@@ -66,12 +66,12 @@ class Shrine
66
66
 
67
67
  def download_remote_url(url, options)
68
68
  opts[:remote_url][:downloader].call(url, options)
69
- rescue Down::NotFound
70
- fail DownloadError, "remote file not found"
71
69
  rescue Down::TooLarge
72
70
  fail DownloadError, "remote file too large"
71
+ rescue Down::Error
72
+ fail DownloadError, "remote file not found"
73
73
  rescue DownloadError
74
- fail
74
+ fail # re-raise
75
75
  end
76
76
 
77
77
  # Sends a `remote_url.shrine` event for instrumentation plugin.
@@ -70,6 +70,16 @@ class Shrine
70
70
  path(id).exist?
71
71
  end
72
72
 
73
+ # If #prefix is not present, returns a path composed of #directory and
74
+ # the given `id`. If #prefix is present, it excludes the #directory part
75
+ # from the returned path (e.g. #directory can be set to "public" folder).
76
+ # Both cases accept a `:host` value which will be prefixed to the
77
+ # generated path.
78
+ def url(id, host: nil, **options)
79
+ path = (prefix ? relative_path(id) : path(id)).to_s
80
+ host ? host + path : path
81
+ end
82
+
73
83
  # Delets the file, and by default deletes the containing directory if
74
84
  # it's empty.
75
85
  def delete(id)
@@ -79,14 +89,11 @@ class Shrine
79
89
  rescue Errno::ENOENT
80
90
  end
81
91
 
82
- # If #prefix is not present, returns a path composed of #directory and
83
- # the given `id`. If #prefix is present, it excludes the #directory part
84
- # from the returned path (e.g. #directory can be set to "public" folder).
85
- # Both cases accept a `:host` value which will be prefixed to the
86
- # generated path.
87
- def url(id, host: nil, **options)
88
- path = (prefix ? relative_path(id) : path(id)).to_s
89
- host ? host + path : path
92
+ # Deletes the specified directory on the filesystem.
93
+ #
94
+ # file_system.delete_prefixed("somekey/derivatives")
95
+ def delete_prefixed(delete_prefix)
96
+ FileUtils.rm_rf directory.join(delete_prefix)
90
97
  end
91
98
 
92
99
  # Deletes all files from the #directory. If a block is passed in, deletes
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "shrine"
4
2
 
5
3
  require "forwardable"
@@ -30,19 +28,32 @@ class Shrine
30
28
  new(*args).call
31
29
  end
32
30
 
33
- def initialize(storage, action: :error)
34
- @storage = storage
35
- @action = action
31
+ def initialize(storage, action: :error, nonexisting: "nonexisting")
32
+ @storage = storage
33
+ @action = action
34
+ @nonexisting = nonexisting
36
35
  end
37
36
 
38
37
  def call(io_factory = default_io_factory)
39
- storage.upload(io_factory.call, id = "foo".dup, {})
38
+ storage.upload(io_factory.call, id = "foo", {})
40
39
 
41
40
  lint_open(id)
42
41
  lint_exists(id)
43
42
  lint_url(id)
44
43
  lint_delete(id)
45
44
 
45
+ if storage.respond_to?(:delete_prefixed)
46
+ storage.upload(io_factory.call, id1 = "a/a/a")
47
+ storage.upload(io_factory.call, id2 = "a/a/b")
48
+ storage.upload(io_factory.call, id3 = "a/aaa/a")
49
+
50
+ lint_delete_prefixed(prefix: "a/a",
51
+ expect_deleted: [id1, id2],
52
+ expect_remaining: [id3])
53
+
54
+ storage.delete(id3)
55
+ end
56
+
46
57
  if storage.respond_to?(:clear!)
47
58
  storage.upload(io_factory.call, id = "quux".dup)
48
59
  lint_clear(id)
@@ -51,6 +62,8 @@ class Shrine
51
62
  if storage.respond_to?(:presign)
52
63
  lint_presign(id)
53
64
  end
65
+
66
+ true
54
67
  end
55
68
 
56
69
  def lint_open(id)
@@ -60,7 +73,7 @@ class Shrine
60
73
  opened.close
61
74
 
62
75
  begin
63
- storage.open("nonexisting", {})
76
+ storage.open(@nonexisting, {})
64
77
  error :open, "should raise an exception on nonexisting file"
65
78
  rescue Shrine::FileNotFound
66
79
  rescue => exception
@@ -100,6 +113,20 @@ class Shrine
100
113
  error :presign, "result should include :url key" unless data.to_h.key?(:url)
101
114
  end
102
115
 
116
+ def lint_delete_prefixed(prefix:, expect_deleted:, expect_remaining:)
117
+ storage.delete_prefixed(prefix)
118
+
119
+ expect_deleted.each do |key|
120
+ next unless storage.exists?(key)
121
+ error :delete_prefixed, "#{key} still #exists? after #clear_prefix('a/a/')"
122
+ end
123
+
124
+ expect_remaining.each do |key|
125
+ next if storage.exists?(key)
126
+ error :delete_prefixed, "#{key} doesn't #exists? but should after #clear_prefix('a/a/')"
127
+ end
128
+ end
129
+
103
130
  private
104
131
 
105
132
  attr_reader :storage
@@ -26,12 +26,16 @@ class Shrine
26
26
  store.key?(id)
27
27
  end
28
28
 
29
+ def url(id, *)
30
+ "memory://#{id}"
31
+ end
32
+
29
33
  def delete(id)
30
34
  store.delete(id)
31
35
  end
32
36
 
33
- def url(id, *)
34
- "memory://#{id}"
37
+ def delete_prefixed(delete_prefix)
38
+ store.delete_if { |k, _v| k.start_with?(delete_prefix + "/") }
35
39
  end
36
40
 
37
41
  def clear!
@@ -205,6 +205,16 @@ class Shrine
205
205
  object(id).delete
206
206
  end
207
207
 
208
+ # Deletes objects at keys starting with the specified prefix.
209
+ #
210
+ # s3.delete_prefixed("somekey/derivatives")
211
+ def delete_prefixed(delete_prefix)
212
+ # We need to make sure to combine with storage prefix, and
213
+ # that it ends in '/' cause S3 can be squirrely about matching interior.
214
+ absolute_prefix = [*prefix, delete_prefix + "/"].join("/")
215
+ bucket.objects(prefix: absolute_prefix).batch_delete!
216
+ end
217
+
208
218
  # If block is given, deletes all objects from the storage for which the
209
219
  # block evaluates to true. Otherwise deletes all objects from the storage.
210
220
  #
@@ -9,7 +9,7 @@ class Shrine
9
9
  MAJOR = 3
10
10
  MINOR = 0
11
11
  TINY = 0
12
- PRE = "beta3"
12
+ PRE = "rc"
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15
15
  end
@@ -33,7 +33,7 @@ direct uploads for fully asynchronous user experience.
33
33
  gem.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "lib/**/*.rb", "shrine.gemspec", "doc/**/*.md"]
34
34
  gem.require_path = "lib"
35
35
 
36
- gem.add_dependency "down", "~> 4.1"
36
+ gem.add_dependency "down", "~> 5.0"
37
37
  gem.add_dependency "content_disposition", "~> 1.0"
38
38
 
39
39
  # general testing helpers
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.beta3
4
+ version: 3.0.0.rc
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-25 00:00:00.000000000 Z
11
+ date: 2019-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '4.1'
19
+ version: '5.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '4.1'
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: content_disposition
29
29
  requirement: !ruby/object:Gem::Requirement