shrine 2.18.1 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -1
  3. data/README.md +96 -137
  4. data/doc/advantages.md +4 -4
  5. data/doc/attacher.md +1 -2
  6. data/doc/carrierwave.md +3 -2
  7. data/doc/creating_storages.md +0 -20
  8. data/doc/design.md +1 -1
  9. data/doc/metadata.md +62 -36
  10. data/doc/paperclip.md +7 -6
  11. data/doc/plugins/data_uri.md +50 -4
  12. data/doc/plugins/derivation_endpoint.md +24 -0
  13. data/doc/plugins/determine_mime_type.md +47 -5
  14. data/doc/plugins/infer_extension.md +45 -9
  15. data/doc/plugins/instrumentation.md +170 -0
  16. data/doc/plugins/presign_endpoint.md +1 -1
  17. data/doc/plugins/pretty_location.md +23 -0
  18. data/doc/plugins/remote_url.md +59 -8
  19. data/doc/plugins/signature.md +54 -7
  20. data/doc/plugins/store_dimensions.md +69 -4
  21. data/doc/plugins/upload_endpoint.md +2 -2
  22. data/doc/plugins/validation_helpers.md +71 -29
  23. data/doc/refile.md +1 -1
  24. data/doc/release_notes/2.18.0.md +2 -2
  25. data/doc/release_notes/2.19.0.md +263 -0
  26. data/doc/storage/file_system.md +26 -8
  27. data/doc/testing.md +10 -10
  28. data/lib/shrine.rb +32 -16
  29. data/lib/shrine/attacher.rb +3 -0
  30. data/lib/shrine/attachment.rb +3 -0
  31. data/lib/shrine/plugins/add_metadata.rb +12 -16
  32. data/lib/shrine/plugins/backup.rb +2 -0
  33. data/lib/shrine/plugins/copy.rb +2 -0
  34. data/lib/shrine/plugins/data_uri.rb +56 -28
  35. data/lib/shrine/plugins/derivation_endpoint.rb +61 -27
  36. data/lib/shrine/plugins/determine_mime_type.rb +27 -5
  37. data/lib/shrine/plugins/infer_extension.rb +26 -5
  38. data/lib/shrine/plugins/instrumentation.rb +300 -0
  39. data/lib/shrine/plugins/logging.rb +2 -0
  40. data/lib/shrine/plugins/moving.rb +2 -0
  41. data/lib/shrine/plugins/pretty_location.rb +21 -12
  42. data/lib/shrine/plugins/rack_file.rb +23 -18
  43. data/lib/shrine/plugins/refresh_metadata.rb +4 -4
  44. data/lib/shrine/plugins/remote_url.rb +42 -23
  45. data/lib/shrine/plugins/signature.rb +32 -1
  46. data/lib/shrine/plugins/store_dimensions.rb +54 -9
  47. data/lib/shrine/plugins/validation_helpers.rb +148 -47
  48. data/lib/shrine/storage/file_system.rb +32 -15
  49. data/lib/shrine/storage/linter.rb +0 -13
  50. data/lib/shrine/storage/s3.rb +2 -5
  51. data/lib/shrine/uploaded_file.rb +8 -0
  52. data/lib/shrine/version.rb +2 -2
  53. data/shrine.gemspec +18 -3
  54. metadata +58 -27
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Shrine.deprecation("The logging plugin has been deprecated in favor of instrumentation plugin. The logging plugin will be removed in Shrine 3.")
4
+
3
5
  require "logger"
4
6
  require "json"
5
7
  require "time"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Shrine.deprecation("The moving plugin has been deprecated in favor of the :move upload option for FileSystem storage. It will no longer be available in Shrine 3.")
4
+
3
5
  class Shrine
4
6
  module Plugins
5
7
  # Documentation lives in [doc/plugins/moving.md] on GitHub.
@@ -7,29 +7,38 @@ class Shrine
7
7
  # [doc/plugins/pretty_location.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/pretty_location.md
8
8
  module PrettyLocation
9
9
  def self.configure(uploader, opts = {})
10
- uploader.opts[:pretty_location_namespace] = opts.fetch(:namespace, uploader.opts[:pretty_location_namespace])
10
+ uploader.opts[:pretty_location] ||= { identifier: :id }
11
+ uploader.opts[:pretty_location].merge!(opts)
11
12
  end
12
13
 
13
14
  module InstanceMethods
14
15
  def generate_location(io, context)
15
- if context[:record]
16
- type = class_location(context[:record].class) if context[:record].class.name
17
- id = context[:record].id if context[:record].respond_to?(:id)
16
+ pretty_location(io, context)
17
+ end
18
+
19
+ def pretty_location(io, name: nil, record: nil, version: nil, identifier: nil, **)
20
+ if record
21
+ namespace = record_namespace(record)
22
+ identifier ||= record_identifier(record)
18
23
  end
19
- name = context[:name]
20
24
 
21
- dirname, slash, basename = super.rpartition("/")
22
- basename = "#{context[:version]}-#{basename}" if context[:version]
23
- original = dirname + slash + basename
25
+ basename = basic_location(io)
26
+ basename = "#{version}-#{basename}" if version
24
27
 
25
- [type, id, name, original].compact.join("/")
28
+ [*namespace, *identifier, *name, basename].join("/")
26
29
  end
27
30
 
28
31
  private
29
32
 
30
- def class_location(klass)
31
- parts = klass.name.downcase.split("::")
32
- if separator = opts[:pretty_location_namespace]
33
+ def record_identifier(record)
34
+ record.public_send(opts[:pretty_location][:identifier])
35
+ end
36
+
37
+ def record_namespace(record)
38
+ class_name = record.class.name or return
39
+ parts = class_name.downcase.split("::")
40
+
41
+ if separator = opts[:pretty_location][:namespace]
33
42
  parts.join(separator)
34
43
  else
35
44
  parts.last
@@ -19,7 +19,7 @@ class Shrine
19
19
  )
20
20
  end
21
21
 
22
- UploadedFile.new(hash)
22
+ Shrine::RackFile.new(hash)
23
23
  end
24
24
  end
25
25
 
@@ -77,27 +77,32 @@ class Shrine
77
77
  end
78
78
  end
79
79
 
80
- # This is used to wrap the Rack hash into an IO-like object which Shrine
81
- # can upload.
82
- class UploadedFile
83
- attr_reader :tempfile, :original_filename, :content_type
84
- alias :to_io :tempfile
80
+ end
85
81
 
86
- def initialize(hash)
87
- @tempfile = hash[:tempfile]
88
- @original_filename = hash[:filename]
89
- @content_type = hash[:type]
90
- end
82
+ register_plugin(:rack_file, RackFile)
83
+ end
91
84
 
92
- def path
93
- @tempfile.path
94
- end
85
+ # This is used to wrap the Rack hash into an IO-like object which Shrine
86
+ # can upload.
87
+ class RackFile
88
+ attr_reader :tempfile, :original_filename, :content_type
89
+ alias :to_io :tempfile
95
90
 
96
- extend Forwardable
97
- delegate [:read, :size, :rewind, :eof?, :close] => :@tempfile
98
- end
91
+ def initialize(hash)
92
+ @tempfile = hash[:tempfile]
93
+ @original_filename = hash[:filename]
94
+ @content_type = hash[:type]
99
95
  end
100
96
 
101
- register_plugin(:rack_file, RackFile)
97
+ def path
98
+ @tempfile.path
99
+ end
100
+
101
+ extend Forwardable
102
+ delegate [:read, :size, :rewind, :eof?, :close] => :@tempfile
102
103
  end
104
+
105
+ # backwards compatibility
106
+ Plugins::RackFile.const_set(:UploadedFile, RackFile)
107
+ Plugins::RackFile.deprecate_constant(:UploadedFile)
103
108
  end
@@ -7,12 +7,12 @@ class Shrine
7
7
  # [doc/plugins/refresh_metadata.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/refresh_metadata.md
8
8
  module RefreshMetadata
9
9
  module FileMethods
10
- def refresh_metadata!(context = {})
10
+ def refresh_metadata!(**context)
11
11
  refreshed_metadata =
12
- if @io
13
- uploader.extract_metadata(self, context)
12
+ if opened?
13
+ uploader.send(:get_metadata, self, metadata: true, **context)
14
14
  else
15
- open { uploader.extract_metadata(self, context) }
15
+ open { uploader.send(:get_metadata, self, metadata: true, **context) }
16
16
  end
17
17
 
18
18
  @data = @data.merge("metadata" => metadata.merge(refreshed_metadata))
@@ -8,12 +8,48 @@ class Shrine
8
8
  #
9
9
  # [doc/plugins/remote_url.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/remote_url.md
10
10
  module RemoteUrl
11
+ LOG_SUBSCRIBER = -> (event) do
12
+ Shrine.logger.info "Remote URL (#{event.duration}ms) – #{{
13
+ remote_url: event[:remote_url],
14
+ download_options: event[:download_options],
15
+ uploader: event[:uploader],
16
+ }.inspect}"
17
+ end
18
+
11
19
  def self.configure(uploader, opts = {})
12
- raise Error, "The :max_size option is required for remote_url plugin" if !opts.key?(:max_size) && !uploader.opts.key?(:remote_url_max_size)
20
+ uploader.opts[:remote_url] ||= { downloader: Down.method(:download), log_subscriber: LOG_SUBSCRIBER }
21
+ uploader.opts[:remote_url].merge!(opts)
22
+
23
+ unless uploader.opts[:remote_url].key?(:max_size)
24
+ fail Error, "The :max_size option is required for remote_url plugin"
25
+ end
26
+
27
+ # instrumentation plugin integration
28
+ if uploader.respond_to?(:subscribe)
29
+ uploader.subscribe(:remote_url, &uploader.opts[:remote_url][:log_subscriber])
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ # Downloads the file using the "down" gem or a custom downloader.
35
+ # Checks the file size and terminates the download early if the file
36
+ # is too big.
37
+ def remote_url(url, **options)
38
+ options = { max_size: opts[:remote_url][:max_size] }.merge(options)
39
+
40
+ instrument_remote_url(url, options) do
41
+ opts[:remote_url][:downloader].call(url, options)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Sends a `remote_url.shrine` event for instrumentation plugin.
48
+ def instrument_remote_url(url, options, &block)
49
+ return yield unless respond_to?(:instrument)
13
50
 
14
- uploader.opts[:remote_url_downloader] = opts.fetch(:downloader, uploader.opts.fetch(:remote_url_downloader, :open_uri))
15
- uploader.opts[:remote_url_max_size] = opts.fetch(:max_size, uploader.opts[:remote_url_max_size])
16
- uploader.opts[:remote_url_error_message] = opts.fetch(:error_message, uploader.opts[:remote_url_error_message])
51
+ instrument(:remote_url, remote_url: url, download_options: options, &block)
52
+ end
17
53
  end
18
54
 
19
55
  module AttachmentMethods
@@ -38,7 +74,7 @@ class Shrine
38
74
  return if url == "" || url.nil?
39
75
 
40
76
  begin
41
- downloaded_file = download(url, downloader)
77
+ downloaded_file = shrine_class.remote_url(url, downloader)
42
78
  rescue => error
43
79
  download_error = error
44
80
  end
@@ -64,25 +100,8 @@ class Shrine
64
100
 
65
101
  private
66
102
 
67
- # Downloads the file using the "down" gem or a custom downloader.
68
- # Checks the file size and terminates the download early if the file
69
- # is too big.
70
- def download(url, options)
71
- downloader = shrine_class.opts[:remote_url_downloader]
72
- downloader = method(:"download_with_#{downloader}") if downloader.is_a?(Symbol)
73
- max_size = shrine_class.opts[:remote_url_max_size]
74
-
75
- downloader.call(url, { max_size: max_size }.merge(options))
76
- end
77
-
78
- # We silence any download errors, because for the user's point of view
79
- # the download simply failed.
80
- def download_with_open_uri(url, options)
81
- Down.download(url, options)
82
- end
83
-
84
103
  def download_error_message(url, error)
85
- if message = shrine_class.opts[:remote_url_error_message]
104
+ if message = shrine_class.opts[:remote_url][:error_message]
86
105
  if message.respond_to?(:call)
87
106
  args = [url, error].take(message.arity.abs)
88
107
  message = message.call(*args)
@@ -6,11 +6,42 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/signature.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/signature.md
8
8
  module Signature
9
+ LOG_SUBSCRIBER = -> (event) do
10
+ Shrine.logger.info "Signature (#{event.duration}ms) – #{{
11
+ io: event[:io].class,
12
+ algorithm: event[:algorithm],
13
+ format: event[:format],
14
+ uploader: event[:uploader],
15
+ }.inspect}"
16
+ end
17
+
18
+ def self.configure(uploader, opts = {})
19
+ uploader.opts[:signature] ||= { log_subscriber: LOG_SUBSCRIBER }
20
+ uploader.opts[:signature].merge!(opts)
21
+
22
+ # instrumentation plugin integration
23
+ if uploader.respond_to?(:subscribe)
24
+ uploader.subscribe(:signature, &uploader.opts[:signature][:log_subscriber])
25
+ end
26
+ end
27
+
9
28
  module ClassMethods
10
29
  # Calculates `algorithm` hash of the contents of the IO object, and
11
30
  # encodes it into `format`.
12
31
  def calculate_signature(io, algorithm, format: :hex)
13
- SignatureCalculator.new(algorithm.downcase, format: format).call(io)
32
+ instrument_signature(io, algorithm, format) do
33
+ SignatureCalculator.new(algorithm.downcase, format: format).call(io)
34
+ end
35
+ end
36
+ alias signature calculate_signature
37
+
38
+ private
39
+
40
+ # Sends a `signature.shrine` event for instrumentation plugin.
41
+ def instrument_signature(io, algorithm, format, &block)
42
+ return yield unless respond_to?(:instrument)
43
+
44
+ instrument(:signature, io: io, algorithm: algorithm, format: format, &block)
14
45
  end
15
46
  end
16
47
 
@@ -6,23 +6,47 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/store_dimensions.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/store_dimensions.md
8
8
  module StoreDimensions
9
+ LOG_SUBSCRIBER = -> (event) do
10
+ Shrine.logger.info "Image Dimensions (#{event.duration}ms) – #{{
11
+ io: event[:io].class,
12
+ uploader: event[:uploader],
13
+ }.inspect}"
14
+ end
15
+
9
16
  def self.configure(uploader, opts = {})
10
- uploader.opts[:dimensions_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:dimensions_analyzer, :fastimage))
17
+ uploader.opts[:store_dimensions] ||= { analyzer: :fastimage, on_error: :warn, log_subscriber: LOG_SUBSCRIBER }
18
+ uploader.opts[:store_dimensions].merge!(opts)
19
+
20
+ # resolve error strategy
21
+ case uploader.opts[:store_dimensions][:on_error]
22
+ when :fail
23
+ uploader.opts[:store_dimensions][:on_error] = -> (error) { fail error }
24
+ when :warn
25
+ uploader.opts[:store_dimensions][:on_error] = -> (error) { Shrine.warn "Error occurred when attempting to extract image dimensions: #{error.inspect}" }
26
+ when :ignore
27
+ uploader.opts[:store_dimensions][:on_error] = -> (error) { }
28
+ end
29
+
30
+ # instrumentation plugin integration
31
+ if uploader.respond_to?(:subscribe)
32
+ uploader.subscribe(:image_dimensions, &uploader.opts[:store_dimensions][:log_subscriber])
33
+ end
11
34
  end
12
35
 
13
36
  module ClassMethods
14
37
  # Determines the dimensions of the IO object by calling the specified
15
38
  # analyzer.
16
39
  def extract_dimensions(io)
17
- analyzer = opts[:dimensions_analyzer]
40
+ analyzer = opts[:store_dimensions][:analyzer]
18
41
  analyzer = dimensions_analyzer(analyzer) if analyzer.is_a?(Symbol)
19
42
  args = [io, dimensions_analyzers].take(analyzer.arity.abs)
20
43
 
21
- dimensions = analyzer.call(*args)
44
+ dimensions = instrument_dimensions(io) { analyzer.call(*args) }
22
45
  io.rewind
23
46
 
24
47
  dimensions
25
48
  end
49
+ alias dimensions extract_dimensions
26
50
 
27
51
  # Returns a hash of built-in dimensions analyzers, where keys are
28
52
  # analyzer names and values are `#call`-able objects which accepts the
@@ -35,7 +59,18 @@ class Shrine
35
59
 
36
60
  # Returns callable dimensions analyzer object.
37
61
  def dimensions_analyzer(name)
38
- DimensionsAnalyzer.new(name).method(:call)
62
+ on_error = opts[:store_dimensions][:on_error]
63
+
64
+ DimensionsAnalyzer.new(name, on_error: on_error).method(:call)
65
+ end
66
+
67
+ private
68
+
69
+ # Sends a `image_dimensions.shrine` event for instrumentation plugin.
70
+ def instrument_dimensions(io, &block)
71
+ return yield unless respond_to?(:instrument)
72
+
73
+ instrument(:image_dimensions, io: io, &block)
39
74
  end
40
75
  end
41
76
 
@@ -77,10 +112,11 @@ class Shrine
77
112
  class DimensionsAnalyzer
78
113
  SUPPORTED_TOOLS = [:fastimage, :mini_magick, :ruby_vips]
79
114
 
80
- def initialize(tool)
115
+ def initialize(tool, on_error: method(:fail))
81
116
  raise Error, "unknown dimensions analyzer #{tool.inspect}, supported analyzers are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
82
117
 
83
- @tool = tool
118
+ @tool = tool
119
+ @on_error = on_error
84
120
  end
85
121
 
86
122
  def call(io)
@@ -93,19 +129,28 @@ class Shrine
93
129
 
94
130
  def extract_with_fastimage(io)
95
131
  require "fastimage"
96
- FastImage.size(io)
132
+ FastImage.size(io, raise_on_failure: true)
133
+ rescue FastImage::FastImageException => error
134
+ on_error(error)
97
135
  end
98
136
 
99
137
  def extract_with_mini_magick(io)
100
138
  require "mini_magick"
101
139
  Shrine.with_file(io) { |file| MiniMagick::Image.new(file.path).dimensions }
102
- rescue MiniMagick::Error
140
+ rescue MiniMagick::Error => error
141
+ on_error(error)
103
142
  end
104
143
 
105
144
  def extract_with_ruby_vips(io)
106
145
  require "vips"
107
146
  Shrine.with_file(io) { |file| Vips::Image.new_from_file(file.path).size }
108
- rescue Vips::Error
147
+ rescue Vips::Error => error
148
+ on_error(error)
149
+ end
150
+
151
+ def on_error(error)
152
+ @on_error.call(error)
153
+ nil
109
154
  end
110
155
  end
111
156
  end
@@ -12,16 +12,18 @@ class Shrine
12
12
  end
13
13
 
14
14
  DEFAULT_MESSAGES = {
15
- max_size: ->(max) { "is too large (max is #{PRETTY_FILESIZE.call(max)})" },
16
- min_size: ->(min) { "is too small (min is #{PRETTY_FILESIZE.call(min)})" },
17
- max_width: ->(max) { "is too wide (max is #{max} px)" },
18
- min_width: ->(min) { "is too narrow (min is #{min} px)" },
19
- max_height: ->(max) { "is too tall (max is #{max} px)" },
20
- min_height: ->(min) { "is too short (min is #{min} px)" },
21
- mime_type_inclusion: ->(list) { "isn't of allowed type (allowed types: #{list.join(", ")})" },
22
- mime_type_exclusion: ->(list) { "is of forbidden type" },
23
- extension_inclusion: ->(list) { "isn't of allowed format (allowed formats: #{list.join(", ")})" },
24
- extension_exclusion: ->(list) { "is of forbidden format" },
15
+ max_size: -> (max) { "size must not be greater than #{PRETTY_FILESIZE.call(max)}" },
16
+ min_size: -> (min) { "size must not be less than #{PRETTY_FILESIZE.call(min)}" },
17
+ max_width: -> (max) { "width must not be greater than #{max}px" },
18
+ min_width: -> (min) { "width must not be less than #{min}px" },
19
+ max_height: -> (max) { "height must not be greater than #{max}px" },
20
+ min_height: -> (min) { "height must not be less than #{min}px" },
21
+ max_dimensions: -> (dims) { "dimensions must not be greater than #{dims.join("x")}" },
22
+ min_dimensions: -> (dims) { "dimensions must not be less than #{dims.join("x")}" },
23
+ mime_type_inclusion: -> (list) { "type must be one of: #{list.join(", ")}" },
24
+ mime_type_exclusion: -> (list) { "type must not be one of: #{list.join(", ")}" },
25
+ extension_inclusion: -> (list) { "extension must be one of: #{list.join(", ")}" },
26
+ extension_exclusion: -> (list) { "extension must not be one of: #{list.join(", ")}" },
25
27
  }
26
28
 
27
29
  FILESIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"].freeze
@@ -45,96 +47,195 @@ class Shrine
45
47
  end
46
48
 
47
49
  module AttacherMethods
48
- # Validates that the file is not larger than `max`.
50
+ # Validates that the `size` metadata is not larger than `max`.
51
+ #
52
+ # validate_max_size 5*1024*1024
49
53
  def validate_max_size(max, message: nil)
50
- get.size <= max or add_error(:max_size, message, max) && false
54
+ validate_result(get.size <= max, :max_size, message, max)
51
55
  end
52
56
 
53
- # Validates that the file is not smaller than `min`.
57
+ # Validates that the `size` metadata is not smaller than `min`.
58
+ #
59
+ # validate_min_size 1024
54
60
  def validate_min_size(min, message: nil)
55
- get.size >= min or add_error(:min_size, message, min) && false
61
+ validate_result(get.size >= min, :min_size, message, min)
62
+ end
63
+
64
+ # Validates that the `size` metadata is in the given range.
65
+ #
66
+ # validate_size 1024..5*1024*1024
67
+ def validate_size(size_range)
68
+ min_size, max_size = size_range.begin, size_range.end
69
+
70
+ validate_min_size(min_size) && validate_max_size(max_size)
56
71
  end
57
72
 
58
- # Validates that the file is not wider than `max`. Requires the
59
- # `store_dimensions` plugin.
73
+
74
+ # Validates that the `width` metadata is not larger than `max`.
75
+ # Requires the `store_dimensions` plugin.
76
+ #
77
+ # validate_max_width 5000
60
78
  def validate_max_width(max, message: nil)
61
- raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
79
+ fail Error, ":store_dimensions plugin is required" unless get.respond_to?(:width)
62
80
  if get.width
63
- get.width <= max or add_error(:max_width, message, max) && false
81
+ validate_result(get.width <= max, :max_width, message, max)
64
82
  else
65
83
  Shrine.deprecation("Width of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if width is nil.")
66
84
  end
67
85
  end
68
86
 
69
- # Validates that the file is not narrower than `min`. Requires the
70
- # `store_dimensions` plugin.
87
+ # Validates that the `width` metadata is not smaller than `min`.
88
+ # Requires the `store_dimensions` plugin.
89
+ #
90
+ # validate_min_width 100
71
91
  def validate_min_width(min, message: nil)
72
- raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:width)
92
+ fail Error, ":store_dimensions plugin is required" unless get.respond_to?(:width)
73
93
  if get.width
74
- get.width >= min or add_error(:min_width, message, min) && false
94
+ validate_result(get.width >= min, :min_width, message, min)
75
95
  else
76
96
  Shrine.deprecation("Width of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if width is nil.")
77
97
  end
78
98
  end
79
99
 
80
- # Validates that the file is not taller than `max`. Requires the
81
- # `store_dimensions` plugin.
100
+ # Validates that the `width` metadata is in the given range.
101
+ #
102
+ # validate_width 100..5000
103
+ def validate_width(width_range)
104
+ min_width, max_width = width_range.begin, width_range.end
105
+
106
+ validate_min_width(min_width) && validate_max_width(max_width)
107
+ end
108
+
109
+
110
+ # Validates that the `height` metadata is not larger than `max`.
111
+ # Requires the `store_dimensions` plugin.
112
+ #
113
+ # validate_max_height 5000
82
114
  def validate_max_height(max, message: nil)
83
- raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
115
+ fail Error, ":store_dimensions plugin is required" unless get.respond_to?(:height)
84
116
  if get.height
85
- get.height <= max or add_error(:max_height, message, max) && false
117
+ validate_result(get.height <= max, :max_height, message, max)
86
118
  else
87
119
  Shrine.deprecation("Height of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if height is nil.")
88
120
  end
89
121
  end
90
122
 
91
- # Validates that the file is not shorter than `min`. Requires the
92
- # `store_dimensions` plugin.
123
+ # Validates that the `height` metadata is not smaller than `min`.
124
+ # Requires the `store_dimensions` plugin.
125
+ #
126
+ # validate_min_height 100
93
127
  def validate_min_height(min, message: nil)
94
- raise Error, ":store_dimensions plugin is required" if !get.respond_to?(:height)
128
+ fail Error, ":store_dimensions plugin is required" unless get.respond_to?(:height)
95
129
  if get.height
96
- get.height >= min or add_error(:min_height, message, min) && false
130
+ validate_result(get.height >= min, :min_height, message, min)
97
131
  else
98
132
  Shrine.deprecation("Height of the uploaded file is nil, and Shrine skipped the validation. In Shrine 3 the validation will fail if height is nil.")
99
133
  end
100
134
  end
101
135
 
102
- # Validates that the MIME type is in the given collection.
136
+ # Validates that the `height` metadata is in the given range.
137
+ #
138
+ # validate_height 100..5000
139
+ def validate_height(height_range)
140
+ min_height, max_height = height_range.begin, height_range.end
141
+
142
+ validate_min_height(min_height) && validate_max_height(max_height)
143
+ end
144
+
145
+ # Validates that the dimensions are not larger than specified.
146
+ #
147
+ # validate_max_dimensions [5000, 5000]
148
+ def validate_max_dimensions((max_width, max_height), message: nil)
149
+ fail Error, ":store_dimensions plugin is required" unless get.respond_to?(:width) && get.respond_to?(:height)
150
+ fail Error, "width or height metadata is nil" unless get.width && get.height
151
+
152
+ validate_result(
153
+ get.width <= max_width && get.height <= max_height,
154
+ :max_dimensions, message, [max_width, max_height]
155
+ )
156
+ end
157
+
158
+ # Validates that the dimensions are not smaller than specified.
159
+ #
160
+ # validate_max_dimensions [100, 100]
161
+ def validate_min_dimensions((min_width, min_height), message: nil)
162
+ fail Error, ":store_dimensions plugin is required" unless get.respond_to?(:width) && get.respond_to?(:height)
163
+ fail Error, "width or height metadata is nil" unless get.width && get.height
164
+
165
+ validate_result(
166
+ get.width >= min_width && get.height >= min_height,
167
+ :min_dimensions, message, [min_width, min_height]
168
+ )
169
+ end
170
+
171
+ # Validates that the dimensions are in the given range.
172
+ #
173
+ # validate_dimensions [100..5000, 100..5000]
174
+ def validate_dimensions((width_range, height_range))
175
+ min_dims = width_range.begin, height_range.begin
176
+ max_dims = width_range.end, height_range.end
177
+
178
+ validate_min_dimensions(min_dims) && validate_max_dimensions(max_dims)
179
+ end
180
+
181
+ # Validates that the `mime_type` metadata is included in the given
182
+ # list.
103
183
  #
104
184
  # validate_mime_type_inclusion %w[audio/mp3 audio/flac]
105
- def validate_mime_type_inclusion(whitelist, message: nil)
106
- whitelist.any? { |mime_type| regex(mime_type) =~ get.mime_type.to_s } \
107
- or add_error(:mime_type_inclusion, message, whitelist) && false
185
+ def validate_mime_type_inclusion(types, message: nil)
186
+ validate_result(
187
+ types.any? { |type| regex(type) =~ get.mime_type.to_s },
188
+ :mime_type_inclusion, message, types
189
+ )
108
190
  end
191
+ alias validate_mime_type validate_mime_type_inclusion
109
192
 
110
- # Validates that the MIME type is not in the given collection.
193
+ # Validates that the `mime_type` metadata is not included in the given
194
+ # list.
111
195
  #
112
196
  # validate_mime_type_exclusion %w[text/x-php]
113
- def validate_mime_type_exclusion(blacklist, message: nil)
114
- blacklist.none? { |mime_type| regex(mime_type) =~ get.mime_type.to_s } \
115
- or add_error(:mime_type_exclusion, message, blacklist) && false
197
+ def validate_mime_type_exclusion(types, message: nil)
198
+ validate_result(
199
+ types.none? { |type| regex(type) =~ get.mime_type.to_s },
200
+ :mime_type_exclusion, message, types
201
+ )
116
202
  end
117
203
 
118
- # Validates that the extension is in the given collection. Comparison
119
- # is case insensitive.
204
+ # Validates that the extension is included in the given list.
205
+ # Comparison is case insensitive.
120
206
  #
121
207
  # validate_extension_inclusion %w[jpg jpeg png gif]
122
- def validate_extension_inclusion(whitelist, message: nil)
123
- whitelist.any? { |extension| regex(extension) =~ get.extension.to_s } \
124
- or add_error(:extension_inclusion, message, whitelist) && false
208
+ def validate_extension_inclusion(extensions, message: nil)
209
+ validate_result(
210
+ extensions.any? { |extension| regex(extension) =~ get.extension.to_s },
211
+ :extension_inclusion, message, extensions
212
+ )
125
213
  end
214
+ alias validate_extension validate_extension_inclusion
126
215
 
127
- # Validates that the extension is not in the given collection.
216
+ # Validates that the extension is not included in the given list.
128
217
  # Comparison is case insensitive.
129
218
  #
130
219
  # validate_extension_exclusion %[php jar]
131
- def validate_extension_exclusion(blacklist, message: nil)
132
- blacklist.none? { |extension| regex(extension) =~ get.extension.to_s } \
133
- or add_error(:extension_exclusion, message, blacklist) && false
220
+ def validate_extension_exclusion(extensions, message: nil)
221
+ validate_result(
222
+ extensions.none? { |extension| regex(extension) =~ get.extension.to_s },
223
+ :extension_exclusion, message, extensions
224
+ )
134
225
  end
135
226
 
136
227
  private
137
228
 
229
+ # Adds an error if result is false and returns the result.
230
+ def validate_result(result, type, message, *args)
231
+ if result
232
+ true
233
+ else
234
+ add_error(type, message, *args)
235
+ false
236
+ end
237
+ end
238
+
138
239
  # Converts a string to a regex.
139
240
  def regex(value)
140
241
  if value.is_a?(Regexp)