active_storage_validations 1.3.5 → 2.0.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.
- checksums.yaml +4 -4
- data/README.md +620 -279
- data/config/locales/da.yml +50 -29
- data/config/locales/de.yml +50 -29
- data/config/locales/en.yml +50 -29
- data/config/locales/es.yml +50 -29
- data/config/locales/fr.yml +50 -29
- data/config/locales/it.yml +50 -29
- data/config/locales/ja.yml +50 -29
- data/config/locales/nl.yml +50 -29
- data/config/locales/pl.yml +50 -29
- data/config/locales/pt-BR.yml +50 -29
- data/config/locales/ru.yml +50 -29
- data/config/locales/sv.yml +50 -29
- data/config/locales/tr.yml +50 -29
- data/config/locales/uk.yml +50 -29
- data/config/locales/vi.yml +50 -29
- data/config/locales/zh-CN.yml +50 -29
- data/lib/active_storage_validations/analyzer/audio_analyzer.rb +58 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +60 -0
- data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +12 -11
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +12 -12
- data/lib/active_storage_validations/analyzer/image_analyzer.rb +18 -46
- data/lib/active_storage_validations/analyzer/null_analyzer.rb +2 -2
- data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +61 -0
- data/lib/active_storage_validations/analyzer/video_analyzer.rb +130 -0
- data/lib/active_storage_validations/analyzer.rb +54 -1
- data/lib/active_storage_validations/aspect_ratio_validator.rb +154 -120
- data/lib/active_storage_validations/{base_size_validator.rb → base_comparison_validator.rb} +18 -16
- data/lib/active_storage_validations/content_type_validator.rb +51 -17
- data/lib/active_storage_validations/dimension_validator.rb +20 -19
- data/lib/active_storage_validations/duration_validator.rb +55 -0
- data/lib/active_storage_validations/extensors/asv_blob_metadatable.rb +24 -0
- data/lib/active_storage_validations/{marcel_extensor.rb → extensors/asv_marcelable.rb} +5 -0
- data/lib/active_storage_validations/limit_validator.rb +14 -2
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +1 -1
- data/lib/active_storage_validations/matchers/{base_size_validator_matcher.rb → base_comparison_validator_matcher.rb} +31 -25
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -3
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +1 -1
- data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +39 -0
- data/lib/active_storage_validations/matchers/{processable_image_validator_matcher.rb → processable_file_validator_matcher.rb} +5 -5
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +18 -2
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +18 -2
- data/lib/active_storage_validations/matchers.rb +4 -3
- data/lib/active_storage_validations/{processable_image_validator.rb → processable_file_validator.rb} +4 -3
- data/lib/active_storage_validations/railtie.rb +5 -0
- data/lib/active_storage_validations/shared/asv_active_storageable.rb +2 -2
- data/lib/active_storage_validations/shared/asv_analyzable.rb +38 -3
- data/lib/active_storage_validations/shared/asv_attachable.rb +36 -15
- data/lib/active_storage_validations/size_validator.rb +11 -3
- data/lib/active_storage_validations/total_size_validator.rb +9 -3
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +7 -3
- metadata +14 -8
- data/lib/active_storage_validations/content_type_spoof_detector.rb +0 -96
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveStorageValidations
|
4
|
-
# =
|
4
|
+
# = ActiveStorageValidations Image \Analyzer
|
5
5
|
#
|
6
6
|
# This is an abstract base class for image analyzers, which extract width and height from an image attachable.
|
7
7
|
#
|
@@ -9,68 +9,40 @@ module ActiveStorageValidations
|
|
9
9
|
#
|
10
10
|
# Example:
|
11
11
|
#
|
12
|
-
#
|
12
|
+
# ActiveStorageValidations::Analyzer::ImageAnalyzer::ImageMagick.new(attachable).metadata
|
13
13
|
# # => { width: 4104, height: 2736 }
|
14
14
|
class Analyzer::ImageAnalyzer < Analyzer
|
15
|
+
@@supported_analyzers = {}
|
16
|
+
|
15
17
|
def metadata
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
return {} unless analyzer_supported?
|
19
|
+
|
20
|
+
read_media do |media|
|
21
|
+
if rotated_image?(media)
|
22
|
+
{ width: media.height, height: media.width }
|
19
23
|
else
|
20
|
-
{ width:
|
24
|
+
{ width: media.width, height: media.height }
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
24
28
|
|
25
29
|
private
|
26
30
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
blob = @attachable.is_a?(String) ? ActiveStorage::Blob.find_signed!(@attachable) : @attachable
|
31
|
-
image_from_tempfile_path(tempfile, blob)
|
32
|
-
when Hash
|
33
|
-
io = @attachable[:io]
|
34
|
-
if io.is_a?(StringIO)
|
35
|
-
image_from_tempfile_path(tempfile, io)
|
36
|
-
else
|
37
|
-
File.open(io) do |file|
|
38
|
-
image_from_path(file.path)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
42
|
-
image_from_path(@attachable.path)
|
43
|
-
when File
|
44
|
-
supports_file_attachment? ? image_from_path(@attachable.path) : raise_rails_like_error(@attachable)
|
45
|
-
when Pathname
|
46
|
-
supports_pathname_attachment? ? image_from_path(@attachable.to_s) : raise_rails_like_error(@attachable)
|
47
|
-
else
|
48
|
-
raise_rails_like_error(@attachable)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def image_from_tempfile_path(tempfile, file_representation)
|
53
|
-
if file_representation.is_a?(ActiveStorage::Blob)
|
54
|
-
file_representation.download { |chunk| tempfile.write(chunk) }
|
31
|
+
def analyzer_supported?
|
32
|
+
if @@supported_analyzers.key?(self)
|
33
|
+
@@supported_analyzers.fetch(self)
|
55
34
|
else
|
56
|
-
|
57
|
-
file_representation.rewind
|
35
|
+
@@supported_analyzers[self] = supported?
|
58
36
|
end
|
59
|
-
|
60
|
-
tempfile.flush
|
61
|
-
tempfile.rewind
|
62
|
-
image_from_path(tempfile.path)
|
63
|
-
end
|
64
|
-
|
65
|
-
def read_image
|
66
|
-
raise NotImplementedError
|
67
37
|
end
|
68
38
|
|
69
|
-
|
39
|
+
# Override this method in a concrete subclass. Have it return true if the image is rotated.
|
40
|
+
def rotated_image?(media)
|
70
41
|
raise NotImplementedError
|
71
42
|
end
|
72
43
|
|
73
|
-
|
44
|
+
# Override this method in a concrete subclass. Have it return true if the analyzer is supported.
|
45
|
+
def supported?
|
74
46
|
raise NotImplementedError
|
75
47
|
end
|
76
48
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveStorageValidations
|
4
|
-
# =
|
4
|
+
# = ActiveStorageValidations Null Analyzer
|
5
5
|
#
|
6
6
|
# This is a fallback analyzer when the attachable media type is not supported
|
7
7
|
# by our gem.
|
8
8
|
#
|
9
9
|
# Example:
|
10
10
|
#
|
11
|
-
#
|
11
|
+
# ActiveStorageValidations::Analyzer::NullAnalyzer.new(attachable).metadata
|
12
12
|
# # => {}
|
13
13
|
class Analyzer::NullAnalyzer < Analyzer
|
14
14
|
def metadata
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
# ActiveStorageValidations:::ASVFFProbable
|
5
|
+
#
|
6
|
+
# Validator helper methods for analyzers using FFprobe.
|
7
|
+
module ASVFFProbable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def read_media
|
13
|
+
Tempfile.create(binmode: true) do |tempfile|
|
14
|
+
begin
|
15
|
+
if media(tempfile).present?
|
16
|
+
yield media(tempfile)
|
17
|
+
else
|
18
|
+
logger.info "Skipping file metadata analysis because ffprobe doesn't support the file"
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
ensure
|
22
|
+
tempfile.close
|
23
|
+
end
|
24
|
+
end
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
logger.info "Skipping file metadata analysis because ffprobe isn't installed"
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
def media_from_path(path)
|
31
|
+
instrument(File.basename(ffprobe_path)) do
|
32
|
+
stdout, _stderr, status = Open3.capture3(
|
33
|
+
ffprobe_path,
|
34
|
+
"-print_format", "json",
|
35
|
+
"-show_streams",
|
36
|
+
"-show_format",
|
37
|
+
"-v", "error",
|
38
|
+
path
|
39
|
+
)
|
40
|
+
|
41
|
+
status.success? ? JSON.parse(stdout) : nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def ffprobe_path
|
46
|
+
ActiveStorage.paths[:ffprobe] || "ffprobe"
|
47
|
+
end
|
48
|
+
|
49
|
+
def video_stream
|
50
|
+
@video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def audio_stream
|
54
|
+
@audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def streams
|
58
|
+
@streams ||= @media["streams"] || []
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require_relative 'shared/asv_ff_probable'
|
5
|
+
|
6
|
+
module ActiveStorageValidations
|
7
|
+
# = ActiveStorageValidations Video \Analyzer
|
8
|
+
#
|
9
|
+
# Extracts the following from a video attachable:
|
10
|
+
#
|
11
|
+
# * Width (pixels)
|
12
|
+
# * Height (pixels)
|
13
|
+
# * Duration (seconds)
|
14
|
+
# * Angle (degrees)
|
15
|
+
# * Audio (true if file has an audio channel, false if not)
|
16
|
+
# * Video (true if file has an video channel, false if not)
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
#
|
20
|
+
# ActiveStorageValidations::Analyzer::VideoAnalyzer.new(attachable).metadata
|
21
|
+
# # => { width: 640, height: 480, duration: 5.0, angle: 0, audio: true, video: true }
|
22
|
+
#
|
23
|
+
# When a video's angle is 90, -90, 270 or -270 degrees, its width and height are automatically swapped for convenience.
|
24
|
+
#
|
25
|
+
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
|
26
|
+
class Analyzer::VideoAnalyzer < Analyzer
|
27
|
+
include ASVFFProbable
|
28
|
+
|
29
|
+
def metadata
|
30
|
+
read_media do |media|
|
31
|
+
{
|
32
|
+
width: (Integer(width) if width),
|
33
|
+
height: (Integer(height) if height),
|
34
|
+
duration: duration,
|
35
|
+
angle: angle,
|
36
|
+
audio: audio?,
|
37
|
+
video: video?
|
38
|
+
}.compact
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def width
|
45
|
+
if rotated?
|
46
|
+
computed_height || encoded_height
|
47
|
+
else
|
48
|
+
encoded_width
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def height
|
53
|
+
if rotated?
|
54
|
+
encoded_width
|
55
|
+
else
|
56
|
+
computed_height || encoded_height
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def duration
|
61
|
+
duration = video_stream["duration"] || container["duration"]
|
62
|
+
Float(duration).round(1) if duration
|
63
|
+
end
|
64
|
+
|
65
|
+
def angle
|
66
|
+
if tags["rotate"]
|
67
|
+
Integer(tags["rotate"])
|
68
|
+
elsif display_matrix && display_matrix["rotation"]
|
69
|
+
Integer(display_matrix["rotation"])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def display_matrix
|
74
|
+
side_data.detect { |data| data["side_data_type"] == "Display Matrix" }
|
75
|
+
end
|
76
|
+
|
77
|
+
def display_aspect_ratio
|
78
|
+
if descriptor = video_stream["display_aspect_ratio"]
|
79
|
+
if terms = descriptor.split(":", 2)
|
80
|
+
numerator = Integer(terms[0])
|
81
|
+
denominator = Integer(terms[1])
|
82
|
+
|
83
|
+
[numerator, denominator] unless numerator == 0
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def rotated?
|
89
|
+
angle == 90 || angle == 270 || angle == -90 || angle == -270
|
90
|
+
end
|
91
|
+
|
92
|
+
def audio?
|
93
|
+
audio_stream.present?
|
94
|
+
end
|
95
|
+
|
96
|
+
def video?
|
97
|
+
video_stream.present?
|
98
|
+
end
|
99
|
+
|
100
|
+
def computed_height
|
101
|
+
if encoded_width && display_height_scale
|
102
|
+
encoded_width * display_height_scale
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def encoded_width
|
107
|
+
@encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
|
108
|
+
end
|
109
|
+
|
110
|
+
def encoded_height
|
111
|
+
@encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
|
112
|
+
end
|
113
|
+
|
114
|
+
def display_height_scale
|
115
|
+
@display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
|
116
|
+
end
|
117
|
+
|
118
|
+
def tags
|
119
|
+
@tags ||= video_stream["tags"] || {}
|
120
|
+
end
|
121
|
+
|
122
|
+
def side_data
|
123
|
+
@side_data ||= video_stream["side_data_list"] || {}
|
124
|
+
end
|
125
|
+
|
126
|
+
def container
|
127
|
+
@container ||= @media["format"] || {}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -7,7 +7,7 @@ module ActiveStorageValidations
|
|
7
7
|
# = Active Storage Validations \Analyzer
|
8
8
|
#
|
9
9
|
# This is an abstract base class for analyzers, which extract metadata from attachables.
|
10
|
-
# See ActiveStorageValidations::Analyzer::
|
10
|
+
# See ActiveStorageValidations::Analyzer::VideoAnalyzer for an example of a concrete subclass.
|
11
11
|
#
|
12
12
|
# Heavily (not to say 100%) inspired by Rails own ActiveStorage::Analyzer
|
13
13
|
class Analyzer
|
@@ -20,6 +20,11 @@ module ActiveStorageValidations
|
|
20
20
|
@attachable = attachable
|
21
21
|
end
|
22
22
|
|
23
|
+
# Override this method in a concrete subclass. Have it return a String content type.
|
24
|
+
def content_type
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
23
28
|
# Override this method in a concrete subclass. Have it return a Hash of metadata.
|
24
29
|
def metadata
|
25
30
|
raise NotImplementedError
|
@@ -27,6 +32,54 @@ module ActiveStorageValidations
|
|
27
32
|
|
28
33
|
private
|
29
34
|
|
35
|
+
# Override this method in a concrete subclass. Have it yield a media object.
|
36
|
+
def read_media
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def media(tempfile)
|
41
|
+
@media ||= case @attachable
|
42
|
+
when ActiveStorage::Blob, String
|
43
|
+
blob = @attachable.is_a?(String) ? ActiveStorage::Blob.find_signed!(@attachable) : @attachable
|
44
|
+
media_from_tempfile_path(tempfile, blob)
|
45
|
+
when Hash
|
46
|
+
io = @attachable[:io]
|
47
|
+
if io.is_a?(StringIO)
|
48
|
+
media_from_tempfile_path(tempfile, io)
|
49
|
+
else
|
50
|
+
File.open(io) do |file|
|
51
|
+
media_from_path(file.path)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
55
|
+
media_from_path(@attachable.path)
|
56
|
+
when File
|
57
|
+
supports_file_attachment? ? media_from_path(@attachable.path) : raise_rails_like_error(@attachable)
|
58
|
+
when Pathname
|
59
|
+
supports_pathname_attachment? ? media_from_path(@attachable.to_s) : raise_rails_like_error(@attachable)
|
60
|
+
else
|
61
|
+
raise_rails_like_error(@attachable)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def media_from_tempfile_path(tempfile, file_representation)
|
66
|
+
if file_representation.is_a?(ActiveStorage::Blob)
|
67
|
+
file_representation.download { |chunk| tempfile.write(chunk) }
|
68
|
+
else
|
69
|
+
IO.copy_stream(file_representation, tempfile)
|
70
|
+
file_representation.rewind
|
71
|
+
end
|
72
|
+
|
73
|
+
tempfile.flush
|
74
|
+
tempfile.rewind
|
75
|
+
media_from_path(tempfile.path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Override this method in a concrete subclass. Have it return a media object.
|
79
|
+
def media_from_path(path)
|
80
|
+
raise NotImplementedError
|
81
|
+
end
|
82
|
+
|
30
83
|
def instrument(analyzer, &block)
|
31
84
|
ActiveSupport::Notifications.instrument("analyze.active_storage_validations", analyzer: analyzer, &block)
|
32
85
|
end
|
@@ -1,120 +1,154 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'shared/asv_active_storageable'
|
4
|
-
require_relative 'shared/asv_analyzable'
|
5
|
-
require_relative 'shared/asv_attachable'
|
6
|
-
require_relative 'shared/asv_errorable'
|
7
|
-
require_relative 'shared/asv_optionable'
|
8
|
-
require_relative 'shared/asv_symbolizable'
|
9
|
-
|
10
|
-
module ActiveStorageValidations
|
11
|
-
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
12
|
-
include ASVActiveStorageable
|
13
|
-
include ASVAnalyzable
|
14
|
-
include ASVAttachable
|
15
|
-
include ASVErrorable
|
16
|
-
include ASVOptionable
|
17
|
-
include ASVSymbolizable
|
18
|
-
|
19
|
-
AVAILABLE_CHECKS = %i[with].freeze
|
20
|
-
NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
|
21
|
-
ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
|
22
|
-
ERROR_TYPES = %i[
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
errors_options =
|
70
|
-
errors_options[:
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
errors_options
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_analyzable'
|
5
|
+
require_relative 'shared/asv_attachable'
|
6
|
+
require_relative 'shared/asv_errorable'
|
7
|
+
require_relative 'shared/asv_optionable'
|
8
|
+
require_relative 'shared/asv_symbolizable'
|
9
|
+
|
10
|
+
module ActiveStorageValidations
|
11
|
+
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
12
|
+
include ASVActiveStorageable
|
13
|
+
include ASVAnalyzable
|
14
|
+
include ASVAttachable
|
15
|
+
include ASVErrorable
|
16
|
+
include ASVOptionable
|
17
|
+
include ASVSymbolizable
|
18
|
+
|
19
|
+
AVAILABLE_CHECKS = %i[with in].freeze
|
20
|
+
NAMED_ASPECT_RATIOS = %i[square portrait landscape].freeze
|
21
|
+
ASPECT_RATIO_REGEX = /is_([1-9]\d*)_([1-9]\d*)/.freeze
|
22
|
+
ERROR_TYPES = %i[
|
23
|
+
aspect_ratio_not_square
|
24
|
+
aspect_ratio_not_portrait
|
25
|
+
aspect_ratio_not_landscape
|
26
|
+
aspect_ratio_not_x_y
|
27
|
+
aspect_ratio_invalid
|
28
|
+
media_metadata_missing
|
29
|
+
].freeze
|
30
|
+
PRECISION = 3.freeze
|
31
|
+
METADATA_KEYS = %i[width height].freeze
|
32
|
+
|
33
|
+
def check_validity!
|
34
|
+
ensure_at_least_one_validator_option
|
35
|
+
ensure_aspect_ratio_validity
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_each(record, attribute, _value)
|
39
|
+
return if no_attachments?(record, attribute)
|
40
|
+
|
41
|
+
flat_options = set_flat_options(record)
|
42
|
+
@authorized_aspect_ratios = authorized_aspect_ratios_from_options(flat_options).compact
|
43
|
+
return if @authorized_aspect_ratios.empty?
|
44
|
+
|
45
|
+
validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def is_valid?(record, attribute, attachable, metadata)
|
51
|
+
!media_metadata_missing?(record, attribute, attachable, metadata) &&
|
52
|
+
authorized_aspect_ratio?(record, attribute, attachable, metadata)
|
53
|
+
end
|
54
|
+
|
55
|
+
def authorized_aspect_ratio?(record, attribute, attachable, metadata)
|
56
|
+
attachable_aspect_ratio_is_authorized = @authorized_aspect_ratios.any? do |authorized_aspect_ratio|
|
57
|
+
case authorized_aspect_ratio
|
58
|
+
when :square then valid_square_aspect_ratio?(metadata)
|
59
|
+
when :portrait then valid_portrait_aspect_ratio?(metadata)
|
60
|
+
when :landscape then valid_landscape_aspect_ratio?(metadata)
|
61
|
+
when ASPECT_RATIO_REGEX then valid_regex_aspect_ratio?(authorized_aspect_ratio, metadata)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
return true if attachable_aspect_ratio_is_authorized
|
66
|
+
|
67
|
+
errors_options = initialize_error_options(options, attachable)
|
68
|
+
error_type = aspect_ratio_error_mapping
|
69
|
+
errors_options[:authorized_aspect_ratios] = string_aspect_ratios
|
70
|
+
errors_options[:width] = metadata[:width]
|
71
|
+
errors_options[:height] = metadata[:height]
|
72
|
+
add_error(record, attribute, error_type, **errors_options)
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def aspect_ratio_error_mapping
|
77
|
+
return :aspect_ratio_invalid if @authorized_aspect_ratios.many?
|
78
|
+
|
79
|
+
aspect_ratio = @authorized_aspect_ratios.first
|
80
|
+
NAMED_ASPECT_RATIOS.include?(aspect_ratio) ? :"aspect_ratio_not_#{aspect_ratio}" : :aspect_ratio_not_x_y
|
81
|
+
end
|
82
|
+
|
83
|
+
def media_metadata_missing?(record, attribute, attachable, metadata)
|
84
|
+
return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
|
85
|
+
|
86
|
+
errors_options = initialize_error_options(options, attachable)
|
87
|
+
errors_options[:authorized_aspect_ratios] = string_aspect_ratios
|
88
|
+
add_error(record, attribute, :media_metadata_missing, **errors_options)
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
def valid_square_aspect_ratio?(metadata)
|
93
|
+
metadata[:width] == metadata[:height]
|
94
|
+
end
|
95
|
+
|
96
|
+
def valid_portrait_aspect_ratio?(metadata)
|
97
|
+
metadata[:width] < metadata[:height]
|
98
|
+
end
|
99
|
+
|
100
|
+
def valid_landscape_aspect_ratio?(metadata)
|
101
|
+
metadata[:width] > metadata[:height]
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid_regex_aspect_ratio?(aspect_ratio, metadata)
|
105
|
+
aspect_ratio =~ ASPECT_RATIO_REGEX
|
106
|
+
x = ::Regexp.last_match(1).to_i
|
107
|
+
y = ::Regexp.last_match(2).to_i
|
108
|
+
|
109
|
+
x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
|
110
|
+
end
|
111
|
+
|
112
|
+
def ensure_at_least_one_validator_option
|
113
|
+
return if AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
114
|
+
|
115
|
+
raise ArgumentError, 'You must pass either :with or :in to the validator'
|
116
|
+
end
|
117
|
+
|
118
|
+
def ensure_aspect_ratio_validity
|
119
|
+
return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
|
120
|
+
|
121
|
+
authorized_aspect_ratios_from_options(options).each do |aspect_ratio|
|
122
|
+
unless NAMED_ASPECT_RATIOS.include?(aspect_ratio) || aspect_ratio =~ ASPECT_RATIO_REGEX
|
123
|
+
raise ArgumentError, invalid_aspect_ratio_message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def invalid_aspect_ratio_message
|
129
|
+
<<~ERROR_MESSAGE
|
130
|
+
You must pass a valid aspect ratio to the validator
|
131
|
+
It should either be a named aspect ratio (#{NAMED_ASPECT_RATIOS.join(', ')})
|
132
|
+
Or an aspect ratio like 'is_16_9' (matching /#{ASPECT_RATIO_REGEX.source}/)
|
133
|
+
ERROR_MESSAGE
|
134
|
+
end
|
135
|
+
|
136
|
+
def authorized_aspect_ratios_from_options(flat_options)
|
137
|
+
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in]))
|
138
|
+
end
|
139
|
+
|
140
|
+
def string_aspect_ratios
|
141
|
+
@authorized_aspect_ratios.map do |aspect_ratio|
|
142
|
+
if NAMED_ASPECT_RATIOS.include?(aspect_ratio)
|
143
|
+
aspect_ratio
|
144
|
+
else
|
145
|
+
aspect_ratio =~ ASPECT_RATIO_REGEX
|
146
|
+
x = ::Regexp.last_match(1).to_i
|
147
|
+
y = ::Regexp.last_match(2).to_i
|
148
|
+
|
149
|
+
"#{x}:#{y}"
|
150
|
+
end
|
151
|
+
end.join(', ')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|