active_storage_validations 1.3.4 → 1.3.5
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 +2 -2
- data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +46 -0
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +57 -0
- data/lib/active_storage_validations/analyzer/image_analyzer.rb +77 -0
- data/lib/active_storage_validations/analyzer/null_analyzer.rb +18 -0
- data/lib/active_storage_validations/analyzer.rb +34 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +3 -1
- data/lib/active_storage_validations/content_type_spoof_detector.rb +2 -0
- data/lib/active_storage_validations/content_type_validator.rb +3 -1
- data/lib/active_storage_validations/dimension_validator.rb +2 -0
- data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +4 -4
- data/lib/active_storage_validations/matchers.rb +2 -1
- data/lib/active_storage_validations/processable_image_validator.rb +2 -0
- data/lib/active_storage_validations/shared/asv_analyzable.rb +45 -0
- data/lib/active_storage_validations/shared/asv_attachable.rb +63 -16
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +6 -0
- metadata +8 -3
- data/lib/active_storage_validations/metadata.rb +0 -179
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fe705d1a3b76d875ddc0a61d7d3caa439ddad3ba4a8b5be3c203f8d8cdd00ca
|
4
|
+
data.tar.gz: 826dbac96af4e1ed424ad16670aea3e428296ab07ae9bfdad29e4c0b453d114d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4ccb7a673c9c5a7d6508e49c6b4b92b50b08faa9395a6e6c7da3bef460a6b72b64900d2ce7e529c5f18224c5b96e23bf82e8142f5d6dc047c327216b117bff21
|
7
|
+
data.tar.gz: f29bcdf4a6142b99d1cabf53086b682925256afc433e106a52feca3e96f097203c5e27ed7419ea51fc9b4ee6a2c454b06b0cdbb56a1d3f890fa6f452b529be4f
|
data/README.md
CHANGED
@@ -354,8 +354,8 @@ describe User do
|
|
354
354
|
|
355
355
|
# limit
|
356
356
|
# #min, #max
|
357
|
-
it { is_expected.to
|
358
|
-
it { is_expected.to
|
357
|
+
it { is_expected.to validate_limits_of(:avatar).min(1) }
|
358
|
+
it { is_expected.to validate_limits_of(:avatar).max(5) }
|
359
359
|
|
360
360
|
# content_type:
|
361
361
|
# #allowing, #rejecting
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem.
|
5
|
+
# MiniMagick requires the {ImageMagick}[http://www.imagemagick.org] system library.
|
6
|
+
# This is the default Rails image analyzer.
|
7
|
+
class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def read_image
|
12
|
+
begin
|
13
|
+
require "mini_magick" unless defined?(MiniMagick)
|
14
|
+
rescue LoadError
|
15
|
+
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
16
|
+
return {}
|
17
|
+
end
|
18
|
+
|
19
|
+
Tempfile.create(binmode: true) do |tempfile|
|
20
|
+
begin
|
21
|
+
if image(tempfile).valid?
|
22
|
+
yield image(tempfile)
|
23
|
+
else
|
24
|
+
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
ensure
|
28
|
+
tempfile.close
|
29
|
+
end
|
30
|
+
end
|
31
|
+
rescue MiniMagick::Error => error
|
32
|
+
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
|
36
|
+
def image_from_path(path)
|
37
|
+
instrument("mini_magick") do
|
38
|
+
MiniMagick::Image.new(path)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def rotated_image?(image)
|
43
|
+
%w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
# This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem.
|
5
|
+
# Ruby-vips requires the {libvips}[https://libvips.github.io/libvips/] system library.
|
6
|
+
class Analyzer::ImageAnalyzer::Vips < Analyzer::ImageAnalyzer
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def read_image
|
11
|
+
begin
|
12
|
+
require "vips" unless defined?(::Vips)
|
13
|
+
rescue LoadError
|
14
|
+
logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
|
15
|
+
return {}
|
16
|
+
end
|
17
|
+
|
18
|
+
Tempfile.create(binmode: true) do |tempfile|
|
19
|
+
begin
|
20
|
+
if image(tempfile)
|
21
|
+
yield image(tempfile)
|
22
|
+
else
|
23
|
+
logger.info "Skipping image analysis because Vips doesn't support the file"
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
tempfile.close
|
28
|
+
end
|
29
|
+
end
|
30
|
+
rescue ::Vips::Error => error
|
31
|
+
logger.error "Skipping image analysis due to a Vips error: #{error.message}"
|
32
|
+
{}
|
33
|
+
end
|
34
|
+
|
35
|
+
def image_from_path(path)
|
36
|
+
instrument("vips") do
|
37
|
+
begin
|
38
|
+
::Vips::Image.new_from_file(path, access: :sequential)
|
39
|
+
rescue ::Vips::Error
|
40
|
+
# Vips throw errors rather than returning false when reading a not
|
41
|
+
# supported attachable.
|
42
|
+
# We stumbled upon this issue while reading 0 byte size attachable
|
43
|
+
# https://github.com/janko/image_processing/issues/97
|
44
|
+
logger.info "Skipping image analysis because Vips doesn't support the file"
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
|
51
|
+
def rotated_image?(image)
|
52
|
+
ROTATIONS === image.get("exif-ifd0-Orientation")
|
53
|
+
rescue ::Vips::Error
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
# = Active Storage Image \Analyzer
|
5
|
+
#
|
6
|
+
# This is an abstract base class for image analyzers, which extract width and height from an image attachable.
|
7
|
+
#
|
8
|
+
# If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(attachable).metadata
|
13
|
+
# # => { width: 4104, height: 2736 }
|
14
|
+
class Analyzer::ImageAnalyzer < Analyzer
|
15
|
+
def metadata
|
16
|
+
read_image do |image|
|
17
|
+
if rotated_image?(image)
|
18
|
+
{ width: image.height, height: image.width }
|
19
|
+
else
|
20
|
+
{ width: image.width, height: image.height }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def image(tempfile)
|
28
|
+
case @attachable
|
29
|
+
when ActiveStorage::Blob, String
|
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) }
|
55
|
+
else
|
56
|
+
IO.copy_stream(file_representation, tempfile)
|
57
|
+
file_representation.rewind
|
58
|
+
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
|
+
end
|
68
|
+
|
69
|
+
def image_from_path(path)
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
def rotated_image?(image)
|
74
|
+
raise NotImplementedError
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
# = Active Storage Null Analyzer
|
5
|
+
#
|
6
|
+
# This is a fallback analyzer when the attachable media type is not supported
|
7
|
+
# by our gem.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# ActiveStorage::Analyzer::NullAnalyzer.new(attachable).metadata
|
12
|
+
# # => {}
|
13
|
+
class Analyzer::NullAnalyzer < Analyzer
|
14
|
+
def metadata
|
15
|
+
{}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'shared/asv_attachable'
|
4
|
+
require_relative 'shared/asv_loggable'
|
5
|
+
|
6
|
+
module ActiveStorageValidations
|
7
|
+
# = Active Storage Validations \Analyzer
|
8
|
+
#
|
9
|
+
# This is an abstract base class for analyzers, which extract metadata from attachables.
|
10
|
+
# See ActiveStorageValidations::Analyzer::ImageAnalyzer for an example of a concrete subclass.
|
11
|
+
#
|
12
|
+
# Heavily (not to say 100%) inspired by Rails own ActiveStorage::Analyzer
|
13
|
+
class Analyzer
|
14
|
+
include ASVAttachable
|
15
|
+
include ASVLoggable
|
16
|
+
|
17
|
+
attr_reader :attachable
|
18
|
+
|
19
|
+
def initialize(attachable)
|
20
|
+
@attachable = attachable
|
21
|
+
end
|
22
|
+
|
23
|
+
# Override this method in a concrete subclass. Have it return a Hash of metadata.
|
24
|
+
def metadata
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def instrument(analyzer, &block)
|
31
|
+
ActiveSupport::Notifications.instrument("analyze.active_storage_validations", analyzer: analyzer, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_analyzable'
|
4
5
|
require_relative 'shared/asv_attachable'
|
5
6
|
require_relative 'shared/asv_errorable'
|
6
7
|
require_relative 'shared/asv_optionable'
|
@@ -9,6 +10,7 @@ require_relative 'shared/asv_symbolizable'
|
|
9
10
|
module ActiveStorageValidations
|
10
11
|
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
11
12
|
include ASVActiveStorageable
|
13
|
+
include ASVAnalyzable
|
12
14
|
include ASVAttachable
|
13
15
|
include ASVErrorable
|
14
16
|
include ASVOptionable
|
@@ -53,7 +55,7 @@ module ActiveStorageValidations
|
|
53
55
|
end
|
54
56
|
|
55
57
|
def image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
|
56
|
-
return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
|
58
|
+
return false if metadata.present? && metadata[:width].to_i > 0 && metadata[:height].to_i > 0
|
57
59
|
|
58
60
|
errors_options = initialize_error_options(options, attachable)
|
59
61
|
errors_options[:aspect_ratio] = flat_options[:with]
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'shared/asv_analyzable'
|
3
4
|
require_relative 'shared/asv_attachable'
|
4
5
|
require_relative 'shared/asv_loggable'
|
5
6
|
require 'open3'
|
@@ -8,6 +9,7 @@ module ActiveStorageValidations
|
|
8
9
|
class ContentTypeSpoofDetector
|
9
10
|
class FileCommandLineToolNotInstalledError < StandardError; end
|
10
11
|
|
12
|
+
include ASVAnalyzable
|
11
13
|
include ASVAttachable
|
12
14
|
include ASVLoggable
|
13
15
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_analyzable'
|
4
5
|
require_relative 'shared/asv_attachable'
|
5
6
|
require_relative 'shared/asv_errorable'
|
6
7
|
require_relative 'shared/asv_optionable'
|
@@ -10,6 +11,7 @@ require_relative 'content_type_spoof_detector'
|
|
10
11
|
module ActiveStorageValidations
|
11
12
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
12
13
|
include ASVActiveStorageable
|
14
|
+
include ASVAnalyzable
|
13
15
|
include ASVAttachable
|
14
16
|
include ASVErrorable
|
15
17
|
include ASVOptionable
|
@@ -52,7 +54,7 @@ module ActiveStorageValidations
|
|
52
54
|
end
|
53
55
|
|
54
56
|
def set_attachable_cached_values(attachable)
|
55
|
-
@attachable_content_type =
|
57
|
+
@attachable_content_type = attachable_content_type_rails_like(attachable)
|
56
58
|
@attachable_filename = attachable_filename(attachable).to_s
|
57
59
|
end
|
58
60
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_analyzable'
|
4
5
|
require_relative 'shared/asv_attachable'
|
5
6
|
require_relative 'shared/asv_errorable'
|
6
7
|
require_relative 'shared/asv_optionable'
|
@@ -9,6 +10,7 @@ require_relative 'shared/asv_symbolizable'
|
|
9
10
|
module ActiveStorageValidations
|
10
11
|
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
11
12
|
include ASVActiveStorageable
|
13
|
+
include ASVAnalyzable
|
12
14
|
include ASVAttachable
|
13
15
|
include ASVErrorable
|
14
16
|
include ASVOptionable
|
@@ -43,10 +43,10 @@ module ActiveStorageValidations
|
|
43
43
|
@subject = subject.is_a?(Class) ? subject.new : subject
|
44
44
|
|
45
45
|
is_a_valid_active_storage_attribute? &&
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
46
|
+
is_context_valid? &&
|
47
|
+
is_custom_message_valid? &&
|
48
|
+
is_valid_when_image_processable? &&
|
49
|
+
is_invalid_when_image_not_processable?
|
50
50
|
end
|
51
51
|
|
52
52
|
private
|
@@ -27,7 +27,8 @@ module ActiveStorageValidations
|
|
27
27
|
|
28
28
|
def self.mock_metadata(attachment, width, height)
|
29
29
|
mock = Struct.new(:metadata).new({ width: width, height: height })
|
30
|
-
|
30
|
+
|
31
|
+
stub_method(ActiveStorageValidations::Analyzer, :new, mock) do
|
31
32
|
yield
|
32
33
|
end
|
33
34
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'shared/asv_active_storageable'
|
4
|
+
require_relative 'shared/asv_analyzable'
|
4
5
|
require_relative 'shared/asv_attachable'
|
5
6
|
require_relative 'shared/asv_errorable'
|
6
7
|
require_relative 'shared/asv_symbolizable'
|
@@ -8,6 +9,7 @@ require_relative 'shared/asv_symbolizable'
|
|
8
9
|
module ActiveStorageValidations
|
9
10
|
class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
|
10
11
|
include ASVActiveStorageable
|
12
|
+
include ASVAnalyzable
|
11
13
|
include ASVAttachable
|
12
14
|
include ASVErrorable
|
13
15
|
include ASVSymbolizable
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorageValidations
|
4
|
+
# ActiveStorageValidations::ASVAnalyzable
|
5
|
+
#
|
6
|
+
# Validator methods for choosing the right analyzer depending on the file
|
7
|
+
# media type and available third-party analyzers.
|
8
|
+
module ASVAnalyzable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def metadata_for(attachable)
|
16
|
+
analyzer_for(attachable).metadata
|
17
|
+
end
|
18
|
+
|
19
|
+
def analyzer_for(attachable)
|
20
|
+
case attachable_media_type(attachable)
|
21
|
+
when "image" then image_analyzer_for(attachable)
|
22
|
+
else fallback_analyzer_for(attachable)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def image_analyzer_for(attachable)
|
27
|
+
case image_processor
|
28
|
+
when :mini_magick
|
29
|
+
ActiveStorageValidations::Analyzer::ImageAnalyzer::ImageMagick.new(attachable)
|
30
|
+
when :vips
|
31
|
+
ActiveStorageValidations::Analyzer::ImageAnalyzer::Vips.new(attachable)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def image_processor
|
36
|
+
# Rails returns nil for default image processor, because it is set in an after initialize callback
|
37
|
+
# https://github.com/rails/rails/blob/main/activestorage/lib/active_storage/engine.rb
|
38
|
+
ActiveStorage.variant_processor || DEFAULT_IMAGE_PROCESSOR
|
39
|
+
end
|
40
|
+
|
41
|
+
def fallback_analyzer_for(attachable)
|
42
|
+
ActiveStorageValidations::Analyzer::NullAnalyzer.new(attachable)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "../metadata"
|
4
|
-
|
5
3
|
module ActiveStorageValidations
|
6
4
|
# ActiveStorageValidations::ASVAttachable
|
7
5
|
#
|
@@ -20,7 +18,7 @@ module ActiveStorageValidations
|
|
20
18
|
# to perform file analyses.
|
21
19
|
def validate_changed_files_from_metadata(record, attribute)
|
22
20
|
attachables_from_changes(record, attribute).each do |attachable|
|
23
|
-
is_valid?(record, attribute, attachable,
|
21
|
+
is_valid?(record, attribute, attachable, metadata_for(attachable))
|
24
22
|
end
|
25
23
|
end
|
26
24
|
|
@@ -64,25 +62,67 @@ module ActiveStorageValidations
|
|
64
62
|
def attachable_content_type(attachable)
|
65
63
|
full_attachable_content_type(attachable) && full_attachable_content_type(attachable).downcase.split(/[;,\s]/, 2).first
|
66
64
|
end
|
65
|
+
|
66
|
+
# Retrieve the content_type from attachable using the same logic as Rails
|
67
|
+
# ActiveStorage::Blob::Identifiable#identify_content_type
|
68
|
+
def attachable_content_type_rails_like(attachable)
|
69
|
+
Marcel::MimeType.for(
|
70
|
+
attachable_io(attachable, max_byte_size: 4.kilobytes),
|
71
|
+
name: attachable_filename(attachable).to_s,
|
72
|
+
declared_type: full_attachable_content_type(attachable)
|
73
|
+
)
|
74
|
+
end
|
67
75
|
|
76
|
+
# Retrieve the media type of the attachable, which is the first part of the
|
77
|
+
# content type (or mime type).
|
78
|
+
# Possible values are: application/audio/example/font/image/model/text/video
|
79
|
+
def attachable_media_type(attachable)
|
80
|
+
(full_attachable_content_type(attachable) || marcel_content_type_from_filename(attachable)).split("/").first
|
81
|
+
end
|
82
|
+
|
68
83
|
# Retrieve the io from attachable.
|
69
|
-
def attachable_io(attachable)
|
84
|
+
def attachable_io(attachable, max_byte_size: nil)
|
85
|
+
io = case attachable
|
86
|
+
when ActiveStorage::Blob
|
87
|
+
(max_byte_size && supports_blob_download_chunk?) ? attachable.download_chunk(0...max_byte_size) : attachable.download
|
88
|
+
when ActionDispatch::Http::UploadedFile
|
89
|
+
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
90
|
+
when Rack::Test::UploadedFile
|
91
|
+
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
92
|
+
when String
|
93
|
+
blob = ActiveStorage::Blob.find_signed!(attachable)
|
94
|
+
(max_byte_size && supports_blob_download_chunk?) ? blob.download_chunk(0...max_byte_size) : blob.download
|
95
|
+
when Hash
|
96
|
+
max_byte_size ? attachable[:io].read(max_byte_size) : attachable[:io].read
|
97
|
+
when File
|
98
|
+
raise_rails_like_error(attachable) unless supports_file_attachment?
|
99
|
+
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
100
|
+
when Pathname
|
101
|
+
raise_rails_like_error(attachable) unless supports_pathname_attachment?
|
102
|
+
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
103
|
+
else
|
104
|
+
raise_rails_like_error(attachable)
|
105
|
+
end
|
106
|
+
|
107
|
+
rewind_attachable_io(attachable)
|
108
|
+
io
|
109
|
+
end
|
110
|
+
|
111
|
+
# Rewind the io attachable.
|
112
|
+
def rewind_attachable_io(attachable)
|
70
113
|
case attachable
|
71
|
-
when ActiveStorage::Blob
|
72
|
-
|
73
|
-
when ActionDispatch::Http::UploadedFile
|
74
|
-
attachable.
|
75
|
-
when Rack::Test::UploadedFile
|
76
|
-
attachable.read
|
77
|
-
when String
|
78
|
-
blob = ActiveStorage::Blob.find_signed!(attachable)
|
79
|
-
blob.download
|
114
|
+
when ActiveStorage::Blob, String
|
115
|
+
# nothing to do
|
116
|
+
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
117
|
+
attachable.rewind
|
80
118
|
when Hash
|
81
|
-
attachable[:io].
|
119
|
+
attachable[:io].rewind
|
82
120
|
when File
|
83
|
-
|
121
|
+
raise_rails_like_error(attachable) unless supports_file_attachment?
|
122
|
+
attachable.rewind
|
84
123
|
when Pathname
|
85
|
-
|
124
|
+
raise_rails_like_error(attachable) unless supports_pathname_attachment?
|
125
|
+
File.open(attachable) { |f| f.rewind }
|
86
126
|
else
|
87
127
|
raise_rails_like_error(attachable)
|
88
128
|
end
|
@@ -128,6 +168,13 @@ module ActiveStorageValidations
|
|
128
168
|
end
|
129
169
|
alias :supports_pathname_attachment? :supports_file_attachment?
|
130
170
|
|
171
|
+
# Check if the current Rails version supports ActiveStorage::Blob#download_chunk
|
172
|
+
#
|
173
|
+
# https://github.com/rails/rails/blob/7-0-stable/activestorage/CHANGELOG.md#rails-700alpha1-september-15-2021
|
174
|
+
def supports_blob_download_chunk?
|
175
|
+
Rails.gem_version >= Gem::Version.new('7.0.0.alpha1')
|
176
|
+
end
|
177
|
+
|
131
178
|
# Retrieve the content_type from the file name only
|
132
179
|
def marcel_content_type_from_filename(attachable)
|
133
180
|
Marcel::MimeType.for(name: attachable_filename(attachable).to_s)
|
@@ -3,6 +3,12 @@
|
|
3
3
|
require 'active_model'
|
4
4
|
require 'active_support/concern'
|
5
5
|
|
6
|
+
require 'active_storage_validations/analyzer'
|
7
|
+
require 'active_storage_validations/analyzer/image_analyzer'
|
8
|
+
require 'active_storage_validations/analyzer/image_analyzer/image_magick'
|
9
|
+
require 'active_storage_validations/analyzer/image_analyzer/vips'
|
10
|
+
require 'active_storage_validations/analyzer/null_analyzer'
|
11
|
+
|
6
12
|
require 'active_storage_validations/railtie'
|
7
13
|
require 'active_storage_validations/engine'
|
8
14
|
require 'active_storage_validations/attached_validator'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_storage_validations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Kasyanchuk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11
|
11
|
+
date: 2024-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -259,6 +259,11 @@ files:
|
|
259
259
|
- config/locales/vi.yml
|
260
260
|
- config/locales/zh-CN.yml
|
261
261
|
- lib/active_storage_validations.rb
|
262
|
+
- lib/active_storage_validations/analyzer.rb
|
263
|
+
- lib/active_storage_validations/analyzer/image_analyzer.rb
|
264
|
+
- lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb
|
265
|
+
- lib/active_storage_validations/analyzer/image_analyzer/vips.rb
|
266
|
+
- lib/active_storage_validations/analyzer/null_analyzer.rb
|
262
267
|
- lib/active_storage_validations/aspect_ratio_validator.rb
|
263
268
|
- lib/active_storage_validations/attached_validator.rb
|
264
269
|
- lib/active_storage_validations/base_size_validator.rb
|
@@ -285,10 +290,10 @@ files:
|
|
285
290
|
- lib/active_storage_validations/matchers/shared/asv_validatable.rb
|
286
291
|
- lib/active_storage_validations/matchers/size_validator_matcher.rb
|
287
292
|
- lib/active_storage_validations/matchers/total_size_validator_matcher.rb
|
288
|
-
- lib/active_storage_validations/metadata.rb
|
289
293
|
- lib/active_storage_validations/processable_image_validator.rb
|
290
294
|
- lib/active_storage_validations/railtie.rb
|
291
295
|
- lib/active_storage_validations/shared/asv_active_storageable.rb
|
296
|
+
- lib/active_storage_validations/shared/asv_analyzable.rb
|
292
297
|
- lib/active_storage_validations/shared/asv_attachable.rb
|
293
298
|
- lib/active_storage_validations/shared/asv_errorable.rb
|
294
299
|
- lib/active_storage_validations/shared/asv_loggable.rb
|
@@ -1,179 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'shared/asv_loggable'
|
4
|
-
|
5
|
-
module ActiveStorageValidations
|
6
|
-
class Metadata
|
7
|
-
include ASVLoggable
|
8
|
-
|
9
|
-
class InvalidImageError < StandardError; end
|
10
|
-
|
11
|
-
attr_reader :attachable
|
12
|
-
|
13
|
-
DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
|
14
|
-
|
15
|
-
def initialize(attachable)
|
16
|
-
require_image_processor
|
17
|
-
@attachable = attachable
|
18
|
-
end
|
19
|
-
|
20
|
-
def valid?
|
21
|
-
read_image
|
22
|
-
true
|
23
|
-
rescue InvalidImageError
|
24
|
-
false
|
25
|
-
end
|
26
|
-
|
27
|
-
def metadata
|
28
|
-
read_image do |image|
|
29
|
-
if rotated_image?(image)
|
30
|
-
{ width: image.height, height: image.width }
|
31
|
-
else
|
32
|
-
{ width: image.width, height: image.height }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
rescue InvalidImageError
|
36
|
-
logger.info "Skipping image analysis because ImageMagick or Vips doesn't support the file"
|
37
|
-
{}
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def image_processor
|
43
|
-
# Rails returns nil for default image processor, because it is set in an after initialize callback
|
44
|
-
# https://github.com/rails/rails/blob/89d8569abe2564c8187debf32dd3b4e33d6ad983/activestorage/lib/active_storage/engine.rb
|
45
|
-
Rails.application.config.active_storage.variant_processor || DEFAULT_IMAGE_PROCESSOR
|
46
|
-
end
|
47
|
-
|
48
|
-
def require_image_processor
|
49
|
-
case image_processor
|
50
|
-
when :vips then require 'vips' unless defined?(Vips)
|
51
|
-
when :mini_magick then require 'mini_magick' unless defined?(MiniMagick)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def exception_class
|
56
|
-
case image_processor
|
57
|
-
when :vips then Vips::Error
|
58
|
-
when :mini_magick then MiniMagick::Error
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def vips_image_processor?
|
63
|
-
image_processor == :vips
|
64
|
-
end
|
65
|
-
|
66
|
-
def read_image
|
67
|
-
is_string = attachable.is_a?(String)
|
68
|
-
if is_string || attachable.is_a?(ActiveStorage::Blob)
|
69
|
-
blob = is_string ? ActiveStorage::Blob.find_signed!(attachable) : attachable
|
70
|
-
|
71
|
-
tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
|
72
|
-
tempfile.binmode
|
73
|
-
|
74
|
-
blob.download do |chunk|
|
75
|
-
tempfile.write(chunk)
|
76
|
-
end
|
77
|
-
|
78
|
-
tempfile.flush
|
79
|
-
tempfile.rewind
|
80
|
-
|
81
|
-
image = new_image_from_path(tempfile.path)
|
82
|
-
else
|
83
|
-
file_path = read_file_path
|
84
|
-
image = new_image_from_path(file_path)
|
85
|
-
end
|
86
|
-
|
87
|
-
|
88
|
-
raise InvalidImageError unless valid_image?(image)
|
89
|
-
yield image if block_given?
|
90
|
-
rescue LoadError, NameError
|
91
|
-
logger.info "Skipping image analysis because the mini_magick or ruby-vips gem isn't installed"
|
92
|
-
{}
|
93
|
-
rescue exception_class => error
|
94
|
-
logger.error "Skipping image analysis due to an #{exception_class.name.split('::').map(&:downcase).join(' ').capitalize} error: #{error.message}"
|
95
|
-
{}
|
96
|
-
ensure
|
97
|
-
image = nil
|
98
|
-
end
|
99
|
-
|
100
|
-
def new_image_from_path(path)
|
101
|
-
if vips_image_processor? && (supported_vips_suffix?(path) || vips_version_below_8_8? || open_uri_tempfile?(path))
|
102
|
-
begin
|
103
|
-
Vips::Image.new_from_file(path)
|
104
|
-
rescue exception_class
|
105
|
-
# We handle cases where an error is raised when reading the attachable
|
106
|
-
# because Vips can throw errors rather than returning false
|
107
|
-
# We stumble upon this issue while reading 0 byte size attachable
|
108
|
-
# https://github.com/janko/image_processing/issues/97
|
109
|
-
false
|
110
|
-
end
|
111
|
-
elsif defined?(MiniMagick)
|
112
|
-
MiniMagick::Image.new(path)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def supported_vips_suffix?(path)
|
117
|
-
Vips::get_suffixes.include?(File.extname(path).downcase)
|
118
|
-
end
|
119
|
-
|
120
|
-
def vips_version_below_8_8?
|
121
|
-
# FYI, Vips 8.8 was released in 2019
|
122
|
-
# https://github.com/libvips/libvips/releases/tag/v8.8.0
|
123
|
-
!Vips::respond_to?(:vips_foreign_get_suffixes)
|
124
|
-
end
|
125
|
-
|
126
|
-
def open_uri_tempfile?(path)
|
127
|
-
# When trying to open urls for 'large' images, OpenURI will return a
|
128
|
-
# tempfile. That tempfile does not have an extension indicating the type
|
129
|
-
# of file. However, Vips will be able to process it anyway.
|
130
|
-
# The 'large' file value is derived from OpenUri::Buffer class (> 10ko)
|
131
|
-
path.split('/').last.starts_with?("open-uri")
|
132
|
-
end
|
133
|
-
|
134
|
-
def valid_image?(image)
|
135
|
-
return false unless image
|
136
|
-
|
137
|
-
vips_image_processor? && image.is_a?(Vips::Image) ? image.avg : image.valid?
|
138
|
-
rescue exception_class
|
139
|
-
false
|
140
|
-
end
|
141
|
-
|
142
|
-
def rotated_image?(image)
|
143
|
-
if vips_image_processor? && image.is_a?(Vips::Image)
|
144
|
-
image.get('exif-ifd0-Orientation').include?('Right-top') ||
|
145
|
-
image.get('exif-ifd0-Orientation').include?('Left-bottom')
|
146
|
-
else
|
147
|
-
%w[ RightTop LeftBottom ].include?(image["%[orientation]"])
|
148
|
-
end
|
149
|
-
rescue exception_class # field "exif-ifd0-Orientation" not found
|
150
|
-
false
|
151
|
-
end
|
152
|
-
|
153
|
-
def read_file_path
|
154
|
-
case attachable
|
155
|
-
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
156
|
-
attachable.path
|
157
|
-
when Hash
|
158
|
-
io = attachable.fetch(:io)
|
159
|
-
if io.is_a?(StringIO)
|
160
|
-
tempfile = Tempfile.new([File.basename(attachable[:filename], '.*'), File.extname(attachable[:filename])])
|
161
|
-
tempfile.binmode
|
162
|
-
IO.copy_stream(io, tempfile)
|
163
|
-
io.rewind
|
164
|
-
tempfile.flush
|
165
|
-
tempfile.rewind
|
166
|
-
tempfile.path
|
167
|
-
else
|
168
|
-
File.open(io).path
|
169
|
-
end
|
170
|
-
when File
|
171
|
-
attachable.path
|
172
|
-
when Pathname
|
173
|
-
attachable.to_s
|
174
|
-
else
|
175
|
-
raise "Something wrong with params."
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|