active_storage_validations 4.0.0 → 4.1.1
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 +71 -8
- data/config/locales/da.yml +2 -0
- data/config/locales/de.yml +2 -0
- data/config/locales/en-GB.yml +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/es.yml +2 -0
- data/config/locales/fr.yml +2 -0
- data/config/locales/it.yml +2 -0
- data/config/locales/ja.yml +2 -0
- data/config/locales/nl.yml +2 -0
- data/config/locales/pl.yml +2 -0
- data/config/locales/pt-BR.yml +2 -0
- data/config/locales/ru.yml +2 -0
- data/config/locales/sv.yml +4 -2
- data/config/locales/tr.yml +2 -0
- data/config/locales/uk.yml +2 -0
- data/config/locales/vi.yml +2 -0
- data/config/locales/zh-CN.yml +2 -0
- data/lib/active_storage_validations/analyzer/audio_analyzer.rb +7 -1
- data/lib/active_storage_validations/analyzer/content_type_analyzer/file.rb +0 -2
- data/lib/active_storage_validations/analyzer/content_type_analyzer/magika.rb +0 -2
- data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +3 -0
- data/lib/active_storage_validations/analyzer/image_analyzer.rb +16 -6
- data/lib/active_storage_validations/analyzer/pdf_analyzer.rb +11 -2
- data/lib/active_storage_validations/analyzer.rb +2 -35
- data/lib/active_storage_validations/aspect_ratio_validator.rb +21 -15
- data/lib/active_storage_validations/asv_attachable_adapter.rb +221 -0
- data/lib/active_storage_validations/content_type_validator.rb +40 -64
- data/lib/active_storage_validations/duration_validator.rb +3 -1
- data/lib/active_storage_validations/engine.rb +3 -0
- data/lib/active_storage_validations/extensors/asv_marcelable.rb +5 -5
- data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +11 -4
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +2 -1
- data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +1 -0
- data/lib/active_storage_validations/matchers/shared/asv_allow_blankable.rb +1 -1
- data/lib/active_storage_validations/matchers/with_audio_validator_matcher.rb +114 -0
- data/lib/active_storage_validations/matchers.rb +22 -10
- data/lib/active_storage_validations/shared/asv_analyzable.rb +51 -8
- data/lib/active_storage_validations/shared/asv_attachable.rb +14 -97
- data/lib/active_storage_validations/shared/asv_errorable.rb +9 -8
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations/with_audio_validator.rb +43 -0
- data/lib/active_storage_validations.rb +1 -0
- metadata +5 -2
|
@@ -8,7 +8,11 @@ module ActiveStorageValidations
|
|
|
8
8
|
module ASVAnalyzable
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
|
-
DEFAULT_IMAGE_PROCESSOR = :mini_magick
|
|
11
|
+
DEFAULT_IMAGE_PROCESSOR = :mini_magick
|
|
12
|
+
# Keys written by the content-type sniffers. They record what the file
|
|
13
|
+
# claims to be, not whether an analyzer could decode it, so they must never
|
|
14
|
+
# count as evidence that a media analysis happened.
|
|
15
|
+
CONTENT_TYPE_METADATA_KEYS = %i[content_type content_type_backend].freeze
|
|
12
16
|
|
|
13
17
|
private
|
|
14
18
|
|
|
@@ -21,13 +25,42 @@ module ActiveStorageValidations
|
|
|
21
25
|
return content_type_metadata_for(blob, attachable)
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
return blob
|
|
28
|
+
return media_metadata(blob) if blob_has_asv_metadata?(blob, metadata_keys)
|
|
25
29
|
|
|
26
30
|
new_metadata = generate_metadata_for(attachable, metadata_keys) || {}
|
|
27
|
-
blob.merge_into_active_storage_validations_metadata(new_metadata)
|
|
28
|
-
blob
|
|
31
|
+
blob.merge_into_active_storage_validations_metadata(memoize_unavailable_keys(new_metadata, metadata_keys))
|
|
32
|
+
persist_asv_metadata(blob)
|
|
29
33
|
|
|
30
|
-
blob
|
|
34
|
+
media_metadata(blob)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The metadata produced by the media analyzers, i.e. everything the blob
|
|
38
|
+
# carries except the content-type sniffer keys. Validators that ask for an
|
|
39
|
+
# empty METADATA_KEYS (ProcessableFileValidator) treat a non-empty result as
|
|
40
|
+
# proof that an analyzer decoded the file, so a cached +asv_content_type+
|
|
41
|
+
# left behind by spoofing_protection must not leak into it.
|
|
42
|
+
def media_metadata(blob)
|
|
43
|
+
blob.active_storage_validations_metadata.except(*CONTENT_TYPE_METADATA_KEYS)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Analyzers only return the keys they could extract, so a requested key the
|
|
47
|
+
# analyzer cannot produce for this file (e.g. :duration for an image) would
|
|
48
|
+
# never be cached and the expensive analysis would run again on every
|
|
49
|
+
# validation. Store those keys as blank to memoize the miss: the validator
|
|
50
|
+
# still sees no usable value and adds its usual error, but only analyzes once.
|
|
51
|
+
#
|
|
52
|
+
# An entirely empty result means no analysis happened at all — missing CLI,
|
|
53
|
+
# timed out command, unreadable file — so it is left unmemoized and retried
|
|
54
|
+
# on the next validation.
|
|
55
|
+
#
|
|
56
|
+
# Teaching an existing analyzer a new metadata key therefore requires
|
|
57
|
+
# clearing the misses memoized by previous versions of the gem, see
|
|
58
|
+
# ASVBlobMetadatable#remove_active_storage_validations_metadata!.
|
|
59
|
+
def memoize_unavailable_keys(new_metadata, metadata_keys)
|
|
60
|
+
return new_metadata if new_metadata.blank?
|
|
61
|
+
|
|
62
|
+
unavailable_keys = metadata_keys.reject { |key| new_metadata.key?(key) }
|
|
63
|
+
new_metadata.merge(unavailable_keys.index_with(nil))
|
|
31
64
|
end
|
|
32
65
|
|
|
33
66
|
def content_type_metadata_keys?(metadata_keys)
|
|
@@ -48,11 +81,20 @@ module ActiveStorageValidations
|
|
|
48
81
|
return failed_content_type_metadata(backend) if content_type_analysis_failed?(new_metadata)
|
|
49
82
|
|
|
50
83
|
blob.merge_into_active_storage_validations_metadata(new_metadata)
|
|
51
|
-
blob
|
|
84
|
+
persist_asv_metadata(blob)
|
|
52
85
|
|
|
53
86
|
blob.active_storage_validations_metadata
|
|
54
87
|
end
|
|
55
88
|
|
|
89
|
+
# A bare +valid?+ on a new record must not INSERT an +active_storage_blobs+
|
|
90
|
+
# row (no file on the service, orphaned if the record is never saved).
|
|
91
|
+
# In-memory +asv_*+ on the unsaved blob is persisted by Rails when the
|
|
92
|
+
# record is saved. Already-persisted blobs (re-validation, attach after
|
|
93
|
+
# save, pre-v2 blobs) still need +save!+ so the cache survives +reload+.
|
|
94
|
+
def persist_asv_metadata(blob)
|
|
95
|
+
blob.save! if blob.persisted?
|
|
96
|
+
end
|
|
97
|
+
|
|
56
98
|
def content_type_analysis_failed?(new_metadata)
|
|
57
99
|
new_metadata.blank? || new_metadata[:content_type].blank?
|
|
58
100
|
end
|
|
@@ -74,9 +116,10 @@ module ActiveStorageValidations
|
|
|
74
116
|
end
|
|
75
117
|
|
|
76
118
|
def blob_has_asv_metadata?(blob, metadata_keys)
|
|
77
|
-
|
|
119
|
+
cached = media_metadata(blob)
|
|
120
|
+
return false unless cached.present?
|
|
78
121
|
|
|
79
|
-
metadata_keys.all? { |key|
|
|
122
|
+
metadata_keys.all? { |key| cached.key?(key) }
|
|
80
123
|
end
|
|
81
124
|
|
|
82
125
|
def generate_metadata_for(attachable, metadata_keys)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../asv_attachable_adapter"
|
|
4
|
+
|
|
3
5
|
module ActiveStorageValidations
|
|
4
6
|
# ActiveStorageValidations::ASVAttachable
|
|
5
7
|
#
|
|
@@ -66,35 +68,17 @@ module ActiveStorageValidations
|
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
# Retrieve the full declared content_type from attachable.
|
|
70
|
-
# rubocop:disable Metrics/MethodLength
|
|
71
71
|
def full_attachable_content_type(attachable)
|
|
72
|
-
|
|
73
|
-
when ActiveStorage::Blob
|
|
74
|
-
attachable.content_type
|
|
75
|
-
when ActionDispatch::Http::UploadedFile
|
|
76
|
-
attachable.content_type
|
|
77
|
-
when Rack::Test::UploadedFile
|
|
78
|
-
attachable.content_type
|
|
79
|
-
when String
|
|
80
|
-
blob = ActiveStorage::Blob.find_signed!(attachable)
|
|
81
|
-
blob.content_type
|
|
82
|
-
when Hash
|
|
83
|
-
attachable[:content_type]
|
|
84
|
-
when File
|
|
85
|
-
supports_file_attachment? ? marcel_content_type_from_filename(attachable) : raise_rails_like_error(attachable)
|
|
86
|
-
when Pathname
|
|
87
|
-
supports_pathname_attachment? ? marcel_content_type_from_filename(attachable) : raise_rails_like_error(attachable)
|
|
88
|
-
else
|
|
89
|
-
raise_rails_like_error(attachable)
|
|
90
|
-
end
|
|
72
|
+
wrap_attachable(attachable).content_type
|
|
91
73
|
end
|
|
92
|
-
# rubocop:enable Metrics/MethodLength
|
|
93
74
|
|
|
94
75
|
# Retrieve the declared content_type from attachable without potential mime
|
|
95
76
|
# type parameters (e.g. 'application/x-rar-compressed;version=5')
|
|
96
77
|
def attachable_content_type(attachable)
|
|
97
|
-
|
|
78
|
+
declared_content_type = full_attachable_content_type(attachable)
|
|
79
|
+
|
|
80
|
+
(declared_content_type && content_type_without_parameters(declared_content_type)) ||
|
|
81
|
+
marcel_content_type_from_filename(attachable)
|
|
98
82
|
end
|
|
99
83
|
|
|
100
84
|
# Remove the potential mime type parameters from the content_type (e.g.
|
|
@@ -120,90 +104,23 @@ module ActiveStorageValidations
|
|
|
120
104
|
(full_attachable_content_type(attachable) || marcel_content_type_from_filename(attachable)).split("/").first
|
|
121
105
|
end
|
|
122
106
|
|
|
123
|
-
# Retrieve the io from attachable.
|
|
124
|
-
# rubocop:disable Metrics/MethodLength
|
|
125
|
-
# rubocop:disable Metrics/AbcSize
|
|
126
107
|
def attachable_io(attachable, max_byte_size: nil)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
when ActionDispatch::Http::UploadedFile
|
|
131
|
-
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
|
132
|
-
when Rack::Test::UploadedFile
|
|
133
|
-
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
|
134
|
-
when String
|
|
135
|
-
blob = ActiveStorage::Blob.find_signed!(attachable)
|
|
136
|
-
max_byte_size ? blob.download_chunk(0...max_byte_size) : blob.download
|
|
137
|
-
when Hash
|
|
138
|
-
max_byte_size ? attachable[:io].read(max_byte_size) : attachable[:io].read
|
|
139
|
-
when File
|
|
140
|
-
raise_rails_like_error(attachable) unless supports_file_attachment?
|
|
141
|
-
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
|
142
|
-
when Pathname
|
|
143
|
-
raise_rails_like_error(attachable) unless supports_pathname_attachment?
|
|
144
|
-
max_byte_size ? attachable.read(max_byte_size) : attachable.read
|
|
145
|
-
else
|
|
146
|
-
raise_rails_like_error(attachable)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
rewind_attachable_io(attachable)
|
|
108
|
+
wrapped = wrap_attachable(attachable)
|
|
109
|
+
io = wrapped.read(max_byte_size: max_byte_size)
|
|
110
|
+
wrapped.rewind
|
|
150
111
|
io
|
|
151
112
|
end
|
|
152
|
-
# rubocop:enable Metrics/MethodLength
|
|
153
|
-
# rubocop:enable Metrics/AbcSize
|
|
154
113
|
|
|
155
|
-
# Rewind the io attachable.
|
|
156
114
|
def rewind_attachable_io(attachable)
|
|
157
|
-
|
|
158
|
-
when ActiveStorage::Blob, String
|
|
159
|
-
# nothing to do
|
|
160
|
-
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
|
161
|
-
attachable.rewind
|
|
162
|
-
when Hash
|
|
163
|
-
attachable[:io].rewind
|
|
164
|
-
when File
|
|
165
|
-
raise_rails_like_error(attachable) unless supports_file_attachment?
|
|
166
|
-
attachable.rewind
|
|
167
|
-
when Pathname
|
|
168
|
-
raise_rails_like_error(attachable) unless supports_pathname_attachment?
|
|
169
|
-
File.open(attachable) { |f| f.rewind }
|
|
170
|
-
else
|
|
171
|
-
raise_rails_like_error(attachable)
|
|
172
|
-
end
|
|
115
|
+
wrap_attachable(attachable).rewind
|
|
173
116
|
end
|
|
174
117
|
|
|
175
|
-
# Retrieve the declared filename from attachable.
|
|
176
|
-
# rubocop:disable Metrics/MethodLength
|
|
177
118
|
def attachable_filename(attachable)
|
|
178
|
-
|
|
179
|
-
when ActiveStorage::Blob
|
|
180
|
-
attachable.filename
|
|
181
|
-
when ActionDispatch::Http::UploadedFile
|
|
182
|
-
attachable.original_filename
|
|
183
|
-
when Rack::Test::UploadedFile
|
|
184
|
-
attachable.original_filename
|
|
185
|
-
when String
|
|
186
|
-
blob = ActiveStorage::Blob.find_signed!(attachable)
|
|
187
|
-
blob.filename
|
|
188
|
-
when Hash
|
|
189
|
-
attachable[:filename]
|
|
190
|
-
when File
|
|
191
|
-
supports_file_attachment? ? File.basename(attachable) : raise_rails_like_error(attachable)
|
|
192
|
-
when Pathname
|
|
193
|
-
supports_pathname_attachment? ? File.basename(attachable) : raise_rails_like_error(attachable)
|
|
194
|
-
else
|
|
195
|
-
raise_rails_like_error(attachable)
|
|
196
|
-
end
|
|
119
|
+
wrap_attachable(attachable).filename
|
|
197
120
|
end
|
|
198
|
-
# rubocop:enable Metrics/MethodLength
|
|
199
121
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
raise(
|
|
203
|
-
ArgumentError,
|
|
204
|
-
"Could not find or build blob: expected attachable, " \
|
|
205
|
-
"got #{attachable.inspect}"
|
|
206
|
-
)
|
|
122
|
+
def wrap_attachable(attachable)
|
|
123
|
+
ASVAttachableAdapter.wrap(attachable, file_supported: supports_file_attachment?)
|
|
207
124
|
end
|
|
208
125
|
|
|
209
126
|
# Check if the current Rails version supports File or Pathname attachment
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../asv_attachable_adapter"
|
|
4
|
+
|
|
3
5
|
module ActiveStorageValidations
|
|
4
6
|
module ASVErrorable
|
|
5
7
|
extend ActiveSupport::Concern
|
|
@@ -17,9 +19,9 @@ module ActiveStorageValidations
|
|
|
17
19
|
curated_options.merge(active_storage_validations_options)
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
# Every offending file gets its own error, so a has_many_attached relation
|
|
23
|
+
# reports each invalid filename rather than only the first one.
|
|
20
24
|
def add_error(record, attribute, error_type, **errors_options)
|
|
21
|
-
return if record.errors.added?(attribute, error_type)
|
|
22
|
-
|
|
23
25
|
# You can read https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add
|
|
24
26
|
# to better understand how Rails model errors work
|
|
25
27
|
record.errors.add(attribute, error_type, **errors_options)
|
|
@@ -30,12 +32,11 @@ module ActiveStorageValidations
|
|
|
30
32
|
def get_filename(file)
|
|
31
33
|
return nil unless file
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
end.to_s
|
|
35
|
+
filename_from(file)&.to_s.presence
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def filename_from(file)
|
|
39
|
+
ASVAttachableAdapter.filename_for(file)
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
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_symbolizable"
|
|
8
|
+
|
|
9
|
+
module ActiveStorageValidations
|
|
10
|
+
class WithAudioValidator < ActiveModel::EachValidator # :nodoc
|
|
11
|
+
include ASVActiveStorageable
|
|
12
|
+
include ASVAnalyzable
|
|
13
|
+
include ASVAttachable
|
|
14
|
+
include ASVErrorable
|
|
15
|
+
include ASVSymbolizable
|
|
16
|
+
|
|
17
|
+
ERROR_TYPES = %i[audio_missing audio_present].freeze
|
|
18
|
+
METADATA_KEYS = %i[audio].freeze
|
|
19
|
+
|
|
20
|
+
def validate_each(record, attribute, _value)
|
|
21
|
+
return if no_attachments?(record, attribute)
|
|
22
|
+
|
|
23
|
+
validate_changed_files_from_metadata(record, attribute, METADATA_KEYS)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def is_valid?(record, attribute, attachable, metadata)
|
|
29
|
+
expected_audio = options.fetch(:with, true)
|
|
30
|
+
return if audio_track?(metadata) == expected_audio
|
|
31
|
+
|
|
32
|
+
errors_options = initialize_error_options(options, attachable)
|
|
33
|
+
error_type = expected_audio ? :audio_missing : :audio_present
|
|
34
|
+
add_error(record, attribute, error_type, **errors_options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Files the analyzer cannot inspect for audio streams (images, pdfs) have no
|
|
38
|
+
# audio metadata, or a memoized unavailable value. Both mean no audio track.
|
|
39
|
+
def audio_track?(metadata)
|
|
40
|
+
metadata&.fetch(:audio, false) == true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -41,6 +41,7 @@ require "active_storage_validations/content_type_validator"
|
|
|
41
41
|
require "active_storage_validations/limit_validator"
|
|
42
42
|
require "active_storage_validations/dimension_validator"
|
|
43
43
|
require "active_storage_validations/duration_validator"
|
|
44
|
+
require "active_storage_validations/with_audio_validator"
|
|
44
45
|
require "active_storage_validations/aspect_ratio_validator"
|
|
45
46
|
require "active_storage_validations/processable_file_validator"
|
|
46
47
|
require "active_storage_validations/size_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: 4.
|
|
4
|
+
version: 4.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Kasyanchuk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activejob
|
|
@@ -233,6 +233,7 @@ files:
|
|
|
233
233
|
- lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb
|
|
234
234
|
- lib/active_storage_validations/analyzer/video_analyzer.rb
|
|
235
235
|
- lib/active_storage_validations/aspect_ratio_validator.rb
|
|
236
|
+
- lib/active_storage_validations/asv_attachable_adapter.rb
|
|
236
237
|
- lib/active_storage_validations/attached_validator.rb
|
|
237
238
|
- lib/active_storage_validations/base_comparison_validator.rb
|
|
238
239
|
- lib/active_storage_validations/content_type_validator.rb
|
|
@@ -265,6 +266,7 @@ files:
|
|
|
265
266
|
- lib/active_storage_validations/matchers/shared/asv_validatable.rb
|
|
266
267
|
- lib/active_storage_validations/matchers/size_validator_matcher.rb
|
|
267
268
|
- lib/active_storage_validations/matchers/total_size_validator_matcher.rb
|
|
269
|
+
- lib/active_storage_validations/matchers/with_audio_validator_matcher.rb
|
|
268
270
|
- lib/active_storage_validations/pages_validator.rb
|
|
269
271
|
- lib/active_storage_validations/processable_file_validator.rb
|
|
270
272
|
- lib/active_storage_validations/railtie.rb
|
|
@@ -279,6 +281,7 @@ files:
|
|
|
279
281
|
- lib/active_storage_validations/size_validator.rb
|
|
280
282
|
- lib/active_storage_validations/total_size_validator.rb
|
|
281
283
|
- lib/active_storage_validations/version.rb
|
|
284
|
+
- lib/active_storage_validations/with_audio_validator.rb
|
|
282
285
|
homepage: https://github.com/igorkasyanchuk/active_storage_validations
|
|
283
286
|
licenses:
|
|
284
287
|
- MIT
|