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.

@@ -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 do |context|
7
- # "/#{context[:name]}/missing.jpg"
6
+ # plugin :default_url
7
+ #
8
+ # Attacher.default_url do |options|
9
+ # "/#{name}/missing.jpg"
8
10
  # end
9
11
  #
10
- # The default URL gets triggered when calling `<attachment>_url` on the
11
- # model:
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
- # user.avatar #=> nil
14
- # user.avatar_url # "/avatar/missing.jpg"
19
+ # The default URL block is evaluated in the context of an instance of
20
+ # `Shrine::Attacher`.
15
21
  #
16
- # Any additional URL options will be present in the `context` hash.
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
- uploader.opts[:default_url] = block if block
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
- default_url_block.call(context.merge(options){|k,old,new|old})
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[:default_url]
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
- uploader.send(:generate_uid, nil) + request.params["extension"].to_s
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
- # The `processing` plugin allows you to declaratively define file processing
4
- # for specified actions.
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
- # The `io` is the original file, while the `context` contains some
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
- # The declarations are additive and inherited, so for the same action you
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
- # where output from previous will be input to next. You can return `nil`
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
- # The `.process` call is just a shorthand for
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
- # def process(io, context)
26
- # if context[:action] == :store
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 models to accept Rack file hashes as
6
- # attachments.
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
- # Internally the plugin wraps the Rack file hash into an IO-like object,
20
- # and this is what is passed to `Shrine#upload`.
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
- # plugin :rack_file
28
+ # uploader.upload(params[:file])
29
+ # # or
30
+ # attacher.assign(params[:file])
31
+ # # or
32
+ # user.avatar = params[:file]
23
33
  #
24
- # Note that this plugin is not needed in Rails applications, because Rails
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 AttacherMethods
28
- # Checks whether a file is a Rack file hash, and in that case wraps the
29
- # hash in an IO-like object.
30
- def assign(value)
31
- if value.is_a?(Hash) && rack_file?(value)
32
- assign(UploadedFile.new(value))
33
- else
34
- super
35
- end
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
- def rack_file?(hash)
41
- hash.key?(:tempfile)
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
 
@@ -13,6 +13,7 @@ class Shrine
13
13
  if errors.any? && cached?
14
14
  _delete(get, action: :validate)
15
15
  _set(@old)
16
+ remove_instance_variable(:@old)
16
17
  end
17
18
  end
18
19
  end
@@ -5,13 +5,18 @@ class Shrine
5
5
  #
6
6
  # plugin :store_dimensions
7
7
  #
8
- # You can access the dimensions through `#width` and `#height` methods:
8
+ # It adds "width" and "height" metadata values to Shrine::UploadedFile,
9
+ # and creates `#width`, `#height` and `#dimensions` reader methods.
9
10
  #
10
- # uploader = Shrine.new(:store)
11
- # uploaded_file = uploader.upload(File.open("image.jpg"))
11
+ # image = uploader.upload(file)
12
12
  #
13
- # uploaded_file.width #=> 300
14
- # uploaded_file.height #=> 500
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
- # class ImageUploader < Shrine
7
- # plugin :validation_helpers
6
+ # plugin :validation_helpers
8
7
  #
9
- # Attacher.validate do
10
- # validat_mime_type_inclusion %w[image/jpeg image/png image/gif]
11
- # validate_max_size 5*1024*1024 if record.guest?
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
- if get.size > max
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
- if get.size < min
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
- if get.width && get.width > max
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
- if get.width && get.width < min
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
- if get.height && get.height > max
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
- if get.height && get.height < min
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
- if whitelist.none? { |mime_type| regex(mime_type) =~ get.mime_type.to_s }
115
- errors << error_message(:mime_type_inclusion, message, whitelist)
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
- if blacklist.any? { |mime_type| regex(mime_type) =~ get.mime_type.to_s }
125
- errors << error_message(:mime_type_exclusion, message, blacklist)
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
- if whitelist.none? { |extension| regex(extension) =~ get.extension.to_s }
135
- errors << error_message(:extension_inclusion, message, whitelist)
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
- if blacklist.any? { |extension| regex(extension) =~ get.extension.to_s }
145
- errors << error_message(:extension_exclusion, message, blacklist)
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