shrine 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +101 -149
- data/doc/carrierwave.md +12 -16
- data/doc/changing_location.md +50 -0
- data/doc/creating_plugins.md +2 -2
- data/doc/creating_storages.md +70 -9
- data/doc/direct_s3.md +132 -61
- data/doc/migrating_storage.md +12 -10
- data/doc/paperclip.md +12 -17
- data/doc/refile.md +338 -0
- data/doc/regenerating_versions.md +75 -11
- data/doc/securing_uploads.md +172 -0
- data/lib/shrine.rb +21 -16
- data/lib/shrine/plugins/activerecord.rb +2 -2
- data/lib/shrine/plugins/background_helpers.rb +2 -148
- data/lib/shrine/plugins/backgrounding.rb +148 -0
- data/lib/shrine/plugins/backup.rb +88 -0
- data/lib/shrine/plugins/data_uri.rb +25 -4
- data/lib/shrine/plugins/default_url.rb +37 -0
- data/lib/shrine/plugins/delete_uploaded.rb +40 -0
- data/lib/shrine/plugins/determine_mime_type.rb +4 -2
- data/lib/shrine/plugins/direct_upload.rb +107 -62
- data/lib/shrine/plugins/download_endpoint.rb +157 -0
- data/lib/shrine/plugins/hooks.rb +19 -5
- data/lib/shrine/plugins/keep_location.rb +43 -0
- data/lib/shrine/plugins/moving.rb +11 -10
- data/lib/shrine/plugins/parallelize.rb +1 -5
- data/lib/shrine/plugins/parsed_json.rb +7 -1
- data/lib/shrine/plugins/pretty_location.rb +6 -0
- data/lib/shrine/plugins/rack_file.rb +7 -1
- data/lib/shrine/plugins/remove_invalid.rb +22 -0
- data/lib/shrine/plugins/sequel.rb +2 -2
- data/lib/shrine/plugins/upload_options.rb +41 -0
- data/lib/shrine/plugins/versions.rb +9 -7
- data/lib/shrine/storage/file_system.rb +46 -30
- data/lib/shrine/storage/linter.rb +48 -25
- data/lib/shrine/storage/s3.rb +89 -22
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +3 -3
- metadata +16 -5
@@ -0,0 +1,172 @@
|
|
1
|
+
# Securing uploads
|
2
|
+
|
3
|
+
Shrine does a lot to make your file uploads secure, but there are still a lot
|
4
|
+
of security measures that could be added by the user on the application's side.
|
5
|
+
This guide will try to cover all the well-known security issues, ranging from
|
6
|
+
the obvious ones to not-so-obvious ones, and try to provide solutions.
|
7
|
+
|
8
|
+
## Validate file type
|
9
|
+
|
10
|
+
Almost always you will be accepting certain types of files, and it's a good
|
11
|
+
idea to create a whitelist (or blaclist) of supported extensions and MIME
|
12
|
+
types.
|
13
|
+
|
14
|
+
By default Shrine stores the MIME type derived from the extension, which means
|
15
|
+
it's not guaranteed to hold the actual MIME type of the the file. However, you
|
16
|
+
can load the `determine_mime_type` plugin which by default uses the [file]
|
17
|
+
utility to determine the MIME type from magic file headers.
|
18
|
+
|
19
|
+
```rb
|
20
|
+
class MyUploader < Shrine
|
21
|
+
plugin :validation_helpers
|
22
|
+
plugin :determine_mime_type
|
23
|
+
|
24
|
+
Attacher.validate do
|
25
|
+
validate_extension_inclusion [/jpe?g/, "png", "gif"]
|
26
|
+
validate_mime_type_inclusion ["image/jpeg", "image/png", "image/gif"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
## Limit filesize
|
32
|
+
|
33
|
+
It's a good idea to generally limit the filesize of uploaded files, so that
|
34
|
+
attackers cannot easily flood your storage. There are various layers at which
|
35
|
+
you can apply filesize limits, depending on how you're accepting uploads.
|
36
|
+
Firstly, you should probably add a filesize validation to prevent large files
|
37
|
+
from being uploaded to `:store`:
|
38
|
+
|
39
|
+
```rb
|
40
|
+
class MyUploader < Shrine
|
41
|
+
plugin :validation_helpers
|
42
|
+
|
43
|
+
Attacher.validate do
|
44
|
+
validate_max_size 20*1024*1024 # 20 MB
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
In the following sections we talk about various strategies to prevent files from
|
50
|
+
being uploaded to cache and the temporary directory.
|
51
|
+
|
52
|
+
### Direct uploads
|
53
|
+
|
54
|
+
If you're doing direct uploads with the `direct_upload` plugin, you can pass
|
55
|
+
in the `:max_size` option, which will refuse too large files and automatically
|
56
|
+
delete it from temporary storage.
|
57
|
+
|
58
|
+
```rb
|
59
|
+
plugin :direct_upload, max_size: 20*1024*1024 # 20 MB
|
60
|
+
```
|
61
|
+
|
62
|
+
This option doesn't apply to presigned uploads, if you're using S3 you can
|
63
|
+
limit the filesize on presigning:
|
64
|
+
|
65
|
+
```rb
|
66
|
+
plugin :direct_upload, presign: ->(request) do
|
67
|
+
{content_length_range: 0..20*1024*1024}
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
### Regular uploads
|
72
|
+
|
73
|
+
If you're simply accepting uploads synchronously in the form, you can prevent
|
74
|
+
large files from getting into cache by loading the `remove_invalid` plugin:
|
75
|
+
|
76
|
+
```rb
|
77
|
+
plugin :remove_invalid
|
78
|
+
```
|
79
|
+
|
80
|
+
### Limiting at application level
|
81
|
+
|
82
|
+
If your application is accepting file uploads directly (either through direct
|
83
|
+
uploads or regular ones), you can limit the maximum request body size in your
|
84
|
+
application server (nginx or apache):
|
85
|
+
|
86
|
+
```sh
|
87
|
+
# nginx.conf
|
88
|
+
|
89
|
+
http {
|
90
|
+
# ...
|
91
|
+
server {
|
92
|
+
# ...
|
93
|
+
client_max_body_size 20M;
|
94
|
+
}
|
95
|
+
}
|
96
|
+
```
|
97
|
+
|
98
|
+
### Paranoid limiting
|
99
|
+
|
100
|
+
If you want to make sure that no large files ever get to your storages, and
|
101
|
+
you don't really care about the error message, you can use the `hooks` plugin
|
102
|
+
and raise an error:
|
103
|
+
|
104
|
+
```rb
|
105
|
+
class MyUploader
|
106
|
+
plugin :hooks
|
107
|
+
|
108
|
+
def before_upload(io, context)
|
109
|
+
if io.respond_to?(:read)
|
110
|
+
raise FileTooLarge if io.size >= 20*1024*1024
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
## Limit image dimensions
|
117
|
+
|
118
|
+
It's possible to create so-called [image bombs], which are images that have a
|
119
|
+
small filesize but very large dimensions. These are dangerous if you're doing
|
120
|
+
image processing, since processing them can take a lot of time and memory. This
|
121
|
+
makes it trivial to DoS the application which doesn't have any protection
|
122
|
+
against them.
|
123
|
+
|
124
|
+
Shrine uses the [fastimage] gem for determining image dimensions which has
|
125
|
+
built-in protection against image bombs (ImageMagick for example doesn't), but
|
126
|
+
you still need to prevent those files from being attached and processed:
|
127
|
+
|
128
|
+
```rb
|
129
|
+
class MyUploader < Shrine
|
130
|
+
plugin :store_dimensions
|
131
|
+
plugin :validation_helpers
|
132
|
+
|
133
|
+
Attacher.validate do
|
134
|
+
validate_max_width 2500
|
135
|
+
validate_max_height 2500
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
If you're doing processing on caching, you can use the fastimage gem directly
|
141
|
+
in a conditional.
|
142
|
+
|
143
|
+
## Limit number of files
|
144
|
+
|
145
|
+
When doing direct uploads, it's a good idea to apply some kind of throttling to
|
146
|
+
the endpoint, to ensure the attacker cannot upload an unlimited number files,
|
147
|
+
because even with a filesize limit it would allow flooding the storage. A good
|
148
|
+
library for throttling requests is [rack-attack].
|
149
|
+
|
150
|
+
Also, it's generally a good idea to limit the *minimum* filesize as well as
|
151
|
+
maximum, to prevent uploading large amounts of small files:
|
152
|
+
|
153
|
+
```rb
|
154
|
+
class MyUploader < Shrine
|
155
|
+
plugin :validation_helpers
|
156
|
+
|
157
|
+
Attacher.validate do
|
158
|
+
validate_min_size 10*1024 # 10 KB
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
## References
|
164
|
+
|
165
|
+
* [Nvisium: Secure File Uploads](https://nvisium.com/blog/2015/10/13/secure-file-uploads/)
|
166
|
+
* [OWASP: Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload)
|
167
|
+
* [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/)
|
168
|
+
|
169
|
+
[image bombs]: https://www.bamsoftware.com/hacks/deflate.html
|
170
|
+
[fastimage]: https://github.com/sdsykes/fastimage
|
171
|
+
[file]: http://linux.die.net/man/1/file
|
172
|
+
[rack-attack]: https://github.com/kickstarter/rack-attack
|
data/lib/shrine.rb
CHANGED
@@ -207,7 +207,8 @@ class Shrine
|
|
207
207
|
|
208
208
|
# The main method for uploading files. Takes in an IO object and an
|
209
209
|
# optional context (used internally by Shrine::Attacher). It calls
|
210
|
-
# user-defined #process, and aferwards it calls #store.
|
210
|
+
# user-defined #process, and aferwards it calls #store. The `io` is
|
211
|
+
# closed after upload.
|
211
212
|
def upload(io, context = {})
|
212
213
|
io = processed(io, context) || io
|
213
214
|
store(io, context)
|
@@ -309,15 +310,15 @@ class Shrine
|
|
309
310
|
# the metadata, stores the file, and returns a Shrine::UploadedFile.
|
310
311
|
def _store(io, context)
|
311
312
|
_enforce_io(io)
|
312
|
-
|
313
|
-
metadata
|
313
|
+
context[:location] ||= get_location(io, context)
|
314
|
+
context[:metadata] ||= extract_metadata(io, context)
|
314
315
|
|
315
|
-
put(io, context
|
316
|
+
put(io, context)
|
316
317
|
|
317
318
|
self.class::UploadedFile.new(
|
318
|
-
"id" => location,
|
319
|
+
"id" => context[:location],
|
319
320
|
"storage" => storage_key.to_s,
|
320
|
-
"metadata" => metadata,
|
321
|
+
"metadata" => context[:metadata],
|
321
322
|
)
|
322
323
|
end
|
323
324
|
|
@@ -334,12 +335,7 @@ class Shrine
|
|
334
335
|
# Does the actual uploading, calling `#upload` on the storage.
|
335
336
|
def copy(io, context)
|
336
337
|
storage.upload(io, context[:location], context[:metadata])
|
337
|
-
|
338
|
-
|
339
|
-
# Some storages support moving, so we provide this method for plugins
|
340
|
-
# to use, but by default the file will be copied.
|
341
|
-
def move(io, context)
|
342
|
-
storage.move(io, context[:location], context[:metadata])
|
338
|
+
io.close rescue nil
|
343
339
|
end
|
344
340
|
|
345
341
|
# Does the actual deletion, calls `UploadedFile#delete`.
|
@@ -352,6 +348,12 @@ class Shrine
|
|
352
348
|
process(io, context)
|
353
349
|
end
|
354
350
|
|
351
|
+
# Retrieves the location for the given io and context. First it looks
|
352
|
+
# for the `:location` option, otherwise it calls #generate_location.
|
353
|
+
def get_location(io, context)
|
354
|
+
generate_location(io, context)
|
355
|
+
end
|
356
|
+
|
355
357
|
# Checks if the object is a valid IO by checking that it responds to
|
356
358
|
# `#read`, `#eof?`, `#rewind`, `#size` and `#close`, otherwise raises
|
357
359
|
# Shrine::InvalidFile.
|
@@ -598,7 +600,9 @@ class Shrine
|
|
598
600
|
|
599
601
|
# Delegates to `Shrine#default_url`.
|
600
602
|
def default_url(**options)
|
601
|
-
store.default_url(options.merge(context))
|
603
|
+
url = store.default_url(options.merge(context))
|
604
|
+
warn "Overriding Shrine#default_url is deprecated and will be removed in Shrine 2. You should use the default_url plugin." if url
|
605
|
+
url
|
602
606
|
end
|
603
607
|
|
604
608
|
# The validation block provided by `Shrine.validate`.
|
@@ -630,7 +634,7 @@ class Shrine
|
|
630
634
|
# The context that's sent to Shrine on upload and delete. It holds the
|
631
635
|
# record and the name of the attachment.
|
632
636
|
def context
|
633
|
-
@context ||= {name: name, record: record}
|
637
|
+
@context ||= {name: name, record: record}
|
634
638
|
end
|
635
639
|
end
|
636
640
|
|
@@ -676,8 +680,7 @@ class Shrine
|
|
676
680
|
|
677
681
|
# The extension derived from `#original_filename`.
|
678
682
|
def extension
|
679
|
-
extname
|
680
|
-
extname[1..-1] unless extname.empty?
|
683
|
+
File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
|
681
684
|
end
|
682
685
|
|
683
686
|
# The filesize of the original file.
|
@@ -689,6 +692,7 @@ class Shrine
|
|
689
692
|
def mime_type
|
690
693
|
metadata.fetch("mime_type")
|
691
694
|
end
|
695
|
+
alias content_type mime_type
|
692
696
|
|
693
697
|
# Part of Shrine::UploadedFile's complying to the IO interface. It
|
694
698
|
# delegates to the internally downloaded file.
|
@@ -706,6 +710,7 @@ class Shrine
|
|
706
710
|
# delegates to the internally downloaded file.
|
707
711
|
def close
|
708
712
|
io.close
|
713
|
+
io.delete if io.class.name == "Tempfile"
|
709
714
|
end
|
710
715
|
|
711
716
|
# Part of Shrine::UploadedFile's complying to the IO interface. It
|
@@ -26,7 +26,7 @@ class Shrine
|
|
26
26
|
# you should first disable these transactions for those tests.
|
27
27
|
#
|
28
28
|
# If you want to put some parts of this lifecycle into a background job, see
|
29
|
-
# the
|
29
|
+
# the backgrounding plugin.
|
30
30
|
#
|
31
31
|
# Additionally, any Shrine validation errors will added to ActiveRecord's
|
32
32
|
# errors upon validation. Note that if you want to validate presence of the
|
@@ -64,7 +64,7 @@ class Shrine
|
|
64
64
|
end
|
65
65
|
|
66
66
|
module AttacherClassMethods
|
67
|
-
# Needed by the
|
67
|
+
# Needed by the backgrounding plugin.
|
68
68
|
def find_record(record_class, record_id)
|
69
69
|
record_class.find(record_id)
|
70
70
|
end
|
@@ -1,148 +1,2 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# The background_helpers plugin enables you to intercept phases of
|
4
|
-
# uploading and put them into background jobs. This doesn't require any
|
5
|
-
# additional columns.
|
6
|
-
#
|
7
|
-
# plugin :background_helpers
|
8
|
-
#
|
9
|
-
# ## Promoting
|
10
|
-
#
|
11
|
-
# If you're doing processing, or your `:store` is something other than
|
12
|
-
# Storage::FileSystem, it's recommended to put promoting (moving to store)
|
13
|
-
# into a background job. This plugin allows you to do that by calling
|
14
|
-
# `Shrine::Attacher.promote`:
|
15
|
-
#
|
16
|
-
# Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
|
17
|
-
#
|
18
|
-
# When you call `Shrine::Attacher.promote` with a block, it will save the
|
19
|
-
# block and call it on every promotion. Then in your background job you can
|
20
|
-
# again call `Shrine::Attacher.promote` with the data, and internally it
|
21
|
-
# will resolve all necessary objects, do the promoting and update the
|
22
|
-
# record.
|
23
|
-
#
|
24
|
-
# class UploadJob
|
25
|
-
# include Sidekiq::Worker
|
26
|
-
#
|
27
|
-
# def perform(data)
|
28
|
-
# Shrine::Attacher.promote(data)
|
29
|
-
# end
|
30
|
-
# end
|
31
|
-
#
|
32
|
-
# Shrine automatically handles all concurrency issues, such as canceling
|
33
|
-
# promoting if the attachment has changed in the meanwhile.
|
34
|
-
#
|
35
|
-
# ## Deleting
|
36
|
-
#
|
37
|
-
# If your `:store` is something other than Storage::FileSystem, it's
|
38
|
-
# recommended to put deleting files into a background job. This plugin
|
39
|
-
# allows you to do that by calling `Shrine::Attacher.delete`:
|
40
|
-
#
|
41
|
-
# Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
42
|
-
#
|
43
|
-
# When you call `Shrine::Attacher.delete` with a block, it will save the
|
44
|
-
# block and call it on every delete. Then in your background job you can
|
45
|
-
# again call `Shrine::Attacher.delete` with the data, and internally it
|
46
|
-
# will resolve all necessary objects, and delete the file.
|
47
|
-
#
|
48
|
-
# class DeleteJob
|
49
|
-
# include Sidekiq::Worker
|
50
|
-
#
|
51
|
-
# def perform(data)
|
52
|
-
# Shrine::Attacher.delete(data)
|
53
|
-
# end
|
54
|
-
# end
|
55
|
-
#
|
56
|
-
# ## Conclusion
|
57
|
-
#
|
58
|
-
# The examples above used Sidekiq, but obviously you can just as well use
|
59
|
-
# any other backgrounding library. Also, if you want you can use
|
60
|
-
# backgrounding just for certain uploaders:
|
61
|
-
#
|
62
|
-
# class ImageUploader < Shrine
|
63
|
-
# Attacher.promote { |data| UploadJob.perform_async(data) }
|
64
|
-
# Attacher.delete { |data| DeleteJob.perform_async(data) }
|
65
|
-
# end
|
66
|
-
#
|
67
|
-
# If you would like to speed up your uploads and deletes, you can use the
|
68
|
-
# parallelize plugin, either as a replacement or an addition to
|
69
|
-
# background_helpers.
|
70
|
-
module BackgroundHelpers
|
71
|
-
module AttacherClassMethods
|
72
|
-
# If block is passed in, stores it to be called on promotion. Otherwise
|
73
|
-
# resolves data into objects and calls Attacher#promote.
|
74
|
-
def promote(data = nil, &block)
|
75
|
-
if block
|
76
|
-
shrine_class.opts[:background_promote] = block
|
77
|
-
else
|
78
|
-
record_class, record_id = data["record"]
|
79
|
-
record_class = Object.const_get(record_class)
|
80
|
-
record = find_record(record_class, record_id)
|
81
|
-
|
82
|
-
name = data["attachment"]
|
83
|
-
attacher = record.send("#{name}_attacher")
|
84
|
-
cached_file = attacher.uploaded_file(data["uploaded_file"])
|
85
|
-
|
86
|
-
attacher.promote(cached_file)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# If block is passed in, stores it to be called on deletion. Otherwise
|
91
|
-
# resolves data into objects and calls `Shrine#delete`.
|
92
|
-
def delete(data = nil, &block)
|
93
|
-
if block
|
94
|
-
shrine_class.opts[:background_delete] = block
|
95
|
-
else
|
96
|
-
record_class, record_id = data["record"]
|
97
|
-
record = Object.const_get(record_class).new
|
98
|
-
record.id = record_id
|
99
|
-
|
100
|
-
name, phase = data["attachment"], data["phase"]
|
101
|
-
attacher = record.send("#{name}_attacher")
|
102
|
-
uploaded_file = attacher.uploaded_file(data["uploaded_file"])
|
103
|
-
context = {name: name.to_sym, record: record, phase: phase.to_sym}
|
104
|
-
|
105
|
-
attacher.store.delete(uploaded_file, context)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
module AttacherMethods
|
111
|
-
# Calls the promoting block with the data if it's been registered.
|
112
|
-
def _promote
|
113
|
-
if background_promote = shrine_class.opts[:background_promote]
|
114
|
-
data = {
|
115
|
-
"uploaded_file" => get.to_json,
|
116
|
-
"record" => [record.class.to_s, record.id],
|
117
|
-
"attachment" => name,
|
118
|
-
}
|
119
|
-
|
120
|
-
instance_exec(data, &background_promote) if promote?(get)
|
121
|
-
else
|
122
|
-
super
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
private
|
127
|
-
|
128
|
-
# Calls the deleting block with the data if it's been registered.
|
129
|
-
def delete!(uploaded_file, phase:)
|
130
|
-
if background_delete = shrine_class.opts[:background_delete]
|
131
|
-
data = {
|
132
|
-
"uploaded_file" => uploaded_file.to_json,
|
133
|
-
"record" => [record.class.to_s, record.id],
|
134
|
-
"attachment" => name,
|
135
|
-
"phase" => phase,
|
136
|
-
}
|
137
|
-
|
138
|
-
instance_exec(data, &background_delete)
|
139
|
-
else
|
140
|
-
super(uploaded_file, phase: phase)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
register_plugin(:background_helpers, BackgroundHelpers)
|
147
|
-
end
|
148
|
-
end
|
1
|
+
require "shrine/plugins/backgrounding"
|
2
|
+
Shrine::Plugins.register_plugin(:background_helpers, Shrine::Plugins::Backgrounding)
|
@@ -0,0 +1,148 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The background_helpers plugin enables you to intercept phases of
|
4
|
+
# uploading and put them into background jobs. This doesn't require any
|
5
|
+
# additional columns.
|
6
|
+
#
|
7
|
+
# plugin :backgrounding
|
8
|
+
#
|
9
|
+
# ## Promoting
|
10
|
+
#
|
11
|
+
# If you're doing processing, or your `:store` is something other than
|
12
|
+
# Storage::FileSystem, it's recommended to put promoting (moving to store)
|
13
|
+
# into a background job. This plugin allows you to do that by calling
|
14
|
+
# `Shrine::Attacher.promote`:
|
15
|
+
#
|
16
|
+
# Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
|
17
|
+
#
|
18
|
+
# When you call `Shrine::Attacher.promote` with a block, it will save the
|
19
|
+
# block and call it on every promotion. Then in your background job you can
|
20
|
+
# again call `Shrine::Attacher.promote` with the data, and internally it
|
21
|
+
# will resolve all necessary objects, do the promoting and update the
|
22
|
+
# record.
|
23
|
+
#
|
24
|
+
# class UploadJob
|
25
|
+
# include Sidekiq::Worker
|
26
|
+
#
|
27
|
+
# def perform(data)
|
28
|
+
# Shrine::Attacher.promote(data)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Shrine automatically handles all concurrency issues, such as canceling
|
33
|
+
# promoting if the attachment has changed in the meanwhile.
|
34
|
+
#
|
35
|
+
# ## Deleting
|
36
|
+
#
|
37
|
+
# If your `:store` is something other than Storage::FileSystem, it's
|
38
|
+
# recommended to put deleting files into a background job. This plugin
|
39
|
+
# allows you to do that by calling `Shrine::Attacher.delete`:
|
40
|
+
#
|
41
|
+
# Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
42
|
+
#
|
43
|
+
# When you call `Shrine::Attacher.delete` with a block, it will save the
|
44
|
+
# block and call it on every delete. Then in your background job you can
|
45
|
+
# again call `Shrine::Attacher.delete` with the data, and internally it
|
46
|
+
# will resolve all necessary objects, and delete the file.
|
47
|
+
#
|
48
|
+
# class DeleteJob
|
49
|
+
# include Sidekiq::Worker
|
50
|
+
#
|
51
|
+
# def perform(data)
|
52
|
+
# Shrine::Attacher.delete(data)
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# ## Conclusion
|
57
|
+
#
|
58
|
+
# The examples above used Sidekiq, but obviously you can just as well use
|
59
|
+
# any other backgrounding library. Also, if you want you can use
|
60
|
+
# backgrounding just for certain uploaders:
|
61
|
+
#
|
62
|
+
# class ImageUploader < Shrine
|
63
|
+
# Attacher.promote { |data| UploadJob.perform_async(data) }
|
64
|
+
# Attacher.delete { |data| DeleteJob.perform_async(data) }
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# If you would like to speed up your uploads and deletes, you can use the
|
68
|
+
# parallelize plugin, either as a replacement or an addition to
|
69
|
+
# background_helpers.
|
70
|
+
module Backgrounding
|
71
|
+
module AttacherClassMethods
|
72
|
+
# If block is passed in, stores it to be called on promotion. Otherwise
|
73
|
+
# resolves data into objects and calls `Attacher#promote`.
|
74
|
+
def promote(data = nil, &block)
|
75
|
+
if block
|
76
|
+
shrine_class.opts[:backgrounding_promote] = block
|
77
|
+
else
|
78
|
+
record_class, record_id = data["record"]
|
79
|
+
record_class = Object.const_get(record_class)
|
80
|
+
record = find_record(record_class, record_id)
|
81
|
+
|
82
|
+
name = data["attachment"]
|
83
|
+
attacher = record.send("#{name}_attacher")
|
84
|
+
cached_file = attacher.uploaded_file(data["uploaded_file"])
|
85
|
+
|
86
|
+
attacher.promote(cached_file)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# If block is passed in, stores it to be called on deletion. Otherwise
|
91
|
+
# resolves data into objects and calls `Shrine#delete`.
|
92
|
+
def delete(data = nil, &block)
|
93
|
+
if block
|
94
|
+
shrine_class.opts[:backgrounding_delete] = block
|
95
|
+
else
|
96
|
+
record_class, record_id = data["record"]
|
97
|
+
record = Object.const_get(record_class).new
|
98
|
+
record.id = record_id
|
99
|
+
|
100
|
+
name, phase = data["attachment"], data["phase"]
|
101
|
+
attacher = record.send("#{name}_attacher")
|
102
|
+
uploaded_file = attacher.uploaded_file(data["uploaded_file"])
|
103
|
+
context = {name: name.to_sym, record: record, phase: phase.to_sym}
|
104
|
+
|
105
|
+
attacher.store.delete(uploaded_file, context)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module AttacherMethods
|
111
|
+
# Calls the promoting block with the data if it's been registered.
|
112
|
+
def _promote
|
113
|
+
if background_promote = shrine_class.opts[:backgrounding_promote]
|
114
|
+
data = {
|
115
|
+
"uploaded_file" => get.to_json,
|
116
|
+
"record" => [record.class.to_s, record.id],
|
117
|
+
"attachment" => name.to_s,
|
118
|
+
}
|
119
|
+
|
120
|
+
instance_exec(data, &background_promote) if promote?(get)
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Calls the deleting block with the data if it's been registered.
|
129
|
+
def delete!(uploaded_file, phase:)
|
130
|
+
if background_delete = shrine_class.opts[:backgrounding_delete]
|
131
|
+
data = {
|
132
|
+
"uploaded_file" => uploaded_file.to_json,
|
133
|
+
"record" => [record.class.to_s, record.id],
|
134
|
+
"attachment" => name.to_s,
|
135
|
+
"phase" => phase.to_s,
|
136
|
+
}
|
137
|
+
|
138
|
+
instance_exec(data, &background_delete)
|
139
|
+
else
|
140
|
+
super(uploaded_file, phase: phase)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
register_plugin(:backgrounding, Backgrounding)
|
147
|
+
end
|
148
|
+
end
|