file_validators 2.0.2 → 3.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +32 -0
  4. data/.tool-versions +1 -0
  5. data/.travis.yml +33 -5
  6. data/Appraisals +16 -10
  7. data/CHANGELOG.md +26 -0
  8. data/Gemfile +2 -0
  9. data/README.md +32 -21
  10. data/Rakefile +3 -1
  11. data/file_validators.gemspec +13 -7
  12. data/gemfiles/activemodel_3.2.gemfile +2 -1
  13. data/gemfiles/activemodel_4.0.gemfile +2 -1
  14. data/gemfiles/activemodel_4.1.gemfile +1 -0
  15. data/gemfiles/activemodel_4.2.gemfile +2 -1
  16. data/gemfiles/{activemodel_3.0.gemfile → activemodel_5.0.gemfile} +1 -1
  17. data/gemfiles/{activemodel_3.1.gemfile → activemodel_5.2.gemfile} +1 -1
  18. data/lib/file_validators.rb +11 -2
  19. data/lib/file_validators/error.rb +6 -0
  20. data/lib/file_validators/locale/en.yml +0 -2
  21. data/lib/file_validators/mime_type_analyzer.rb +106 -0
  22. data/lib/file_validators/validators/file_content_type_validator.rb +37 -42
  23. data/lib/file_validators/validators/file_size_validator.rb +62 -19
  24. data/lib/file_validators/version.rb +3 -1
  25. data/spec/integration/combined_validators_integration_spec.rb +3 -1
  26. data/spec/integration/file_content_type_validation_integration_spec.rb +117 -11
  27. data/spec/integration/file_size_validator_integration_spec.rb +100 -10
  28. data/spec/lib/file_validators/mime_type_analyzer_spec.rb +139 -0
  29. data/spec/lib/file_validators/validators/file_content_type_validator_spec.rb +93 -36
  30. data/spec/lib/file_validators/validators/file_size_validator_spec.rb +87 -34
  31. data/spec/spec_helper.rb +9 -1
  32. data/spec/support/fakeio.rb +17 -0
  33. data/spec/support/helpers.rb +7 -0
  34. data/spec/support/matchers/allow_content_type.rb +2 -0
  35. data/spec/support/matchers/allow_file_size.rb +2 -0
  36. metadata +87 -27
  37. data/lib/file_validators/utils/content_type_detector.rb +0 -46
  38. data/lib/file_validators/utils/media_type_spoof_detector.rb +0 -46
  39. data/spec/lib/file_validators/utils/content_type_detector_spec.rb +0 -27
  40. data/spec/lib/file_validators/utils/media_type_spoof_detector_spec.rb +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ff6c87fab7ee11b1ce0fb5770332f0f1b22c5b96
4
- data.tar.gz: c5b54b8b7b01b685b0f9ee3097c3f8cecbe8ddc0
2
+ SHA256:
3
+ metadata.gz: 7536b453f528e82937d43e4349c6cb99f990832023a5567d125b817b59f3b71a
4
+ data.tar.gz: e33270b079d15c08aed6a2b3e1f2aab9f61d39fc222f9c42e15a0a9a34f82885
5
5
  SHA512:
6
- metadata.gz: 87bf7a8b288777610e8365b486ba806715ee155cc2c5b808aadea4b4f95e1bb18398d394b10af215ffde63fe0babbbc9ea70d265005a727cb31d7b01966ad69a
7
- data.tar.gz: d05f373a6c43907891cbdc0358e3f0389027b2b10b41d788a83db65cb6edca23b3bdf3910bd82c8ebb45b350c368b94884033a0f98f5d65093bf00ff7b0d034d
6
+ metadata.gz: 2c47e7fe802dc65553c2dfa5a72c2d05d97d4d14dcdf2c6826f7ae18e586a8754b1b4dc07c20abec2038b55c6d1766df96c19f968217630270586c92bab4101f
7
+ data.tar.gz: b8b0c693d4314614c2cd8f72a699b48c666487c4b44221f25daf9f70cc0d27d1b09766b9ef8e035ed14690ea9d24aac10cdbcc62d1e89bf2a683e46a7e39fec0
data/.gitignore CHANGED
@@ -10,3 +10,6 @@ Gemfile.lock
10
10
  gemfiles/*.lock
11
11
  coverage/
12
12
  /.idea
13
+ .ruby-version
14
+ .agignore
15
+ tags
@@ -0,0 +1,32 @@
1
+ Bundler/OrderedGems:
2
+ Enabled: false
3
+
4
+ Style/Documentation:
5
+ Enabled: false
6
+
7
+ Style/MissingRespondToMissing:
8
+ Enabled: false
9
+
10
+ Style/CaseEquality:
11
+ Enabled: false
12
+
13
+ Style/GuardClause:
14
+ Enabled: false
15
+
16
+ Style/RegexpLiteral:
17
+ Enabled: false
18
+
19
+ Style/Next:
20
+ Enabled: false
21
+
22
+ Metrics/LineLength:
23
+ Max: 110
24
+
25
+ Metrics/ModuleLength:
26
+ Enabled: false
27
+
28
+ Metrics/BlockLength:
29
+ Enabled: false
30
+
31
+ Metrics/MethodLength:
32
+ Enabled: false
@@ -0,0 +1 @@
1
+ ruby 2.3.1
@@ -1,11 +1,39 @@
1
1
  language: ruby
2
+
2
3
  rvm:
3
- - 1.9.3
4
- - 2.2.2
4
+ - 2.2.3
5
+ - 2.5.0
6
+ - ruby-head
7
+ - jruby-9.0.4.0
8
+ - jruby-9.1.7.0
9
+
5
10
  gemfile:
11
+ - gemfiles/activemodel_5.2.gemfile
12
+ - gemfiles/activemodel_5.0.gemfile
6
13
  - gemfiles/activemodel_4.2.gemfile
7
- - gemfiles/activemodel_4.1.gemfile
8
14
  - gemfiles/activemodel_4.0.gemfile
9
15
  - gemfiles/activemodel_3.2.gemfile
10
- - gemfiles/activemodel_3.1.gemfile
11
- - gemfiles/activemodel_3.0.gemfile
16
+
17
+ matrix:
18
+ exclude:
19
+ - rvm: 2.5.0
20
+ gemfile: gemfiles/activemodel_3.2.gemfile
21
+ - rvm: 2.5.0
22
+ gemfile: gemfiles/activemodel_4.0.gemfile
23
+ - rvm: 2.5.0
24
+ gemfile: gemfiles/activemodel_4.2.gemfile
25
+
26
+ - rvm: ruby-head
27
+ gemfile: gemfiles/activemodel_3.2.gemfile
28
+ - rvm: ruby-head
29
+ gemfile: gemfiles/activemodel_4.0.gemfile
30
+ - rvm: ruby-head
31
+ gemfile: gemfiles/activemodel_4.2.gemfile
32
+
33
+ - rvm: jruby-9.0.4.0
34
+ gemfile: gemfiles/activemodel_5.0.gemfile
35
+ - rvm: jruby-9.0.4.0
36
+ gemfile: gemfiles/activemodel_5.2.gemfile
37
+
38
+ allow_failures:
39
+ - rvm: ruby-head
data/Appraisals CHANGED
@@ -1,23 +1,29 @@
1
- appraise 'activemodel-3.0' do
2
- gem 'activemodel', '3.0.20'
3
- end
4
-
5
- appraise 'activemodel-3.1' do
6
- gem 'activemodel', '3.1.12'
7
- end
1
+ # frozen_string_literal: true
8
2
 
9
3
  appraise 'activemodel-3.2' do
10
- gem 'activemodel', '3.2.18'
4
+ gem 'activemodel', '3.2.22.5'
5
+ gem 'rack', '1.6.5'
11
6
  end
12
7
 
13
8
  appraise 'activemodel-4.0' do
14
- gem 'activemodel', '4.0.10'
9
+ gem 'activemodel', '4.0.13'
10
+ gem 'rack', '1.6.5'
15
11
  end
16
12
 
17
13
  appraise 'activemodel-4.1' do
18
14
  gem 'activemodel', '4.1.6'
15
+ gem 'rack', '1.6.5'
19
16
  end
20
17
 
21
18
  appraise 'activemodel-4.2' do
22
- gem 'activemodel', '4.2.3'
19
+ gem 'activemodel', '4.2.7.1'
20
+ gem 'rack', '1.6.5'
21
+ end
22
+
23
+ appraise 'activemodel-5.0' do
24
+ gem 'activemodel', '5.0.1'
25
+ end
26
+
27
+ appraise 'activemodel-5.2' do
28
+ gem 'activemodel', '5.2.1'
23
29
  end
@@ -0,0 +1,26 @@
1
+ # 3.0.0.beta2
2
+
3
+ * [#32](https://github.com/musaffa/file_validators/pull/32) Removed terrapin. Added options for choosing MIME type analyzers with `:tool` option.
4
+ * Rubocop style guide
5
+
6
+ # 3.0.0.beta1
7
+
8
+ * [#29](https://github.com/musaffa/file_validators/pull/29) Upgrade cocaine to terrapin
9
+ * Rubocop style guide
10
+
11
+ # 2.3.0
12
+
13
+ * [#19](https://github.com/musaffa/file_validators/pull/19) Return false with blank size
14
+ * [#27](https://github.com/musaffa/file_validators/pull/27) Fix file size validator for ActiveStorage
15
+
16
+ # 2.2.0-beta.1
17
+
18
+ * [#17](https://github.com/musaffa/file_validators/pull/17) Now Supports multiple file uploads
19
+ * As activemodel 3.0 and 3.1 doesn't support `added?` method on the Errors class, the support for both of them have been deprecated in this release.
20
+
21
+ # 2.1.0
22
+
23
+ * Use autoload for lazy loading of libraries.
24
+ * Media type spoof valiation is moved to content type detector.
25
+ * `spoofed_file_media_type` message isn't needed anymore.
26
+ * Logger info and warning is added.
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Declare your gem's dependencies in file_validators.gemspec.
data/README.md CHANGED
@@ -12,8 +12,11 @@ Any module that uses ActiveModel, for example ActiveRecord, can use these file v
12
12
 
13
13
  ## Support
14
14
 
15
- * ActiveModel versions: 3 and 4.
16
- * Rails versions: 3 and 4.
15
+ * ActiveModel versions: 3.2, 4 and 5.
16
+ * Rails versions: 3.2, 4 and 5.
17
+
18
+ As of version `2.2`, activemodel 3.0 and 3.1 will no longer be supported.
19
+ For activemodel 3.0 and 3.1, please use file_validators version `<= 2.1`.
17
20
 
18
21
  It has been tested to work with Carrierwave, Paperclip, Dragonfly, Refile etc file uploading solutions.
19
22
  Validations works both before and after uploads.
@@ -36,7 +39,7 @@ class Profile
36
39
 
37
40
  attr_accessor :avatar
38
41
  validates :avatar, file_size: { less_than_or_equal_to: 100.kilobytes },
39
- file_content_type: { allow: ['image/jpeg', 'image/png'] }
42
+ file_content_type: { allow: ['image/jpeg', 'image/png'] }
40
43
  end
41
44
  ```
42
45
  ActiveRecord example:
@@ -64,23 +67,23 @@ validates :avatar, file_size: { less_than: 2.gigabytes }
64
67
  ```
65
68
  * `less_than_or_equal_to`: Less than or equal to a number in bytes or a proc that returns a number
66
69
  ```ruby
67
- validates :avatar, file_size: { less_than_or_equal_to: 50.bytes }
70
+ validates :avatar, file_size: { less_than_or_equal_to: 50.bytes }
68
71
  ```
69
72
  * `greater_than`: greater than a number in bytes or a proc that returns a number
70
73
  ```ruby
71
- validates :avatar, file_size: { greater_than: 1.byte }
74
+ validates :avatar, file_size: { greater_than: 1.byte }
72
75
  ```
73
76
  * `greater_than_or_equal_to`: Greater than or equal to a number in bytes or a proc that returns a number
74
77
  ```ruby
75
- validates :avatar, file_size: { greater_than_or_equal_to: 50.bytes }
78
+ validates :avatar, file_size: { greater_than_or_equal_to: 50.bytes }
76
79
  ```
77
- * `message`: Error message to display. With all the options above except `:in`, you will get `count` as a replacement.
78
- With `:in` you will get `min` and `max` as replacements.
80
+ * `message`: Error message to display. With all the options above except `:in`, you will get `count` as a replacement.
81
+ With `:in` you will get `min` and `max` as replacements.
79
82
  `count`, `min` and `max` each will have its value and unit together.
80
83
  You can write error messages without using any replacement.
81
84
  ```ruby
82
85
  validates :avatar, file_size: { less_than: 100.kilobytes,
83
- message: 'avatar should be less than %{count}' }
86
+ message: 'avatar should be less than %{count}' }
84
87
  ```
85
88
  ```ruby
86
89
  validates :document, file_size: { in: 1.kilobyte..1.megabyte,
@@ -139,8 +142,13 @@ validates :video, file_content_type: { allow: lambda { |record| record.content_t
139
142
  can be a String or a Regexp. It also accepts `proc`. See `:allow` options examples.
140
143
  * `mode`: `:strict` or `:relaxed`. `:strict` mode can detect content type based on the contents
141
144
  of the files. It also detects media type spoofing (see more in [security](#security)).
142
- `:relaxed` mode uses file name to detect the content type using `mime-types` gem.
143
- If mode option is not set then the validator uses form supplied content type.
145
+ `:file` analyzer is used in `:strict` model. `:relaxed` mode uses file name to detect
146
+ the content type. `mime_types` analyzer is used in `relaxed` mode. If mode option is not
147
+ set then the validator uses form supplied content type.
148
+ * `tool`: `:file`, `:fastimage`, `:filemagic`, `:mimemagic`, `:marcel`, `:mime_types`, `:mini_mime`.
149
+ You can choose one of these built-in MIME type analyzers. You have to install the analyzer gem you choose.
150
+ By default supplied content type is used to determine the MIME type. This option takes precedence
151
+ over `mode` option.
144
152
  ```ruby
145
153
  validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :strict }
146
154
  validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :relaxed }
@@ -169,7 +177,7 @@ validates :avatar, file_content_type: { allow: /^image\/.*/, exclude: ['image/pn
169
177
  This gem can use Unix file command to get the content type based on the content of the file rather
170
178
  than the extension. This prevents fake content types inserted in the request header.
171
179
 
172
- It also prevents file media type spoofing. For example, user may upload a .html document as
180
+ It also prevents file media type spoofing. For example, user may upload a .html document as
173
181
  a part of the EXIF header of a valid JPEG file. Content type validator will identify its content type
174
182
  as `image/jpeg` and, without spoof detection, it may pass the validation and be saved as .html document
175
183
  thus exposing your application to a security vulnerability. Media type spoof detector wont let that happen.
@@ -177,8 +185,8 @@ It will not allow a file having `image/jpeg` content type to be saved as `text/p
177
185
  type mismatch, for example `text` of `text/plain` and `image` of `image/jpeg`. So it will not prevent
178
186
  `image/jpeg` from saving as `image/png` as both have the same `image` media type.
179
187
 
180
- **note**: This security feature is disabled by default. To enable it, first add `cocaine` gem in
181
- your Gemfile and then add `mode: :strict` option in [content type validations](#file-content-type-validator).
188
+ **note**: This security feature is disabled by default. To enable it, add `mode: :strict` option
189
+ in [content type validations](#file-content-type-validator).
182
190
  `:strict` mode may not work in direct file uploading systems as the file is not passed along with the form.
183
191
 
184
192
  ## i18n Translations
@@ -191,8 +199,6 @@ File Size Errors
191
199
  * `file_size_is_greater_than_or_equal_to`: takes `count` as replacement
192
200
 
193
201
  Content Type Errors
194
- * `spoofed_file_media_type`: generated when file media type from its extension doesn't match the media type of its
195
- content. learn more from [security](#Security).
196
202
  * `allowed_file_content_types`: generated when you have specified allowed types but the content type
197
203
  of the file doesn't match. takes `types` as replacement.
198
204
  * `excluded_file_content_types`: generated when you have specified excluded types and the content type
@@ -200,7 +206,7 @@ of the file matches anyone of them. takes `types` as replacement.
200
206
 
201
207
  This gem provides `en` translations for this errors under `errors.messages` namespace.
202
208
  If you want to override and/or create other locales, you can
203
- check [this](https://github.com/musaffa/file_validators/blob/master/lib/file_validators/locale/en.yml) out to see how translations are done.
209
+ check [this](https://github.com/musaffa/file_validators/blob/master/lib/file_validators/locale/en.yml) out to see how translations are done.
204
210
 
205
211
  You can override all of them with the `:message` option.
206
212
 
@@ -249,10 +255,15 @@ uploaders start processing a file immediately after its assignment (even before
249
255
 
250
256
  ## Tests
251
257
 
252
- ```ruby
253
- rake
254
- rake test:unit
255
- rake test:integration
258
+ ```Shell
259
+ $ rake
260
+ $ rake test:unit
261
+ $ rake test:integration
262
+ $ rubocop
263
+
264
+ # test different active model versions
265
+ $ bundle exec appraisal install
266
+ $ bundle exec appraisal rake
256
267
  ```
257
268
 
258
269
  ## Problems
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'bundler/setup'
3
5
  rescue LoadError
@@ -16,7 +18,7 @@ namespace :test do
16
18
  end
17
19
  end
18
20
 
19
- task :default => ['test:unit', 'test:integration']
21
+ task default: ['test:unit', 'test:integration']
20
22
 
21
23
  # require 'rdoc/task'
22
24
 
@@ -1,4 +1,6 @@
1
- $:.push File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
4
 
3
5
  require 'file_validators/version'
4
6
 
@@ -12,17 +14,21 @@ Gem::Specification.new do |s|
12
14
  s.homepage = 'https://github.com/musaffa/file_validators'
13
15
  s.license = 'MIT'
14
16
 
15
- s.files = `git ls-files`.split($/)
17
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
18
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
19
  s.test_files = s.files.grep(%r{^spec/})
18
- s.require_paths = ['lib']
20
+ s.require_paths = ['lib']
19
21
 
20
- s.add_dependency 'activemodel', '>= 3.0'
22
+ s.add_dependency 'activemodel', '>= 3.2'
21
23
  s.add_dependency 'mime-types', '>= 1.0'
22
24
 
23
- s.add_development_dependency 'cocaine', '~> 0.5.4'
24
- s.add_development_dependency 'rake'
25
- s.add_development_dependency 'rspec', '~> 3.1.0'
26
25
  s.add_development_dependency 'coveralls'
26
+ s.add_development_dependency 'fastimage'
27
+ s.add_development_dependency 'marcel', '~> 0.3' if RUBY_VERSION >= '2.2.0'
28
+ s.add_development_dependency 'mimemagic', '>= 0.3.2'
29
+ s.add_development_dependency 'mini_mime', '~> 1.0'
27
30
  s.add_development_dependency 'rack-test'
31
+ s.add_development_dependency 'rake'
32
+ s.add_development_dependency 'rspec', '~> 3.5.0'
33
+ s.add_development_dependency 'rubocop', '~> 0.58.2'
28
34
  end
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "3.2.18"
6
+ gem "activemodel", "3.2.22.5"
7
+ gem "rack", "1.6.5"
7
8
 
8
9
  gemspec :path => "../"
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "4.0.10"
6
+ gem "activemodel", "4.0.13"
7
+ gem "rack", "1.6.5"
7
8
 
8
9
  gemspec :path => "../"
@@ -4,5 +4,6 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
6
  gem "activemodel", "4.1.6"
7
+ gem "rack", "1.6.5"
7
8
 
8
9
  gemspec :path => "../"
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "4.2.3"
6
+ gem "activemodel", "4.2.7.1"
7
+ gem "rack", "1.6.5"
7
8
 
8
9
  gemspec :path => "../"
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "3.0.20"
6
+ gem "activemodel", "5.0.1"
7
7
 
8
8
  gemspec :path => "../"
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "3.1.12"
6
+ gem "activemodel", "5.2.1"
7
7
 
8
8
  gemspec :path => "../"
@@ -1,6 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
- require 'file_validators/validators/file_size_validator'
3
- require 'file_validators/validators/file_content_type_validator'
4
+ require 'ostruct'
5
+
6
+ module FileValidators
7
+ extend ActiveSupport::Autoload
8
+ autoload :Error
9
+ autoload :MimeTypeAnalyzer
10
+ end
11
+
12
+ Dir[File.dirname(__FILE__) + '/file_validators/validators/*.rb'].each { |file| require file }
4
13
 
5
14
  locale_path = Dir.glob(File.dirname(__FILE__) + '/file_validators/locale/*.yml')
6
15
  I18n.load_path += locale_path unless I18n.load_path.include?(locale_path)
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileValidators
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -7,7 +7,5 @@ en:
7
7
  file_size_is_greater_than: ! 'file size must be greater than %{count}'
8
8
  file_size_is_greater_than_or_equal_to: ! 'file size must be greater than or equal to %{count}'
9
9
 
10
- spoofed_file_media_type: file has an extension that does not match its contents
11
10
  allowed_file_content_types: ! 'file should be one of %{types}'
12
11
  excluded_file_content_types: ! 'file cannot be %{types}'
13
-
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extracted from shrine/plugins/determine_mime_type.rb
4
+ module FileValidators
5
+ class MimeTypeAnalyzer
6
+ SUPPORTED_TOOLS = %i[fastimage file filemagic mimemagic marcel mime_types mini_mime].freeze
7
+ MAGIC_NUMBER = 256 * 1024
8
+
9
+ def initialize(tool)
10
+ raise Error, "unknown mime type analyzer #{tool.inspect}, supported analyzers are: #{SUPPORTED_TOOLS.join(',')}" unless SUPPORTED_TOOLS.include?(tool)
11
+
12
+ @tool = tool
13
+ end
14
+
15
+ def call(io)
16
+ mime_type = send(:"extract_with_#{@tool}", io)
17
+ io.rewind
18
+
19
+ mime_type
20
+ end
21
+
22
+ private
23
+
24
+ def extract_with_file(io)
25
+ require 'open3'
26
+
27
+ return nil if io.eof? # file command returns "application/x-empty" for empty files
28
+
29
+ Open3.popen3(*%W[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
30
+ begin
31
+ IO.copy_stream(io, stdin.binmode)
32
+ rescue Errno::EPIPE
33
+ end
34
+ stdin.close
35
+
36
+ status = thread.value
37
+
38
+ raise Error, "file command failed to spawn: #{stderr.read}" if status.nil?
39
+ raise Error, "file command failed: #{stderr.read}" unless status.success?
40
+ $stderr.print(stderr.read)
41
+
42
+ stdout.read.strip
43
+ end
44
+ rescue Errno::ENOENT
45
+ raise Error, 'file command-line tool is not installed'
46
+ end
47
+
48
+ def extract_with_fastimage(io)
49
+ require 'fastimage'
50
+
51
+ type = FastImage.type(io)
52
+ "image/#{type}" if type
53
+ end
54
+
55
+ def extract_with_filemagic(io)
56
+ require 'filemagic'
57
+
58
+ return nil if io.eof? # FileMagic returns "application/x-empty" for empty files
59
+
60
+ FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
61
+ filemagic.buffer(io.read(MAGIC_NUMBER))
62
+ end
63
+ end
64
+
65
+ def extract_with_mimemagic(io)
66
+ require 'mimemagic'
67
+
68
+ mime = MimeMagic.by_magic(io)
69
+ mime.type if mime
70
+ end
71
+
72
+ def extract_with_marcel(io)
73
+ require 'marcel'
74
+
75
+ return nil if io.eof? # marcel returns "application/octet-stream" for empty files
76
+
77
+ Marcel::MimeType.for(io)
78
+ end
79
+
80
+ def extract_with_mime_types(io)
81
+ require 'mime/types'
82
+
83
+ if filename = extract_filename(io)
84
+ mime_type = MIME::Types.of(filename).first
85
+ mime_type.content_type if mime_type
86
+ end
87
+ end
88
+
89
+ def extract_with_mini_mime(io)
90
+ require 'mini_mime'
91
+
92
+ if filename = extract_filename(io)
93
+ info = MiniMime.lookup_by_filename(filename)
94
+ info.content_type if info
95
+ end
96
+ end
97
+
98
+ def extract_filename(io)
99
+ if io.respond_to?(:original_filename)
100
+ io.original_filename
101
+ elsif io.respond_to?(:path)
102
+ File.basename(io.path)
103
+ end
104
+ end
105
+ end
106
+ end