activestorage 6.1.6.1 → 7.0.3.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -212
  3. data/README.md +25 -11
  4. data/app/assets/javascripts/activestorage.esm.js +844 -0
  5. data/app/assets/javascripts/activestorage.js +257 -376
  6. data/app/controllers/active_storage/base_controller.rb +0 -9
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +15 -4
  8. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  9. data/app/controllers/active_storage/disk_controller.rb +1 -0
  10. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +7 -3
  12. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  13. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  14. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  15. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  16. data/app/javascript/activestorage/ujs.js +1 -1
  17. data/app/models/active_storage/attachment.rb +35 -2
  18. data/app/models/active_storage/blob/representable.rb +7 -5
  19. data/app/models/active_storage/blob.rb +92 -36
  20. data/app/models/active_storage/current.rb +12 -2
  21. data/app/models/active_storage/preview.rb +6 -4
  22. data/app/models/active_storage/record.rb +1 -1
  23. data/app/models/active_storage/variant.rb +3 -6
  24. data/app/models/active_storage/variant_record.rb +2 -0
  25. data/app/models/active_storage/variant_with_record.rb +9 -5
  26. data/app/models/active_storage/variation.rb +2 -2
  27. data/config/routes.rb +10 -10
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +32 -11
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +4 -0
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +17 -2
  31. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
  32. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  33. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  34. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  35. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  36. data/lib/active_storage/analyzer/video_analyzer.rb +27 -12
  37. data/lib/active_storage/analyzer.rb +8 -4
  38. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  39. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  40. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  41. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  42. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  43. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  44. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  45. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  46. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  47. data/lib/active_storage/attached/changes.rb +7 -1
  48. data/lib/active_storage/attached/many.rb +27 -15
  49. data/lib/active_storage/attached/model.rb +35 -7
  50. data/lib/active_storage/attached/one.rb +32 -27
  51. data/lib/active_storage/downloader.rb +4 -4
  52. data/lib/active_storage/engine.rb +45 -1
  53. data/lib/active_storage/fixture_set.rb +76 -0
  54. data/lib/active_storage/gem_version.rb +4 -4
  55. data/lib/active_storage/previewer.rb +4 -4
  56. data/lib/active_storage/reflection.rb +12 -2
  57. data/lib/active_storage/service/azure_storage_service.rb +28 -6
  58. data/lib/active_storage/service/configurator.rb +1 -1
  59. data/lib/active_storage/service/disk_service.rb +24 -19
  60. data/lib/active_storage/service/gcs_service.rb +109 -11
  61. data/lib/active_storage/service/mirror_service.rb +2 -2
  62. data/lib/active_storage/service/registry.rb +1 -1
  63. data/lib/active_storage/service/s3_service.rb +37 -15
  64. data/lib/active_storage/service.rb +13 -5
  65. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  66. data/lib/active_storage/transformers/transformer.rb +1 -1
  67. data/lib/active_storage/version.rb +1 -1
  68. data/lib/active_storage.rb +4 -0
  69. metadata +24 -14
  70. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -66,9 +66,6 @@ class ActiveStorage::Preview
66
66
  end
67
67
  end
68
68
 
69
- alias_method :service_url, :url
70
- deprecate service_url: :url
71
-
72
69
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
70
  def key
74
71
  if processed?
@@ -78,6 +75,11 @@ class ActiveStorage::Preview
78
75
  end
79
76
  end
80
77
 
78
+ # Downloads the file associated with this preview's variant. If no block is
79
+ # given, the entire file is read into memory and returned. That'll use a lot
80
+ # of RAM for very large files. If a block is given, then the download is
81
+ # streamed and yielded in chunks. Raises ActiveStorage::Preview::UnprocessedError
82
+ # if the preview has not been processed yet.
81
83
  def download(&block)
82
84
  if processed?
83
85
  variant.download(&block)
@@ -93,7 +95,7 @@ class ActiveStorage::Preview
93
95
 
94
96
  def process
95
97
  previewer.preview(service_name: blob.service_name) do |attachable|
96
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
98
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
97
99
  image.attach(attachable)
98
100
  end
99
101
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
3
+ class ActiveStorage::Record < ActiveRecord::Base # :nodoc:
4
4
  self.abstract_class = true
5
5
  end
6
6
 
@@ -42,7 +42,7 @@
42
42
  # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
43
43
  # ImageProcessing gem (such as +resize_to_limit+):
44
44
  #
45
- # avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
45
+ # avatar.variant(resize_to_limit: [800, 800], colourspace: "b-w", rotate: "-90")
46
46
  #
47
47
  # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
48
48
  #
@@ -67,7 +67,7 @@ class ActiveStorage::Variant
67
67
 
68
68
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
69
69
  def key
70
- "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
70
+ "variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
71
71
  end
72
72
 
73
73
  # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
@@ -79,9 +79,6 @@ class ActiveStorage::Variant
79
79
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
80
80
  end
81
81
 
82
- alias_method :service_url, :url
83
- deprecate service_url: :url
84
-
85
82
  # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
86
83
  # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
87
84
  def download(&block)
@@ -94,7 +91,7 @@ class ActiveStorage::Variant
94
91
 
95
92
  alias_method :content_type_for_serving, :content_type
96
93
 
97
- def forced_disposition_for_serving #:nodoc:
94
+ def forced_disposition_for_serving # :nodoc:
98
95
  nil
99
96
  end
100
97
 
@@ -6,3 +6,5 @@ class ActiveStorage::VariantRecord < ActiveStorage::Record
6
6
  belongs_to :blob
7
7
  has_one_attached :image
8
8
  end
9
+
10
+ ActiveSupport.run_load_hooks :active_storage_variant_record, ActiveStorage::VariantRecord
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an
4
+ # ActiveStorage::VariantRecord. This is only used if +ActiveStorage.track_variants+ is enabled.
3
5
  class ActiveStorage::VariantWithRecord
4
6
  attr_reader :blob, :variation
7
+ delegate :service, to: :blob
5
8
 
6
9
  def initialize(blob, variation)
7
10
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
@@ -26,9 +29,6 @@ class ActiveStorage::VariantWithRecord
26
29
 
27
30
  delegate :key, :url, :download, to: :image, allow_nil: true
28
31
 
29
- alias_method :service_url, :url
30
- deprecate service_url: :url
31
-
32
32
  private
33
33
  def transform_blob
34
34
  blob.open do |input|
@@ -41,7 +41,7 @@ class ActiveStorage::VariantWithRecord
41
41
 
42
42
  def create_or_find_record(image:)
43
43
  @record =
44
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
44
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
45
45
  blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
46
46
  record.image.attach(image)
47
47
  end
@@ -49,6 +49,10 @@ class ActiveStorage::VariantWithRecord
49
49
  end
50
50
 
51
51
  def record
52
- @record ||= blob.variant_records.find_by(variation_digest: variation.digest)
52
+ @record ||= if blob.variant_records.loaded?
53
+ blob.variant_records.find { |v| v.variation_digest == variation.digest }
54
+ else
55
+ blob.variant_records.find_by(variation_digest: variation.digest)
56
+ end
53
57
  end
54
58
  end
@@ -8,7 +8,7 @@ require "mini_mime"
8
8
  # In case you do need to use this directly, it's instantiated using a hash of transformations where
9
9
  # the key is the command and the value is the arguments. Example:
10
10
  #
11
- # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
11
+ # ActiveStorage::Variation.new(resize_to_limit: [100, 100], colourspace: "b-w", rotate: "-90", saver: { trim: true })
12
12
  #
13
13
  # The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands.
14
14
  class ActiveStorage::Variation
@@ -75,7 +75,7 @@ class ActiveStorage::Variation
75
75
  end
76
76
 
77
77
  def digest
78
- Digest::SHA1.base64digest Marshal.dump(transformations)
78
+ OpenSSL::Digest::SHA1.base64digest Marshal.dump(transformations)
79
79
  end
80
80
 
81
81
  private
data/config/routes.rb CHANGED
@@ -16,11 +16,7 @@ Rails.application.routes.draw do
16
16
  end
17
17
 
18
18
  direct :rails_representation do |representation, options|
19
- signed_blob_id = representation.blob.signed_id
20
- variation_key = representation.variation.key
21
- filename = representation.blob.filename
22
-
23
- route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
19
+ route_for(ActiveStorage.resolve_model_to_route, representation, options)
24
20
  end
25
21
 
26
22
  resolve("ActiveStorage::Variant") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
@@ -28,22 +24,24 @@ Rails.application.routes.draw do
28
24
  resolve("ActiveStorage::Preview") { |preview, options| route_for(ActiveStorage.resolve_model_to_route, preview, options) }
29
25
 
30
26
  direct :rails_blob do |blob, options|
31
- route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
27
+ route_for(ActiveStorage.resolve_model_to_route, blob, options)
32
28
  end
33
29
 
34
30
  resolve("ActiveStorage::Blob") { |blob, options| route_for(ActiveStorage.resolve_model_to_route, blob, options) }
35
31
  resolve("ActiveStorage::Attachment") { |attachment, options| route_for(ActiveStorage.resolve_model_to_route, attachment.blob, options) }
36
32
 
37
33
  direct :rails_storage_proxy do |model, options|
34
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
35
+
38
36
  if model.respond_to?(:signed_id)
39
37
  route_for(
40
38
  :rails_service_blob_proxy,
41
- model.signed_id,
39
+ model.signed_id(expires_in: expires_in),
42
40
  model.filename,
43
41
  options
44
42
  )
45
43
  else
46
- signed_blob_id = model.blob.signed_id
44
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in)
47
45
  variation_key = model.variation.key
48
46
  filename = model.blob.filename
49
47
 
@@ -58,15 +56,17 @@ Rails.application.routes.draw do
58
56
  end
59
57
 
60
58
  direct :rails_storage_redirect do |model, options|
59
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
60
+
61
61
  if model.respond_to?(:signed_id)
62
62
  route_for(
63
63
  :rails_service_blob,
64
- model.signed_id,
64
+ model.signed_id(expires_in: expires_in),
65
65
  model.filename,
66
66
  options
67
67
  )
68
68
  else
69
- signed_blob_id = model.blob.signed_id
69
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in)
70
70
  variation_key = model.variation.key
71
71
  filename = model.blob.filename
72
72
 
@@ -1,35 +1,56 @@
1
1
  class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
2
2
  def change
3
- create_table :active_storage_blobs do |t|
3
+ # Use Active Record's configured type for primary and foreign keys
4
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
5
+
6
+ create_table :active_storage_blobs, id: primary_key_type do |t|
4
7
  t.string :key, null: false
5
8
  t.string :filename, null: false
6
9
  t.string :content_type
7
10
  t.text :metadata
8
11
  t.string :service_name, null: false
9
12
  t.bigint :byte_size, null: false
10
- t.string :checksum, null: false
11
- t.datetime :created_at, null: false
13
+ t.string :checksum
14
+
15
+ if connection.supports_datetime_with_precision?
16
+ t.datetime :created_at, precision: 6, null: false
17
+ else
18
+ t.datetime :created_at, null: false
19
+ end
12
20
 
13
21
  t.index [ :key ], unique: true
14
22
  end
15
23
 
16
- create_table :active_storage_attachments do |t|
24
+ create_table :active_storage_attachments, id: primary_key_type do |t|
17
25
  t.string :name, null: false
18
- t.references :record, null: false, polymorphic: true, index: false
19
- t.references :blob, null: false
26
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
27
+ t.references :blob, null: false, type: foreign_key_type
20
28
 
21
- t.datetime :created_at, null: false
29
+ if connection.supports_datetime_with_precision?
30
+ t.datetime :created_at, precision: 6, null: false
31
+ else
32
+ t.datetime :created_at, null: false
33
+ end
22
34
 
23
- t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
35
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
24
36
  t.foreign_key :active_storage_blobs, column: :blob_id
25
37
  end
26
38
 
27
- create_table :active_storage_variant_records do |t|
28
- t.belongs_to :blob, null: false, index: false
39
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
40
+ t.belongs_to :blob, null: false, index: false, type: foreign_key_type
29
41
  t.string :variation_digest, null: false
30
42
 
31
- t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
43
+ t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
32
44
  t.foreign_key :active_storage_blobs, column: :blob_id
33
45
  end
34
46
  end
47
+
48
+ private
49
+ def primary_and_foreign_key_types
50
+ config = Rails.configuration.generators
51
+ setting = config.options[config.orm][:primary_key_type]
52
+ primary_key_type = setting || :primary_key
53
+ foreign_key_type = setting || :bigint
54
+ [primary_key_type, foreign_key_type]
55
+ end
35
56
  end
@@ -1,5 +1,7 @@
1
1
  class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
2
2
  def up
3
+ return unless table_exists?(:active_storage_blobs)
4
+
3
5
  unless column_exists?(:active_storage_blobs, :service_name)
4
6
  add_column :active_storage_blobs, :service_name, :string
5
7
 
@@ -12,6 +14,8 @@ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
12
14
  end
13
15
 
14
16
  def down
17
+ return unless table_exists?(:active_storage_blobs)
18
+
15
19
  remove_column :active_storage_blobs, :service_name
16
20
  end
17
21
  end
@@ -1,11 +1,26 @@
1
1
  class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
2
  def change
3
- create_table :active_storage_variant_records do |t|
4
- t.belongs_to :blob, null: false, index: false
3
+ return unless table_exists?(:active_storage_blobs)
4
+
5
+ # Use Active Record's configured type for primary key
6
+ create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
7
+ t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
5
8
  t.string :variation_digest, null: false
6
9
 
7
10
  t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
8
11
  t.foreign_key :active_storage_blobs, column: :blob_id
9
12
  end
10
13
  end
14
+
15
+ private
16
+ def primary_key_type
17
+ config = Rails.configuration.generators
18
+ config.options[config.orm][:primary_key_type] || :primary_key
19
+ end
20
+
21
+ def blobs_primary_key_type
22
+ pkey_name = connection.primary_key(:active_storage_blobs)
23
+ pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
24
+ pkey_column.bigint? ? :bigint : pkey_column.type
25
+ end
11
26
  end
@@ -0,0 +1,7 @@
1
+ class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
2
+ def change
3
+ return unless table_exists?(:active_storage_blobs)
4
+
5
+ change_column_null(:active_storage_blobs, :checksum, true)
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
5
+ #
6
+ # Example:
7
+ #
8
+ # ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
9
+ # # => { duration: 5.0, bit_rate: 320340 }
10
+ #
11
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
12
+ class Analyzer::AudioAnalyzer < Analyzer
13
+ def self.accept?(blob)
14
+ blob.audio?
15
+ end
16
+
17
+ def metadata
18
+ { duration: duration, bit_rate: bit_rate }.compact
19
+ end
20
+
21
+ private
22
+ def duration
23
+ duration = audio_stream["duration"]
24
+ Float(duration) if duration
25
+ end
26
+
27
+ def bit_rate
28
+ bit_rate = audio_stream["bit_rate"]
29
+ Integer(bit_rate) if bit_rate
30
+ end
31
+
32
+ def audio_stream
33
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
34
+ end
35
+
36
+ def streams
37
+ probe["streams"] || []
38
+ end
39
+
40
+ def probe
41
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
42
+ end
43
+
44
+ def probe_from(file)
45
+ instrument(File.basename(ffprobe_path)) do
46
+ IO.popen([ ffprobe_path,
47
+ "-print_format", "json",
48
+ "-show_streams",
49
+ "-show_format",
50
+ "-v", "error",
51
+ file.path
52
+ ]) do |output|
53
+ JSON.parse(output.read)
54
+ end
55
+ end
56
+ rescue Errno::ENOENT
57
+ logger.info "Skipping audio analysis because ffprobe isn't installed"
58
+ {}
59
+ end
60
+
61
+ def ffprobe_path
62
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
5
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
6
+ class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
7
+ def self.accept?(blob)
8
+ super && ActiveStorage.variant_processor == :mini_magick
9
+ end
10
+
11
+ private
12
+ def read_image
13
+ download_blob_to_tempfile do |file|
14
+ require "mini_magick"
15
+
16
+ image = instrument("mini_magick") do
17
+ MiniMagick::Image.new(file.path)
18
+ end
19
+
20
+ if image.valid?
21
+ yield image
22
+ else
23
+ logger.info "Skipping image analysis because ImageMagick doesn't support the file"
24
+ {}
25
+ end
26
+ end
27
+ rescue LoadError
28
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
29
+ {}
30
+ rescue MiniMagick::Error => error
31
+ logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
32
+ {}
33
+ end
34
+
35
+ def rotated_image?(image)
36
+ %w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # This analyzer relies on the third-party {ruby-vips}[https://github.com/libvips/ruby-vips] gem. Ruby-vips requires
5
+ # the {libvips}[https://libvips.github.io/libvips/] system library.
6
+ class Analyzer::ImageAnalyzer::Vips < Analyzer::ImageAnalyzer
7
+ def self.accept?(blob)
8
+ super && ActiveStorage.variant_processor == :vips
9
+ end
10
+
11
+ private
12
+ def read_image
13
+ download_blob_to_tempfile do |file|
14
+ require "ruby-vips"
15
+
16
+ image = instrument("vips") do
17
+ ::Vips::Image.new_from_file(file.path, access: :sequential)
18
+ end
19
+
20
+ if valid_image?(image)
21
+ yield image
22
+ else
23
+ logger.info "Skipping image analysis because Vips doesn't support the file"
24
+ {}
25
+ end
26
+ end
27
+ rescue LoadError
28
+ logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
29
+ {}
30
+ rescue ::Vips::Error => error
31
+ logger.error "Skipping image analysis due to an Vips error: #{error.message}"
32
+ {}
33
+ end
34
+
35
+ ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
36
+ def rotated_image?(image)
37
+ ROTATIONS === image.get("exif-ifd0-Orientation")
38
+ rescue ::Vips::Error
39
+ false
40
+ end
41
+
42
+ def valid_image?(image)
43
+ image.avg
44
+ true
45
+ rescue ::Vips::Error
46
+ false
47
+ end
48
+ end
49
+ end
@@ -1,17 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- # Extracts width and height in pixels from an image blob.
4
+ # This is an abstract base class for image analyzers, which extract width and height from an image blob.
5
5
  #
6
6
  # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
7
7
  #
8
8
  # Example:
9
9
  #
10
- # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
10
+ # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
11
11
  # # => { width: 4104, height: 2736 }
12
- #
13
- # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
14
- # the {ImageMagick}[http://www.imagemagick.org] system library.
15
12
  class Analyzer::ImageAnalyzer < Analyzer
16
13
  def self.accept?(blob)
17
14
  blob.image?
@@ -26,30 +23,5 @@ module ActiveStorage
26
23
  end
27
24
  end
28
25
  end
29
-
30
- private
31
- def read_image
32
- download_blob_to_tempfile do |file|
33
- require "mini_magick"
34
- image = MiniMagick::Image.new(file.path)
35
-
36
- if image.valid?
37
- yield image
38
- else
39
- logger.info "Skipping image analysis because ImageMagick doesn't support the file"
40
- {}
41
- end
42
- end
43
- rescue LoadError
44
- logger.info "Skipping image analysis because the mini_magick gem isn't installed"
45
- {}
46
- rescue MiniMagick::Error => error
47
- logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
48
- {}
49
- end
50
-
51
- def rotated_image?(image)
52
- %w[ RightTop LeftBottom ].include?(image["%[orientation]"])
53
- end
54
26
  end
55
27
  end
@@ -8,11 +8,13 @@ module ActiveStorage
8
8
  # * Duration (seconds)
9
9
  # * Angle (degrees)
10
10
  # * Display aspect ratio
11
+ # * Audio (true if file has an audio channel, false if not)
12
+ # * Video (true if file has an video channel, false if not)
11
13
  #
12
14
  # Example:
13
15
  #
14
16
  # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
15
- # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
17
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
16
18
  #
17
19
  # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
18
20
  #
@@ -23,7 +25,7 @@ module ActiveStorage
23
25
  end
24
26
 
25
27
  def metadata
26
- { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
28
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio?, video: video? }.compact
27
29
  end
28
30
 
29
31
  private
@@ -63,11 +65,18 @@ module ActiveStorage
63
65
  end
64
66
  end
65
67
 
66
-
67
68
  def rotated?
68
69
  angle == 90 || angle == 270
69
70
  end
70
71
 
72
+ def audio?
73
+ audio_stream.present?
74
+ end
75
+
76
+ def video?
77
+ video_stream.present?
78
+ end
79
+
71
80
  def computed_height
72
81
  if encoded_width && display_height_scale
73
82
  encoded_width * display_height_scale
@@ -95,6 +104,10 @@ module ActiveStorage
95
104
  @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
96
105
  end
97
106
 
107
+ def audio_stream
108
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
109
+ end
110
+
98
111
  def streams
99
112
  probe["streams"] || []
100
113
  end
@@ -108,17 +121,19 @@ module ActiveStorage
108
121
  end
109
122
 
110
123
  def probe_from(file)
111
- IO.popen([ ffprobe_path,
112
- "-print_format", "json",
113
- "-show_streams",
114
- "-show_format",
115
- "-v", "error",
116
- file.path
117
- ]) do |output|
118
- JSON.parse(output.read)
124
+ instrument(File.basename(ffprobe_path)) do
125
+ IO.popen([ ffprobe_path,
126
+ "-print_format", "json",
127
+ "-show_streams",
128
+ "-show_format",
129
+ "-v", "error",
130
+ file.path
131
+ ]) do |output|
132
+ JSON.parse(output.read)
133
+ end
119
134
  end
120
135
  rescue Errno::ENOENT
121
- logger.info "Skipping video analysis because FFmpeg isn't installed"
136
+ logger.info "Skipping video analysis because ffprobe isn't installed"
122
137
  {}
123
138
  end
124
139
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveStorage
4
4
  # This is an abstract base class for analyzers, which extract metadata from blobs. See
5
- # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
5
+ # ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
6
6
  class Analyzer
7
7
  attr_reader :blob
8
8
 
@@ -29,16 +29,20 @@ module ActiveStorage
29
29
 
30
30
  private
31
31
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
32
- def download_blob_to_tempfile(&block) #:doc:
32
+ def download_blob_to_tempfile(&block) # :doc:
33
33
  blob.open tmpdir: tmpdir, &block
34
34
  end
35
35
 
36
- def logger #:doc:
36
+ def logger # :doc:
37
37
  ActiveStorage.logger
38
38
  end
39
39
 
40
- def tmpdir #:doc:
40
+ def tmpdir # :doc:
41
41
  Dir.tmpdir
42
42
  end
43
+
44
+ def instrument(analyzer, &block) # :doc:
45
+ ActiveSupport::Notifications.instrument("analyze.active_storage", analyzer: analyzer, &block)
46
+ end
43
47
  end
44
48
  end