shrine 2.4.1 → 2.5.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 +510 -416
- data/doc/carrierwave.md +4 -2
- data/doc/direct_s3.md +21 -2
- data/doc/multiple_files.md +118 -0
- data/doc/paperclip.md +4 -2
- data/doc/testing.md +18 -5
- data/lib/shrine.rb +3 -1
- data/lib/shrine/plugins/add_metadata.rb +50 -12
- data/lib/shrine/plugins/backgrounding.rb +103 -51
- data/lib/shrine/plugins/default_url.rb +32 -10
- data/lib/shrine/plugins/direct_upload.rb +3 -1
- data/lib/shrine/plugins/processing.rb +35 -16
- data/lib/shrine/plugins/rack_file.rb +54 -21
- data/lib/shrine/plugins/remove_invalid.rb +1 -0
- data/lib/shrine/plugins/store_dimensions.rb +14 -5
- data/lib/shrine/plugins/upload_options.rb +5 -0
- data/lib/shrine/plugins/validation_helpers.rb +32 -37
- data/lib/shrine/plugins/versions.rb +69 -47
- data/lib/shrine/storage/file_system.rb +3 -3
- data/lib/shrine/storage/linter.rb +1 -1
- data/lib/shrine/storage/s3.rb +8 -4
- data/lib/shrine/version.rb +2 -2
- metadata +3 -2
@@ -3,20 +3,40 @@ class Shrine
|
|
3
3
|
# The `default_url` plugin allows setting the URL which will be returned when
|
4
4
|
# the attachment is missing.
|
5
5
|
#
|
6
|
-
# plugin :default_url
|
7
|
-
#
|
6
|
+
# plugin :default_url
|
7
|
+
#
|
8
|
+
# Attacher.default_url do |options|
|
9
|
+
# "/#{name}/missing.jpg"
|
8
10
|
# end
|
9
11
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
+
# `Attacher#url` returns the default URL when attachment is missing. Any
|
13
|
+
# passed in URL options will be present in the `options` hash.
|
14
|
+
#
|
15
|
+
# attacher.url #=> "/avatar/missing.jpg"
|
16
|
+
# # or
|
17
|
+
# user.avatar_url #=> "/avatar/missing.jpg"
|
12
18
|
#
|
13
|
-
#
|
14
|
-
#
|
19
|
+
# The default URL block is evaluated in the context of an instance of
|
20
|
+
# `Shrine::Attacher`.
|
15
21
|
#
|
16
|
-
#
|
22
|
+
# Attacher.default_url do |options|
|
23
|
+
# self #=> #<Shrine::Attacher>
|
24
|
+
#
|
25
|
+
# name #=> :avatar
|
26
|
+
# record #=> #<User>
|
27
|
+
# end
|
17
28
|
module DefaultUrl
|
18
29
|
def self.configure(uploader, &block)
|
19
|
-
|
30
|
+
if block
|
31
|
+
uploader.opts[:default_url] = block
|
32
|
+
warn "Passing a block to default_url Shrine plugin is deprecated and will probably be removed in future versions of Shrine. Use `Attacher.default_url { ... }` instead."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module AttacherClassMethods
|
37
|
+
def default_url(&block)
|
38
|
+
shrine_class.opts[:default_url_block] = block
|
39
|
+
end
|
20
40
|
end
|
21
41
|
|
22
42
|
module AttacherMethods
|
@@ -28,12 +48,14 @@ class Shrine
|
|
28
48
|
|
29
49
|
def default_url(**options)
|
30
50
|
if default_url_block
|
31
|
-
|
51
|
+
instance_exec(options, &default_url_block)
|
52
|
+
elsif shrine_class.opts[:default_url]
|
53
|
+
shrine_class.opts[:default_url].call(context.merge(options){|k,old,new|old})
|
32
54
|
end
|
33
55
|
end
|
34
56
|
|
35
57
|
def default_url_block
|
36
|
-
shrine_class.opts[:
|
58
|
+
shrine_class.opts[:default_url_block]
|
37
59
|
end
|
38
60
|
end
|
39
61
|
end
|
@@ -251,7 +251,9 @@ class Shrine
|
|
251
251
|
if presign_location
|
252
252
|
presign_location.call(request)
|
253
253
|
else
|
254
|
-
|
254
|
+
extension = request.params["extension"]
|
255
|
+
extension.prepend(".") if extension && !extension.start_with?('.')
|
256
|
+
uploader.send(:generate_uid, nil) + extension.to_s
|
255
257
|
end
|
256
258
|
end
|
257
259
|
|
@@ -1,7 +1,25 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
|
-
#
|
4
|
-
#
|
3
|
+
# Shrine uploaders can define the `#process` method, which will get called
|
4
|
+
# whenever a file is uploaded. It is given the original file, and is
|
5
|
+
# expected to return the processed files.
|
6
|
+
#
|
7
|
+
# def process(io, context)
|
8
|
+
# # you can process the original file `io` and return processed file(s)
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# However, when handling files as attachments, the same file is uploaded
|
12
|
+
# to temporary and permanent storage. Since we only want to apply the same
|
13
|
+
# processing once, we need to branch based on the context.
|
14
|
+
#
|
15
|
+
# def process(io, context)
|
16
|
+
# if context[:action] == :store # promote phase
|
17
|
+
# # ...
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# The `processing` plugin simplifies this by allowing us to declaratively
|
22
|
+
# define file processing for specified actions.
|
5
23
|
#
|
6
24
|
# plugin :processing
|
7
25
|
#
|
@@ -9,27 +27,28 @@ class Shrine
|
|
9
27
|
# # ...
|
10
28
|
# end
|
11
29
|
#
|
12
|
-
#
|
13
|
-
# additional information about the upload. The result of the processing
|
14
|
-
# block should be an IO-like object, which will continue being uploaded
|
15
|
-
# instead of the original.
|
30
|
+
# An example of resizing an image using the [image_processing] library:
|
16
31
|
#
|
17
|
-
#
|
32
|
+
# include ImageProcessing::MiniMagick
|
33
|
+
#
|
34
|
+
# process(:store) do |io, context|
|
35
|
+
# resize_to_limit!(io.download, 800, 800)
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# The declarations are additive and inheritable, so for the same action you
|
18
39
|
# can declare multiple blocks, and they will be performed in the same order,
|
19
|
-
#
|
20
|
-
# in any block to signal that no processing was performed and that the
|
21
|
-
# original file should be used.
|
40
|
+
# with output from previous block being the input to next.
|
22
41
|
#
|
23
|
-
#
|
42
|
+
# You can manually trigger the defined processing via the uploader, you
|
43
|
+
# just need to specify `:action` to the name of your processing block:
|
24
44
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# # ...
|
28
|
-
# end
|
29
|
-
# end
|
45
|
+
# uploader.upload(file, action: :store) # process and upload
|
46
|
+
# uploader.process(file, action: :store) # only process
|
30
47
|
#
|
31
48
|
# If you want the result of processing to be multiple files, use the
|
32
49
|
# `versions` plugin.
|
50
|
+
#
|
51
|
+
# [image_processing]: https://github.com/janko-m/image_processing
|
33
52
|
module Processing
|
34
53
|
def self.configure(uploader)
|
35
54
|
uploader.opts[:processing] = {}
|
@@ -2,43 +2,76 @@ require "forwardable"
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `rack_file` plugin enables
|
6
|
-
#
|
5
|
+
# The `rack_file` plugin enables uploaders to accept Rack uploaded file
|
6
|
+
# hashes for uploading.
|
7
7
|
#
|
8
|
-
# rack_file
|
8
|
+
# plugin :rack_file
|
9
|
+
#
|
10
|
+
# When a file is uploaded to your Rack application using the
|
11
|
+
# `multipart/form-data` parameter encoding, Rack converts the uploaded file
|
12
|
+
# to a hash.
|
13
|
+
#
|
14
|
+
# params[:file] #=>
|
9
15
|
# # {
|
16
|
+
# # name: "file"
|
10
17
|
# # filename: "cats.png",
|
11
18
|
# # type: "image/png",
|
12
19
|
# # tempfile: #<Tempfile:/var/folders/3n/3asd/-Tmp-/RackMultipart201-1476-nfw2-0>,
|
13
20
|
# # head: "Content-Disposition: form-data; ...",
|
14
21
|
# # }
|
15
|
-
# user.avatar = rack_file
|
16
|
-
# user.avatar.original_filename #=> "cats.png"
|
17
|
-
# user.avatar.mime_type #=> "image/png"
|
18
22
|
#
|
19
|
-
#
|
20
|
-
# and
|
23
|
+
# Since Shrine only accepts IO objects, you would normally need to fetch
|
24
|
+
# the `:tempfile` object and pass it directly. This plugin enables the
|
25
|
+
# uploader and attacher to accept the Rack uploaded file hash as a whole,
|
26
|
+
# which is then internally converted into an IO object.
|
21
27
|
#
|
22
|
-
#
|
28
|
+
# uploader.upload(params[:file])
|
29
|
+
# # or
|
30
|
+
# attacher.assign(params[:file])
|
31
|
+
# # or
|
32
|
+
# user.avatar = params[:file]
|
23
33
|
#
|
24
|
-
#
|
34
|
+
# This especially convenient when doing mass attribute assignment with
|
35
|
+
# request parameters. It will also copy the received file information into
|
36
|
+
# metadata.
|
37
|
+
#
|
38
|
+
# uploaded_file = uploader.upload(params[:file])
|
39
|
+
# uploaded_file.original_filename #=> "cats.png"
|
40
|
+
# uploaded_file.mime_type #=> "image/png"
|
41
|
+
#
|
42
|
+
# Note that this plugin is not needed in Rails applications, as Rails
|
25
43
|
# already wraps Rack uploaded files in `ActionDispatch::Http::UploadedFile`.
|
26
44
|
module RackFile
|
27
|
-
module
|
28
|
-
#
|
29
|
-
#
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
45
|
+
module InstanceMethods
|
46
|
+
# If `io` is a Rack uploaded file hash, converts it to an IO-like
|
47
|
+
# object and calls `super`.
|
48
|
+
def upload(io, context = {})
|
49
|
+
super(convert_rack_file(io), context)
|
50
|
+
end
|
51
|
+
|
52
|
+
# If `io` is a Rack uploaded file hash, converts it to an IO-like
|
53
|
+
# object and calls `super`.
|
54
|
+
def store(io, context = {})
|
55
|
+
super(convert_rack_file(io), context)
|
36
56
|
end
|
37
57
|
|
38
58
|
private
|
39
59
|
|
40
|
-
|
41
|
-
|
60
|
+
# If given a Rack uploaded file hash, returns a
|
61
|
+
# `Shrine::Plugins::RackFile::UploadedFile` IO-like object wrapping that
|
62
|
+
# hash, otherwise returns the value unchanged.
|
63
|
+
def convert_rack_file(value)
|
64
|
+
if rack_file?(value)
|
65
|
+
UploadedFile.new(value)
|
66
|
+
else
|
67
|
+
value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns whether a given value is a Rack uploaded file hash, by
|
72
|
+
# checking whether it's a hash with `:tempfile` and `:name` keys.
|
73
|
+
def rack_file?(value)
|
74
|
+
value.is_a?(Hash) && value.key?(:tempfile) && value.key?(:name)
|
42
75
|
end
|
43
76
|
end
|
44
77
|
|
@@ -5,13 +5,18 @@ class Shrine
|
|
5
5
|
#
|
6
6
|
# plugin :store_dimensions
|
7
7
|
#
|
8
|
-
#
|
8
|
+
# It adds "width" and "height" metadata values to Shrine::UploadedFile,
|
9
|
+
# and creates `#width`, `#height` and `#dimensions` reader methods.
|
9
10
|
#
|
10
|
-
#
|
11
|
-
# uploaded_file = uploader.upload(File.open("image.jpg"))
|
11
|
+
# image = uploader.upload(file)
|
12
12
|
#
|
13
|
-
#
|
14
|
-
#
|
13
|
+
# image.metadata["width"] #=> 300
|
14
|
+
# image.metadata["height"] #=> 500
|
15
|
+
# # or
|
16
|
+
# image.width #=> 300
|
17
|
+
# image.height #=> 500
|
18
|
+
# # or
|
19
|
+
# image.dimensions #=> [300, 500]
|
15
20
|
#
|
16
21
|
# The fastimage gem has built-in protection against [image bombs]. However,
|
17
22
|
# if for some reason it doesn't suit your needs, you can provide a custom
|
@@ -77,6 +82,10 @@ class Shrine
|
|
77
82
|
def height
|
78
83
|
Integer(metadata["height"]) if metadata["height"]
|
79
84
|
end
|
85
|
+
|
86
|
+
def dimensions
|
87
|
+
[width, height] if width || height
|
88
|
+
end
|
80
89
|
end
|
81
90
|
end
|
82
91
|
|
@@ -15,6 +15,11 @@ class Shrine
|
|
15
15
|
# {acl: "private"}
|
16
16
|
# end
|
17
17
|
# end
|
18
|
+
#
|
19
|
+
# If you're uploading the file directly, you can also pass `:upload_options`
|
20
|
+
# to the uploader.
|
21
|
+
#
|
22
|
+
# uploader.upload(file, upload_options: {acl: "public-read"})
|
18
23
|
module UploadOptions
|
19
24
|
def self.configure(uploader, options = {})
|
20
25
|
uploader.opts[:upload_options] = (uploader.opts[:upload_options] || {}).merge(options)
|
@@ -3,19 +3,25 @@ class Shrine
|
|
3
3
|
# The `validation_helpers` plugin provides helper methods for validating
|
4
4
|
# attached files.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
# plugin :validation_helpers
|
6
|
+
# plugin :validation_helpers
|
8
7
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# end
|
8
|
+
# Attacher.validate do
|
9
|
+
# validat_mime_type_inclusion %w[image/jpeg image/png image/gif]
|
10
|
+
# validate_max_size 5*1024*1024 if record.guest?
|
13
11
|
# end
|
14
12
|
#
|
15
13
|
# The validation methods are instance-level, the `Attacher.validate` block
|
16
14
|
# is evaluated in context of an instance of `Shrine::Attacher`, so you can
|
17
15
|
# easily do conditional validation.
|
18
16
|
#
|
17
|
+
# The validation methods return whether the validation succeeded, allowing
|
18
|
+
# you to do conditional validation.
|
19
|
+
#
|
20
|
+
# if validate_mime_type_inclusion %w[image/jpeg image/png image/gif]
|
21
|
+
# validate_max_width 2000
|
22
|
+
# validate_max_height 2000
|
23
|
+
# end
|
24
|
+
#
|
19
25
|
# If you would like to change default validation error messages, you can
|
20
26
|
# pass in the `:default_messages` option to the plugin:
|
21
27
|
#
|
@@ -58,52 +64,40 @@ class Shrine
|
|
58
64
|
module AttacherMethods
|
59
65
|
# Validates that the file is not larger than `max`.
|
60
66
|
def validate_max_size(max, message: nil)
|
61
|
-
|
62
|
-
errors << error_message(:max_size, message, max)
|
63
|
-
end
|
67
|
+
get.size <= max or add_error(:max_size, message, max) && false
|
64
68
|
end
|
65
69
|
|
66
70
|
# Validates that the file is not smaller than `min`.
|
67
71
|
def validate_min_size(min, message: nil)
|
68
|
-
|
69
|
-
errors << error_message(:min_size, message, min)
|
70
|
-
end
|
72
|
+
get.size >= min or add_error(:min_size, message, min) && false
|
71
73
|
end
|
72
74
|
|
73
75
|
# Validates that the file is not wider than `max`. Requires the
|
74
76
|
# `store_dimensions` plugin.
|
75
77
|
def validate_max_width(max, message: nil)
|
76
78
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
|
77
|
-
|
78
|
-
errors << error_message(:max_width, message, max)
|
79
|
-
end
|
79
|
+
get.width <= max or add_error(:max_width, message, max) && false if get.width
|
80
80
|
end
|
81
81
|
|
82
82
|
# Validates that the file is not narrower than `min`. Requires the
|
83
83
|
# `store_dimensions` plugin.
|
84
84
|
def validate_min_width(min, message: nil)
|
85
85
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
|
86
|
-
|
87
|
-
errors << error_message(:min_width, message, min)
|
88
|
-
end
|
86
|
+
get.width >= min or add_error(:min_width, message, min) && false if get.width
|
89
87
|
end
|
90
88
|
|
91
89
|
# Validates that the file is not taller than `max`. Requires the
|
92
90
|
# `store_dimensions` plugin.
|
93
91
|
def validate_max_height(max, message: nil)
|
94
92
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
|
95
|
-
|
96
|
-
errors << error_message(:max_height, message, max)
|
97
|
-
end
|
93
|
+
get.height <= max or add_error(:max_height, message, max) && false if get.height
|
98
94
|
end
|
99
95
|
|
100
96
|
# Validates that the file is not shorter than `min`. Requires the
|
101
97
|
# `store_dimensions` plugin.
|
102
98
|
def validate_min_height(min, message: nil)
|
103
99
|
raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
|
104
|
-
|
105
|
-
errors << error_message(:min_height, message, min)
|
106
|
-
end
|
100
|
+
get.height >= min or add_error(:min_height, message, min) && false if get.height
|
107
101
|
end
|
108
102
|
|
109
103
|
# Validates that the MIME type is in the `whitelist`. The whitelist is
|
@@ -111,9 +105,8 @@ class Shrine
|
|
111
105
|
#
|
112
106
|
# validate_mime_type_inclusion ["audio/mp3", /\Avideo/]
|
113
107
|
def validate_mime_type_inclusion(whitelist, message: nil)
|
114
|
-
|
115
|
-
|
116
|
-
end
|
108
|
+
whitelist.any? { |mime_type| regex(mime_type) =~ get.mime_type.to_s } \
|
109
|
+
or add_error(:mime_type_inclusion, message, whitelist) && false
|
117
110
|
end
|
118
111
|
|
119
112
|
# Validates that the MIME type is not in the `blacklist`. The blacklist
|
@@ -121,9 +114,8 @@ class Shrine
|
|
121
114
|
#
|
122
115
|
# validate_mime_type_exclusion ["image/gif", /\Aaudio/]
|
123
116
|
def validate_mime_type_exclusion(blacklist, message: nil)
|
124
|
-
|
125
|
-
|
126
|
-
end
|
117
|
+
blacklist.none? { |mime_type| regex(mime_type) =~ get.mime_type.to_s } \
|
118
|
+
or add_error(:mime_type_exclusion, message, blacklist) && false
|
127
119
|
end
|
128
120
|
|
129
121
|
# Validates that the extension is in the `whitelist`. The whitelist
|
@@ -131,9 +123,8 @@ class Shrine
|
|
131
123
|
#
|
132
124
|
# validate_extension_inclusion [/\Ajpe?g\z/i]
|
133
125
|
def validate_extension_inclusion(whitelist, message: nil)
|
134
|
-
|
135
|
-
|
136
|
-
end
|
126
|
+
whitelist.any? { |extension| regex(extension) =~ get.extension.to_s } \
|
127
|
+
or add_error(:extension_inclusion, message, whitelist) && false
|
137
128
|
end
|
138
129
|
|
139
130
|
# Validates that the extension is not in the `blacklist`. The blacklist
|
@@ -141,16 +132,20 @@ class Shrine
|
|
141
132
|
#
|
142
133
|
# validate_extension_exclusion ["mov", /\Amp/i]
|
143
134
|
def validate_extension_exclusion(blacklist, message: nil)
|
144
|
-
|
145
|
-
|
146
|
-
end
|
135
|
+
blacklist.none? { |extension| regex(extension) =~ get.extension.to_s } \
|
136
|
+
or add_error(:extension_exclusion, message, blacklist) && false
|
147
137
|
end
|
148
138
|
|
149
139
|
private
|
150
140
|
|
151
141
|
# Converts a string to a regex.
|
152
142
|
def regex(value)
|
153
|
-
value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value)}\z/
|
143
|
+
value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value)}\z/i
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generates an error message and appends it to errors array.
|
147
|
+
def add_error(*args)
|
148
|
+
errors << error_message(*args)
|
154
149
|
end
|
155
150
|
|
156
151
|
# Returns the direct message if given, otherwise uses the default error
|