active_storage_validations 3.0.5 → 4.1.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/MIT-LICENSE +1 -1
- data/README.md +227 -116
- data/Rakefile +13 -15
- 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 +2 -0
- 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 +0 -1
- data/lib/active_storage_validations/analyzer/content_type_analyzer/file.rb +31 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer/magika.rb +44 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +15 -24
- data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +41 -7
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +36 -10
- data/lib/active_storage_validations/analyzer/pdf_analyzer.rb +3 -9
- data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +5 -4
- data/lib/active_storage_validations/analyzer/video_analyzer.rb +0 -1
- data/lib/active_storage_validations/analyzer.rb +35 -3
- data/lib/active_storage_validations/content_type_validator.rb +24 -6
- data/lib/active_storage_validations/dimension_validator.rb +4 -2
- data/lib/active_storage_validations/form_builder.rb +43 -0
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +4 -0
- data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +11 -1
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -0
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +4 -0
- data/lib/active_storage_validations/matchers/pages_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/shared/asv_except_onable.rb +57 -0
- data/lib/active_storage_validations/matchers/shared/asv_spoofing_protectable.rb +44 -0
- data/lib/active_storage_validations/matchers/shared/asv_timeoutable.rb +32 -0
- data/lib/active_storage_validations/matchers/with_audio_validator_matcher.rb +114 -0
- data/lib/active_storage_validations/matchers.rb +10 -1
- data/lib/active_storage_validations/processable_file_validator.rb +60 -1
- data/lib/active_storage_validations/railtie.rb +6 -0
- data/lib/active_storage_validations/shared/asv_analyzable.rb +66 -8
- data/lib/active_storage_validations/shared/asv_attachable.rb +9 -10
- data/lib/active_storage_validations/shared/asv_commandable.rb +95 -0
- data/lib/active_storage_validations/shared/asv_optionable.rb +5 -1
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations/with_audio_validator.rb +37 -0
- data/lib/active_storage_validations.rb +23 -0
- metadata +27 -58
data/Rakefile
CHANGED
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
begin
|
|
4
|
-
require
|
|
4
|
+
require "bundler/setup"
|
|
5
5
|
rescue LoadError
|
|
6
|
-
puts
|
|
6
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
require
|
|
9
|
+
require "rdoc/task"
|
|
10
10
|
|
|
11
11
|
RDoc::Task.new(:rdoc) do |rdoc|
|
|
12
|
-
rdoc.rdoc_dir =
|
|
13
|
-
rdoc.title =
|
|
14
|
-
rdoc.options <<
|
|
15
|
-
rdoc.rdoc_files.include(
|
|
16
|
-
rdoc.rdoc_files.include(
|
|
12
|
+
rdoc.rdoc_dir = "rdoc"
|
|
13
|
+
rdoc.title = "ActiveStorageValidations"
|
|
14
|
+
rdoc.options << "--line-numbers"
|
|
15
|
+
rdoc.rdoc_files.include("README.md")
|
|
16
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
require
|
|
19
|
+
require "bundler/gem_tasks"
|
|
20
20
|
|
|
21
|
-
require
|
|
21
|
+
require "rspec/core/rake_task"
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
t.
|
|
25
|
-
t.pattern = 'test/**/*_test.rb'
|
|
26
|
-
t.verbose = false
|
|
23
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
24
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
27
25
|
end
|
|
28
26
|
|
|
29
|
-
task default: :
|
|
27
|
+
task default: :spec
|
data/config/locales/da.yml
CHANGED
|
@@ -55,6 +55,8 @@ da:
|
|
|
55
55
|
aspect_ratio_not_x_y: "skal være %{authorized_aspect_ratios} (nuværende fil er %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "har et ugyldigt aspektforhold (gyldige aspektforhold er %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "identificeres ikke som en gyldig mediefil"
|
|
58
|
+
audio_missing: "skal have et lydspor"
|
|
59
|
+
audio_present: "må ikke have et lydspor"
|
|
58
60
|
pages_not_less_than: "sideantal skal være mindre end %{max} (aktuelt sideantal er %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "sideantal skal være mindre end eller lig med %{max} (aktuelt sideantal er %{pages})"
|
|
60
62
|
pages_not_greater_than: "sideantal skal være større end %{min} (aktuelt sideantal er %{pages})"
|
data/config/locales/de.yml
CHANGED
|
@@ -55,6 +55,8 @@ de:
|
|
|
55
55
|
aspect_ratio_not_x_y: "muss %{authorized_aspect_ratios} sein (aktuelle Datei ist %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "hat ein ungültiges Seitenverhältnis (gültige Seitenverhältnisse sind %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "wird nicht als gültige Mediendatei identifiziert"
|
|
58
|
+
audio_missing: "muss eine Audiospur enthalten"
|
|
59
|
+
audio_present: "darf keine Audiospur enthalten"
|
|
58
60
|
pages_not_less_than: "Die Seitenanzahl muss kleiner als %{max} sein (die aktuelle Seitenanzahl beträgt %{pages})."
|
|
59
61
|
pages_not_less_than_or_equal_to: "Die Seitenanzahl muss kleiner oder gleich %{max} sein (die aktuelle Seitenanzahl beträgt %{pages})."
|
|
60
62
|
pages_not_greater_than: "Die Seitenanzahl muss größer als %{min} sein (die aktuelle Seitenanzahl beträgt %{pages})."
|
data/config/locales/en-GB.yml
CHANGED
|
@@ -55,6 +55,8 @@ en-GB:
|
|
|
55
55
|
aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "is not identified as a valid media file"
|
|
58
|
+
audio_missing: "must have an audio track"
|
|
59
|
+
audio_present: "must not have an audio track"
|
|
58
60
|
pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
|
|
60
62
|
pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
|
data/config/locales/en.yml
CHANGED
|
@@ -55,6 +55,8 @@ en:
|
|
|
55
55
|
aspect_ratio_not_x_y: "must be %{authorized_aspect_ratios} (current file is %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "has an invalid aspect ratio (valid aspect ratios are %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "is not identified as a valid media file"
|
|
58
|
+
audio_missing: "must have an audio track"
|
|
59
|
+
audio_present: "must not have an audio track"
|
|
58
60
|
pages_not_less_than: "page count must be less than %{max} (current page count is %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "page count must be less than or equal to %{max} (current page count is %{pages})"
|
|
60
62
|
pages_not_greater_than: "page count must be greater than %{min} (current page count is %{pages})"
|
data/config/locales/es.yml
CHANGED
|
@@ -55,6 +55,8 @@ es:
|
|
|
55
55
|
aspect_ratio_not_x_y: "debe ser %{authorized_aspect_ratios} (el archivo actual es %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "tiene una relación de aspecto inválida (las relaciones de aspecto válidas son %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "no se identifica como un archivo multimedia válido"
|
|
58
|
+
audio_missing: "debe tener una pista de audio"
|
|
59
|
+
audio_present: "no debe tener una pista de audio"
|
|
58
60
|
pages_not_less_than: "el número de páginas debe ser menor que %{max} (el número actual de páginas es %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "el número de páginas debe ser menor o igual que %{max} (el número actual de páginas es %{pages})"
|
|
60
62
|
pages_not_greater_than: "el número de páginas debe ser mayor que %{min} (el número actual de páginas es %{pages})"
|
data/config/locales/fr.yml
CHANGED
|
@@ -55,6 +55,8 @@ fr:
|
|
|
55
55
|
aspect_ratio_not_x_y: "doit être %{authorized_aspect_ratios} (le fichier actuel est %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "a un rapport d'aspect invalide (les rapports d'aspect valides sont %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "n'est pas identifié comme un fichier média valide"
|
|
58
|
+
audio_missing: "doit contenir une piste audio"
|
|
59
|
+
audio_present: "ne doit pas contenir de piste audio"
|
|
58
60
|
pages_not_less_than: "le nombre de pages doit être inférieur à %{max} (le nombre actuel est de %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "le nombre de pages doit être inférieur ou égal à %{max} (le nombre actuel est de %{pages})"
|
|
60
62
|
pages_not_greater_than: "le nombre de pages doit être supérieur à %{min} (le nombre actuel est de %{pages})"
|
data/config/locales/it.yml
CHANGED
|
@@ -55,6 +55,8 @@ it:
|
|
|
55
55
|
aspect_ratio_not_x_y: "deve essere %{authorized_aspect_ratios} (il file corrente è %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "ha un formato non valido (i rapporti di aspetto validi sono %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "non è identificato come file multimediale valido"
|
|
58
|
+
audio_missing: "deve contenere una traccia audio"
|
|
59
|
+
audio_present: "non deve contenere una traccia audio"
|
|
58
60
|
pages_not_less_than: "il numero di pagine deve essere inferiore a %{max} (il numero di pagine corrente è %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "il numero di pagine deve essere inferiore o uguale a %{max} (il numero di pagine corrente è %{pages})"
|
|
60
62
|
pages_not_greater_than: "il numero di pagine deve essere maggiore di %{min} (il numero di pagine corrente è %{pages})"
|
data/config/locales/ja.yml
CHANGED
|
@@ -55,6 +55,8 @@ ja:
|
|
|
55
55
|
aspect_ratio_not_x_y: "のアスペクト比は%{authorized_aspect_ratios}にしてください (添付されたメディアの解像度は%{width}x%{height})"
|
|
56
56
|
aspect_ratio_invalid: "のアスペクト比を検出できません (許可されたアスペクト比は%{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "のメディアを処理できません"
|
|
58
|
+
audio_missing: "には音声トラックが必要です"
|
|
59
|
+
audio_present: "に音声トラックを含めることはできません"
|
|
58
60
|
pages_not_less_than: "のページ数は%{max}未満にしてください (添付されたファイルは%{pages}ページ)"
|
|
59
61
|
pages_not_less_than_or_equal_to: "のページ数は%{max}以下にしてください (添付されたファイルは%{pages}ページ)"
|
|
60
62
|
pages_not_greater_than: "のページ数は%{min}より大きくしてください (添付されたファイルは%{pages}ページ)"
|
data/config/locales/nl.yml
CHANGED
|
@@ -55,6 +55,8 @@ nl:
|
|
|
55
55
|
aspect_ratio_not_x_y: "moet %{authorized_aspect_ratios} zijn (huidig bestand is %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "heeft een ongeldige beeldverhouding (geldige beeldverhoudingen zijn %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "wordt niet geïdentificeerd als een geldig mediabestand"
|
|
58
|
+
audio_missing: "moet een audiotrack bevatten"
|
|
59
|
+
audio_present: "mag geen audiotrack bevatten"
|
|
58
60
|
pages_not_less_than: "het aantal pagina's moet kleiner zijn dan %{max} (het huidige aantal pagina's is %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "het aantal pagina's moet kleiner zijn dan of gelijk zijn aan %{max} (het huidige aantal pagina's is %{pages})"
|
|
60
62
|
pages_not_greater_than: "het aantal pagina's moet groter zijn dan %{min} (het huidige aantal pagina's is %{pages})"
|
data/config/locales/pl.yml
CHANGED
|
@@ -55,6 +55,8 @@ pl:
|
|
|
55
55
|
aspect_ratio_not_x_y: "musi być %{authorized_aspect_ratios} (bieżący plik to %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "ma nieprawidłowy współczynnik kształtu (ważne współczynniki kształtu wynoszą %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "nie jest identyfikowany jako prawidłowy plik multimedialny"
|
|
58
|
+
audio_missing: "musi zawierać ścieżkę dźwiękową"
|
|
59
|
+
audio_present: "nie może zawierać ścieżki dźwiękowej"
|
|
58
60
|
pages_not_less_than: "liczba stron musi być mniejsza niż %{max} (bieżąca liczba stron to %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "liczba stron musi być mniejsza lub równa %{max} (bieżąca liczba stron to %{pages})"
|
|
60
62
|
pages_not_greater_than: "liczba stron musi być większa niż %{min} (bieżąca liczba stron to %{pages})"
|
data/config/locales/pt-BR.yml
CHANGED
|
@@ -55,6 +55,8 @@ pt-BR:
|
|
|
55
55
|
aspect_ratio_not_x_y: "deve ser %{authorized_aspect_ratios} (o arquivo atual é %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "tem uma proporção inválida (as proporções válidas são %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "não é identificado como um arquivo de mídia válido"
|
|
58
|
+
audio_missing: "deve ter uma faixa de áudio"
|
|
59
|
+
audio_present: "não deve ter uma faixa de áudio"
|
|
58
60
|
pages_not_less_than: "a contagem de páginas deve ser menor que %{max} (a contagem atual de páginas é %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "a contagem de páginas deve ser menor ou igual a %{max} (a contagem atual de páginas é %{pages})"
|
|
60
62
|
pages_not_greater_than: "a contagem de páginas deve ser maior que %{min} (a contagem atual de páginas é %{pages})"
|
data/config/locales/ru.yml
CHANGED
|
@@ -55,6 +55,8 @@ ru:
|
|
|
55
55
|
aspect_ratio_not_x_y: "должно быть %{authorized_aspect_ratios} (текущий файл %{width}x%{height} пикселей)"
|
|
56
56
|
aspect_ratio_invalid: "имеет недействительное соотношение сторон (действительные соотношения сторон %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "не идентифицируется как действительный медиафайл"
|
|
58
|
+
audio_missing: "должен содержать аудиодорожку"
|
|
59
|
+
audio_present: "не должен содержать аудиодорожку"
|
|
58
60
|
pages_not_less_than: "количество страниц должно быть меньше %{max} (текущее количество страниц %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "количество страниц должно быть меньше или равно %{max} (текущее количество страниц %{pages})"
|
|
60
62
|
pages_not_greater_than: "количество страниц должно быть больше %{min} (текущее количество страниц %{pages})"
|
data/config/locales/sv.yml
CHANGED
|
@@ -55,6 +55,8 @@ sv:
|
|
|
55
55
|
aspect_ratio_not_x_y: "måste vara% { authorized_aspect_ratios } (nuvarande fil är %{width}x%{height} pixlar)"
|
|
56
56
|
aspect_ratio_invalid: "har ett ogiltigt bildförhållande (giltiga bildförhållanden är %{förväntat_aspect_ratios})"
|
|
57
57
|
file_not_processable: "identifieras inte som en giltig mediefil"
|
|
58
|
+
audio_missing: "måste innehålla ett ljudspår"
|
|
59
|
+
audio_present: "får inte innehålla ett ljudspår"
|
|
58
60
|
pages_not_less_than: "sidantal måste vara mindre än %{max} (nuvarande sidantal är %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "sidantal måste vara mindre än eller lika med %{max} (nuvarande antal sidor är %{pages})"
|
|
60
62
|
pages_not_greater_than: "sidantal måste vara större än %{min} (nuvarande sidantal är %{pages})"
|
data/config/locales/tr.yml
CHANGED
|
@@ -55,6 +55,8 @@ tr:
|
|
|
55
55
|
aspect_ratio_not_x_y: "%{authorized_aspect_ratios} olmalıdır (geçerli dosya %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "geçersiz bir en boy oranına sahiptir (geçerli en boy oranları %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "geçerli bir medya dosyası olarak tanımlanmadı"
|
|
58
|
+
audio_missing: "bir ses parçası içermelidir"
|
|
59
|
+
audio_present: "bir ses parçası içermemelidir"
|
|
58
60
|
pages_not_less_than: "sayfa sayısı %{max} değerinden az olmalıdır (geçerli sayfa sayısı %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "sayfa sayısı %{max} değerinden az veya eşit olmalıdır (geçerli sayfa sayısı %{pages})"
|
|
60
62
|
pages_not_greater_than: "sayfa sayısı %{min} değerinden fazla olmalıdır (geçerli sayfa sayısı %{pages})"
|
data/config/locales/uk.yml
CHANGED
|
@@ -55,6 +55,8 @@ uk:
|
|
|
55
55
|
aspect_ratio_not_x_y: "повинен бути %{authorized_aspect_ratios} (поточний файл становить %{width}x%{height} пікселям)"
|
|
56
56
|
aspect_ratio_invalid: "має недійсне співвідношення сторін (дійсні співвідношення сторін %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "не ідентифікується як дійсний медіа-файл"
|
|
58
|
+
audio_missing: "має містити аудіодоріжку"
|
|
59
|
+
audio_present: "не має містити аудіодоріжку"
|
|
58
60
|
pages_not_less_than: "кількість сторінок має бути менше %{max} (поточна кількість сторінок %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "кількість сторінок має бути меншою або дорівнювати %{max} (поточна кількість сторінок становить %{pages})"
|
|
60
62
|
pages_not_greater_than: "кількість сторінок має бути більшою за %{min} (поточна кількість сторінок становить %{pages})"
|
data/config/locales/vi.yml
CHANGED
|
@@ -55,6 +55,8 @@ vi:
|
|
|
55
55
|
aspect_ratio_not_x_y: "phải là %{authorized_aspect_ratios} (tệp hiện tại là %{width}x%{height}px)"
|
|
56
56
|
aspect_ratio_invalid: "có tỷ lệ khung hình không hợp lệ (tỷ lệ khung hình hợp lệ là %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "không được xác định là tệp phương tiện hợp lệ"
|
|
58
|
+
audio_missing: "phải có một bản âm thanh"
|
|
59
|
+
audio_present: "không được chứa bản âm thanh"
|
|
58
60
|
pages_not_less_than: "số trang phải nhỏ hơn %{max} (số trang hiện tại là %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "số trang phải nhỏ hơn hoặc bằng %{max} (số trang hiện tại là %{pages})"
|
|
60
62
|
pages_not_greater_than: "số trang phải lớn hơn %{min} (số trang hiện tại là %{pages})"
|
data/config/locales/zh-CN.yml
CHANGED
|
@@ -55,6 +55,8 @@ zh-CN:
|
|
|
55
55
|
aspect_ratio_not_x_y: "必须为 %{authorized_aspect_ratios}(当前文件为 %{width} x%{height} 像素)"
|
|
56
56
|
aspect_ratio_invalid: "具有无效的长宽比(有效长宽比为 %{authorized_aspect_ratios})"
|
|
57
57
|
file_not_processable: "未标识为有效的媒体文件"
|
|
58
|
+
audio_missing: "必须包含音轨"
|
|
59
|
+
audio_present: "不得包含音轨"
|
|
58
60
|
pages_not_less_than: "页数必须小于 %{max}(当前页数为 %{pages})"
|
|
59
61
|
pages_not_less_than_or_equal_to: "页数必须小于或等于 %{max}(当前页数为 %{pages})"
|
|
60
62
|
pages_not_greater_than: "页数必须大于 %{min}(当前页数为 %{pages})"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveStorageValidations
|
|
4
|
+
# Content-type sniffer using the UNIX {file}[https://en.wikipedia.org/wiki/File_(command)] command.
|
|
5
|
+
#
|
|
6
|
+
# Override the binary path with +ActiveStorage.paths[:file]+.
|
|
7
|
+
class Analyzer::ContentTypeAnalyzer::File < Analyzer::ContentTypeAnalyzer
|
|
8
|
+
class CommandLineToolNotInstalledError < StandardError; end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def backend
|
|
13
|
+
:file
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def media_from_path(path)
|
|
17
|
+
instrument("file") do |payload|
|
|
18
|
+
result = run_command(file_path, "-b", "--mime-type", path, payload: payload)
|
|
19
|
+
result.success? ? result.stdout.strip : nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def file_path
|
|
24
|
+
ActiveStorage.paths[:file] || "file"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def missing_backend_error
|
|
28
|
+
CommandLineToolNotInstalledError.new("file command-line tool is not installed")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ActiveStorageValidations
|
|
6
|
+
# Content-type sniffer using the Google {Magika}[https://github.com/google/magika] CLI.
|
|
7
|
+
#
|
|
8
|
+
# Override the binary path with +ActiveStorage.paths[:magika]+.
|
|
9
|
+
class Analyzer::ContentTypeAnalyzer::Magika < Analyzer::ContentTypeAnalyzer
|
|
10
|
+
class CommandLineToolNotInstalledError < StandardError; end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def backend
|
|
15
|
+
:magika
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def media_from_path(path)
|
|
19
|
+
instrument("magika") do |payload|
|
|
20
|
+
result = run_command(magika_path, "--json", path, payload: payload)
|
|
21
|
+
result.success? ? mime_type_from_magika_json(result.stdout) : nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def mime_type_from_magika_json(stdout)
|
|
26
|
+
parsed = JSON.parse(stdout)
|
|
27
|
+
entry = parsed.is_a?(Array) ? parsed.first : parsed
|
|
28
|
+
return nil unless entry.is_a?(Hash)
|
|
29
|
+
return nil unless entry.dig("result", "status") == "ok"
|
|
30
|
+
|
|
31
|
+
entry.dig("result", "value", "output", "mime_type").presence
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def magika_path
|
|
37
|
+
ActiveStorage.paths[:magika] || "magika"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def missing_backend_error
|
|
41
|
+
CommandLineToolNotInstalledError.new("magika command-line tool is not installed")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "open3"
|
|
4
|
-
|
|
5
3
|
module ActiveStorageValidations
|
|
6
4
|
# = ActiveStorageValidations ContentType \Analyzer
|
|
7
5
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# Example:
|
|
6
|
+
# Abstract base for content-type sniffers used by +spoofing_protection+.
|
|
7
|
+
# Concrete backends:
|
|
12
8
|
#
|
|
13
|
-
# ActiveStorageValidations::Analyzer::ContentTypeAnalyzer.new(attachable).content_type
|
|
14
|
-
# # => { content_type: "image/png" }
|
|
9
|
+
# ActiveStorageValidations::Analyzer::ContentTypeAnalyzer::File.new(attachable).content_type
|
|
10
|
+
# # => { content_type: "image/png", content_type_backend: "file" }
|
|
15
11
|
#
|
|
16
|
-
#
|
|
12
|
+
# ActiveStorageValidations::Analyzer::ContentTypeAnalyzer::Magika.new(attachable).content_type
|
|
13
|
+
# # => { content_type: "image/png", content_type_backend: "magika" }
|
|
17
14
|
class Analyzer::ContentTypeAnalyzer < Analyzer
|
|
18
|
-
class FileCommandLineToolNotInstalledError < StandardError; end
|
|
19
|
-
|
|
20
15
|
def content_type
|
|
21
16
|
read_media do |media|
|
|
22
17
|
{
|
|
23
|
-
content_type: media
|
|
18
|
+
content_type: media,
|
|
19
|
+
content_type_backend: backend.to_s
|
|
24
20
|
}
|
|
25
21
|
end
|
|
26
22
|
end
|
|
@@ -33,7 +29,7 @@ module ActiveStorageValidations
|
|
|
33
29
|
if media(tempfile).present?
|
|
34
30
|
yield media(tempfile)
|
|
35
31
|
else
|
|
36
|
-
logger.info "Skipping
|
|
32
|
+
logger.info "Skipping content_type analysis because #{backend} doesn't support the file"
|
|
37
33
|
nil
|
|
38
34
|
end
|
|
39
35
|
ensure
|
|
@@ -41,20 +37,15 @@ module ActiveStorageValidations
|
|
|
41
37
|
end
|
|
42
38
|
end
|
|
43
39
|
rescue Errno::ENOENT
|
|
44
|
-
raise
|
|
40
|
+
raise missing_backend_error
|
|
45
41
|
end
|
|
46
42
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"file",
|
|
51
|
-
"-b",
|
|
52
|
-
"--mime-type",
|
|
53
|
-
path
|
|
54
|
-
)
|
|
43
|
+
def backend
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
def missing_backend_error
|
|
48
|
+
raise NotImplementedError
|
|
58
49
|
end
|
|
59
50
|
end
|
|
60
51
|
end
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveStorageValidations
|
|
4
|
-
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem
|
|
4
|
+
# This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem
|
|
5
|
+
# only to resolve the ImageMagick / GraphicsMagick +identify+ argv. Commands are executed through
|
|
6
|
+
# {ASVCommandable#run_command} so +command_timeout+ can kill hung processes.
|
|
5
7
|
# MiniMagick requires the {ImageMagick}[http://www.imagemagick.org] system library.
|
|
6
8
|
# This is the default Rails image analyzer.
|
|
7
9
|
class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
|
|
10
|
+
ImageInfo = Struct.new(:width, :height, :orientation, keyword_init: true) do
|
|
11
|
+
def valid?
|
|
12
|
+
width.to_i.positive? && height.to_i.positive?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](key)
|
|
16
|
+
orientation if key == "%[orientation]"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
private
|
|
9
21
|
|
|
10
22
|
def read_media
|
|
11
23
|
Tempfile.create(binmode: true) do |tempfile|
|
|
12
24
|
begin
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
info = media(tempfile)
|
|
26
|
+
if info&.valid?
|
|
27
|
+
yield info
|
|
15
28
|
else
|
|
16
29
|
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
|
|
17
30
|
{}
|
|
@@ -20,17 +33,38 @@ module ActiveStorageValidations
|
|
|
20
33
|
tempfile.close
|
|
21
34
|
end
|
|
22
35
|
end
|
|
23
|
-
rescue
|
|
24
|
-
logger.
|
|
36
|
+
rescue Errno::ENOENT
|
|
37
|
+
logger.info "Skipping image analysis because ImageMagick isn't installed"
|
|
25
38
|
{}
|
|
26
39
|
end
|
|
27
40
|
|
|
28
41
|
def media_from_path(path)
|
|
29
|
-
instrument("mini_magick") do
|
|
30
|
-
|
|
42
|
+
instrument("mini_magick") do |payload|
|
|
43
|
+
result = run_command(*identify_command(path), payload: payload)
|
|
44
|
+
return nil unless result.success?
|
|
45
|
+
|
|
46
|
+
parse_identify_output(result.stdout)
|
|
31
47
|
end
|
|
32
48
|
end
|
|
33
49
|
|
|
50
|
+
def identify_command(path)
|
|
51
|
+
require "mini_magick"
|
|
52
|
+
|
|
53
|
+
tool = ::MiniMagick::Tool.new("identify")
|
|
54
|
+
tool.format("%w\t%h\t%[orientation]")
|
|
55
|
+
tool << path
|
|
56
|
+
tool.command
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_identify_output(stdout)
|
|
60
|
+
width, height, orientation = stdout.to_s.strip.split("\t", 3)
|
|
61
|
+
ImageInfo.new(
|
|
62
|
+
width: width.to_i,
|
|
63
|
+
height: height.to_i,
|
|
64
|
+
orientation: orientation.to_s
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
34
68
|
def rotated_image?(image)
|
|
35
69
|
%w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
|
|
36
70
|
end
|
|
@@ -16,6 +16,9 @@ module ActiveStorageValidations
|
|
|
16
16
|
{}
|
|
17
17
|
end
|
|
18
18
|
ensure
|
|
19
|
+
# Best-effort: avoid unlinking the tempfile while a timed-out FFI load
|
|
20
|
+
# may still be using the path.
|
|
21
|
+
wait_for_vips_load_thread
|
|
19
22
|
tempfile.close
|
|
20
23
|
end
|
|
21
24
|
end
|
|
@@ -25,19 +28,42 @@ module ActiveStorageValidations
|
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def media_from_path(path)
|
|
28
|
-
instrument("vips") do
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
instrument("vips") do |payload|
|
|
32
|
+
load_vips_image(path, payload)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_vips_image(path, payload)
|
|
37
|
+
timeout = timeout_in_seconds
|
|
38
|
+
return open_vips_image(path) if timeout.nil?
|
|
39
|
+
|
|
40
|
+
result = nil
|
|
41
|
+
@vips_load_thread = Thread.new { result = open_vips_image(path) }
|
|
42
|
+
|
|
43
|
+
if @vips_load_thread.join(timeout)
|
|
44
|
+
result
|
|
45
|
+
else
|
|
46
|
+
# libvips work may continue in the background until the C call returns.
|
|
47
|
+
mark_timed_out!(payload, "vips")
|
|
48
|
+
nil
|
|
38
49
|
end
|
|
39
50
|
end
|
|
40
51
|
|
|
52
|
+
def wait_for_vips_load_thread
|
|
53
|
+
return unless @vips_load_thread&.alive?
|
|
54
|
+
|
|
55
|
+
@vips_load_thread.join(0.5)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns nil for unsupported / empty files instead of raising.
|
|
59
|
+
# Vips raises on unreadable input (e.g. 0-byte files) rather than returning
|
|
60
|
+
# a falsy image — see https://github.com/janko/image_processing/issues/97
|
|
61
|
+
def open_vips_image(path)
|
|
62
|
+
::Vips::Image.new_from_file(path, access: :sequential)
|
|
63
|
+
rescue ::Vips::Error
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
41
67
|
ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
|
|
42
68
|
def rotated_image?(image)
|
|
43
69
|
ROTATIONS === image.get("exif-ifd0-Orientation")
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "open3"
|
|
4
|
-
|
|
5
3
|
module ActiveStorageValidations
|
|
6
4
|
# = ActiveStorageValidations PDF \Analyzer
|
|
7
5
|
#
|
|
@@ -49,13 +47,9 @@ module ActiveStorageValidations
|
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
def media_from_path(path)
|
|
52
|
-
instrument(File.basename(pdfinfo_path)) do
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
path
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
status.success? ? stdout_to_hash(stdout) : nil
|
|
50
|
+
instrument(File.basename(pdfinfo_path)) do |payload|
|
|
51
|
+
result = run_command(pdfinfo_path, path, payload: payload)
|
|
52
|
+
result.success? ? stdout_to_hash(result.stdout) : nil
|
|
59
53
|
end
|
|
60
54
|
end
|
|
61
55
|
|
|
@@ -28,17 +28,18 @@ module ActiveStorageValidations
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def media_from_path(path)
|
|
31
|
-
instrument(File.basename(ffprobe_path)) do
|
|
32
|
-
|
|
31
|
+
instrument(File.basename(ffprobe_path)) do |payload|
|
|
32
|
+
result = run_command(
|
|
33
33
|
ffprobe_path,
|
|
34
34
|
"-print_format", "json",
|
|
35
35
|
"-show_streams",
|
|
36
36
|
"-show_format",
|
|
37
37
|
"-v", "error",
|
|
38
|
-
path
|
|
38
|
+
path,
|
|
39
|
+
payload: payload
|
|
39
40
|
)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
result.success? ? JSON.parse(result.stdout) : nil
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "shared/asv_attachable"
|
|
4
|
+
require_relative "shared/asv_commandable"
|
|
4
5
|
require_relative "shared/asv_loggable"
|
|
5
6
|
|
|
6
7
|
module ActiveStorageValidations
|
|
@@ -12,12 +13,14 @@ module ActiveStorageValidations
|
|
|
12
13
|
# Heavily (not to say 100%) inspired by Rails own ActiveStorage::Analyzer
|
|
13
14
|
class Analyzer
|
|
14
15
|
include ASVAttachable
|
|
16
|
+
include ASVCommandable
|
|
15
17
|
include ASVLoggable
|
|
16
18
|
|
|
17
19
|
attr_reader :attachable
|
|
18
20
|
|
|
19
|
-
def initialize(attachable)
|
|
21
|
+
def initialize(attachable, timeout: ActiveStorageValidations.command_timeout)
|
|
20
22
|
@attachable = attachable
|
|
23
|
+
@timeout = timeout
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
# Override this method in a concrete subclass. Have it return a String content type.
|
|
@@ -81,8 +84,37 @@ module ActiveStorageValidations
|
|
|
81
84
|
raise NotImplementedError
|
|
82
85
|
end
|
|
83
86
|
|
|
84
|
-
def
|
|
85
|
-
|
|
87
|
+
def timeout_in_seconds
|
|
88
|
+
return nil if @timeout.nil?
|
|
89
|
+
|
|
90
|
+
@timeout.to_f
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def instrument(analyzer)
|
|
94
|
+
payload = {
|
|
95
|
+
analyzer: analyzer,
|
|
96
|
+
command: analyzer,
|
|
97
|
+
timeout: timeout_in_seconds,
|
|
98
|
+
timed_out: false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ActiveSupport::Notifications.instrument("analyze.active_storage_validations", payload) do
|
|
102
|
+
started_at = monotonic_time
|
|
103
|
+
result = yield payload
|
|
104
|
+
payload[:duration] = monotonic_time - started_at
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def mark_timed_out!(payload, command)
|
|
110
|
+
payload[:timed_out] = true if payload
|
|
111
|
+
ActiveSupport::Notifications.instrument(
|
|
112
|
+
"timeout.active_storage_validations",
|
|
113
|
+
analyzer: payload&.[](:analyzer) || command,
|
|
114
|
+
command: command,
|
|
115
|
+
timeout: timeout_in_seconds
|
|
116
|
+
)
|
|
117
|
+
logger.info "Skipping file analysis because #{command} timed out after #{timeout_in_seconds} seconds"
|
|
86
118
|
end
|
|
87
119
|
end
|
|
88
120
|
end
|