activestorage 6.1.7 → 7.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -276
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +29 -15
  5. data/app/assets/javascripts/activestorage.esm.js +848 -0
  6. data/app/assets/javascripts/activestorage.js +263 -376
  7. data/app/controllers/active_storage/base_controller.rb +0 -9
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +16 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/disk_controller.rb +5 -2
  11. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +8 -3
  13. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  14. data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
  15. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  16. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  17. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  18. data/app/controllers/concerns/active_storage/streaming.rb +66 -0
  19. data/app/javascript/activestorage/blob_record.js +4 -1
  20. data/app/javascript/activestorage/direct_upload.js +3 -2
  21. data/app/javascript/activestorage/index.js +3 -1
  22. data/app/javascript/activestorage/ujs.js +1 -1
  23. data/app/jobs/active_storage/analyze_job.rb +1 -1
  24. data/app/jobs/active_storage/mirror_job.rb +1 -1
  25. data/app/jobs/active_storage/purge_job.rb +1 -1
  26. data/app/jobs/active_storage/transform_job.rb +12 -0
  27. data/app/models/active_storage/attachment.rb +111 -4
  28. data/app/models/active_storage/blob/analyzable.rb +4 -3
  29. data/app/models/active_storage/blob/identifiable.rb +1 -0
  30. data/app/models/active_storage/blob/representable.rb +14 -8
  31. data/app/models/active_storage/blob.rb +93 -57
  32. data/app/models/active_storage/current.rb +2 -2
  33. data/app/models/active_storage/filename.rb +2 -0
  34. data/app/models/active_storage/named_variant.rb +21 -0
  35. data/app/models/active_storage/preview.rb +11 -7
  36. data/app/models/active_storage/record.rb +1 -1
  37. data/app/models/active_storage/variant.rb +10 -12
  38. data/app/models/active_storage/variant_record.rb +2 -0
  39. data/app/models/active_storage/variant_with_record.rb +28 -12
  40. data/app/models/active_storage/variation.rb +7 -5
  41. data/config/routes.rb +12 -10
  42. data/db/migrate/20170806125915_create_active_storage_tables.rb +15 -6
  43. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
  44. data/lib/active_storage/analyzer/audio_analyzer.rb +77 -0
  45. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +41 -0
  46. data/lib/active_storage/analyzer/image_analyzer/vips.rb +51 -0
  47. data/lib/active_storage/analyzer/image_analyzer.rb +4 -30
  48. data/lib/active_storage/analyzer/video_analyzer.rb +41 -17
  49. data/lib/active_storage/analyzer.rb +10 -4
  50. data/lib/active_storage/attached/changes/create_many.rb +14 -5
  51. data/lib/active_storage/attached/changes/create_one.rb +46 -4
  52. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  53. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  54. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  55. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  56. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  57. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  58. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  59. data/lib/active_storage/attached/changes.rb +7 -1
  60. data/lib/active_storage/attached/many.rb +32 -19
  61. data/lib/active_storage/attached/model.rb +80 -29
  62. data/lib/active_storage/attached/one.rb +37 -31
  63. data/lib/active_storage/attached.rb +2 -0
  64. data/lib/active_storage/deprecator.rb +7 -0
  65. data/lib/active_storage/downloader.rb +4 -4
  66. data/lib/active_storage/engine.rb +55 -7
  67. data/lib/active_storage/fixture_set.rb +75 -0
  68. data/lib/active_storage/gem_version.rb +3 -3
  69. data/lib/active_storage/log_subscriber.rb +12 -0
  70. data/lib/active_storage/previewer.rb +12 -5
  71. data/lib/active_storage/reflection.rb +12 -2
  72. data/lib/active_storage/service/azure_storage_service.rb +30 -6
  73. data/lib/active_storage/service/configurator.rb +1 -1
  74. data/lib/active_storage/service/disk_service.rb +26 -19
  75. data/lib/active_storage/service/gcs_service.rb +100 -11
  76. data/lib/active_storage/service/mirror_service.rb +12 -7
  77. data/lib/active_storage/service/registry.rb +1 -1
  78. data/lib/active_storage/service/s3_service.rb +39 -15
  79. data/lib/active_storage/service.rb +17 -7
  80. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  81. data/lib/active_storage/transformers/transformer.rb +3 -1
  82. data/lib/active_storage/version.rb +1 -1
  83. data/lib/active_storage.rb +22 -2
  84. metadata +30 -30
  85. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mini_mime"
3
+ require "marcel"
4
4
 
5
+ # = Active Storage \Variation
6
+ #
5
7
  # A set of transformations that can be applied to a blob to create a variant. This class is exposed via
6
8
  # the ActiveStorage::Blob#variant method and should rarely be used directly.
7
9
  #
8
10
  # In case you do need to use this directly, it's instantiated using a hash of transformations where
9
11
  # the key is the command and the value is the arguments. Example:
10
12
  #
11
- # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
13
+ # ActiveStorage::Variation.new(resize_to_limit: [100, 100], colourspace: "b-w", rotate: "-90", saver: { trim: true })
12
14
  #
13
15
  # The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands.
14
16
  class ActiveStorage::Variation
@@ -59,14 +61,14 @@ class ActiveStorage::Variation
59
61
 
60
62
  def format
61
63
  transformations.fetch(:format, :png).tap do |format|
62
- if MiniMime.lookup_by_extension(format.to_s).nil?
64
+ if Marcel::Magic.by_extension(format.to_s).nil?
63
65
  raise ArgumentError, "Invalid variant format (#{format.inspect})"
64
66
  end
65
67
  end
66
68
  end
67
69
 
68
70
  def content_type
69
- MiniMime.lookup_by_extension(format.to_s).content_type
71
+ Marcel::MimeType.for(extension: format.to_s)
70
72
  end
71
73
 
72
74
  # Returns a signed key for all the +transformations+ that this variation was instantiated with.
@@ -75,7 +77,7 @@ class ActiveStorage::Variation
75
77
  end
76
78
 
77
79
  def digest
78
- Digest::SHA1.base64digest Marshal.dump(transformations)
80
+ OpenSSL::Digest::SHA1.base64digest Marshal.dump(transformations)
79
81
  end
80
82
 
81
83
  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,25 @@ 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
+ expires_at = options.delete(:expires_at)
36
+
38
37
  if model.respond_to?(:signed_id)
39
38
  route_for(
40
39
  :rails_service_blob_proxy,
41
- model.signed_id,
40
+ model.signed_id(expires_in: expires_in, expires_at: expires_at),
42
41
  model.filename,
43
42
  options
44
43
  )
45
44
  else
46
- signed_blob_id = model.blob.signed_id
45
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at)
47
46
  variation_key = model.variation.key
48
47
  filename = model.blob.filename
49
48
 
@@ -58,15 +57,18 @@ Rails.application.routes.draw do
58
57
  end
59
58
 
60
59
  direct :rails_storage_redirect do |model, options|
60
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
61
+ expires_at = options.delete(:expires_at)
62
+
61
63
  if model.respond_to?(:signed_id)
62
64
  route_for(
63
65
  :rails_service_blob,
64
- model.signed_id,
66
+ model.signed_id(expires_in: expires_in, expires_at: expires_at),
65
67
  model.filename,
66
68
  options
67
69
  )
68
70
  else
69
- signed_blob_id = model.blob.signed_id
71
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at)
70
72
  variation_key = model.variation.key
71
73
  filename = model.blob.filename
72
74
 
@@ -1,4 +1,4 @@
1
- class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
1
+ class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Use Active Record's configured type for primary and foreign keys
4
4
  primary_key_type, foreign_key_type = primary_and_foreign_key_types
@@ -10,8 +10,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
10
10
  t.text :metadata
11
11
  t.string :service_name, null: false
12
12
  t.bigint :byte_size, null: false
13
- t.string :checksum, null: false
14
- 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
15
20
 
16
21
  t.index [ :key ], unique: true
17
22
  end
@@ -21,9 +26,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
21
26
  t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
22
27
  t.references :blob, null: false, type: foreign_key_type
23
28
 
24
- 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
25
34
 
26
- 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
27
36
  t.foreign_key :active_storage_blobs, column: :blob_id
28
37
  end
29
38
 
@@ -31,7 +40,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
31
40
  t.belongs_to :blob, null: false, index: false, type: foreign_key_type
32
41
  t.string :variation_digest, null: false
33
42
 
34
- 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
35
44
  t.foreign_key :active_storage_blobs, column: :blob_id
36
45
  end
37
46
  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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # = Active Storage Audio \Analyzer
5
+ #
6
+ # Extracts duration (seconds), bit_rate (bits/s), sample_rate (hertz) and tags (internal metadata) from an audio blob.
7
+ #
8
+ # Example:
9
+ #
10
+ # ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
11
+ # # => { duration: 5.0, bit_rate: 320340, sample_rate: 44100, tags: { encoder: "Lavc57.64", ... } }
12
+ #
13
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
14
+ class Analyzer::AudioAnalyzer < Analyzer
15
+ def self.accept?(blob)
16
+ blob.audio?
17
+ end
18
+
19
+ def metadata
20
+ { duration: duration, bit_rate: bit_rate, sample_rate: sample_rate, tags: tags }.compact
21
+ end
22
+
23
+ private
24
+ def duration
25
+ duration = audio_stream["duration"]
26
+ Float(duration) if duration
27
+ end
28
+
29
+ def bit_rate
30
+ bit_rate = audio_stream["bit_rate"]
31
+ Integer(bit_rate) if bit_rate
32
+ end
33
+
34
+ def sample_rate
35
+ sample_rate = audio_stream["sample_rate"]
36
+ Integer(sample_rate) if sample_rate
37
+ end
38
+
39
+ def tags
40
+ tags = audio_stream["tags"]
41
+ Hash(tags) if tags
42
+ end
43
+
44
+ def audio_stream
45
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
46
+ end
47
+
48
+ def streams
49
+ probe["streams"] || []
50
+ end
51
+
52
+ def probe
53
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
54
+ end
55
+
56
+ def probe_from(file)
57
+ instrument(File.basename(ffprobe_path)) do
58
+ IO.popen([ ffprobe_path,
59
+ "-print_format", "json",
60
+ "-show_streams",
61
+ "-show_format",
62
+ "-v", "error",
63
+ file.path
64
+ ]) do |output|
65
+ JSON.parse(output.read)
66
+ end
67
+ end
68
+ rescue Errno::ENOENT
69
+ logger.info "Skipping audio analysis because ffprobe isn't installed"
70
+ {}
71
+ end
72
+
73
+ def ffprobe_path
74
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,41 @@
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
+ begin
14
+ require "mini_magick"
15
+ rescue LoadError
16
+ logger.info "Skipping image analysis because the mini_magick gem isn't installed"
17
+ return {}
18
+ end
19
+
20
+ download_blob_to_tempfile do |file|
21
+ image = instrument("mini_magick") do
22
+ MiniMagick::Image.new(file.path)
23
+ end
24
+
25
+ if image.valid?
26
+ yield image
27
+ else
28
+ logger.info "Skipping image analysis because ImageMagick doesn't support the file"
29
+ {}
30
+ end
31
+ rescue MiniMagick::Error => error
32
+ logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
33
+ {}
34
+ end
35
+ end
36
+
37
+ def rotated_image?(image)
38
+ %w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,51 @@
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
+ begin
14
+ require "ruby-vips"
15
+ rescue LoadError
16
+ logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
17
+ return {}
18
+ end
19
+
20
+ download_blob_to_tempfile do |file|
21
+ image = instrument("vips") do
22
+ ::Vips::Image.new_from_file(file.path, access: :sequential)
23
+ end
24
+
25
+ if valid_image?(image)
26
+ yield image
27
+ else
28
+ logger.info "Skipping image analysis because Vips doesn't support the file"
29
+ {}
30
+ end
31
+ rescue ::Vips::Error => error
32
+ logger.error "Skipping image analysis due to an Vips error: #{error.message}"
33
+ {}
34
+ end
35
+ end
36
+
37
+ ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
38
+ def rotated_image?(image)
39
+ ROTATIONS === image.get("exif-ifd0-Orientation")
40
+ rescue ::Vips::Error
41
+ false
42
+ end
43
+
44
+ def valid_image?(image)
45
+ image.avg
46
+ true
47
+ rescue ::Vips::Error
48
+ false
49
+ end
50
+ end
51
+ end
@@ -1,17 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- # Extracts width and height in pixels from an image blob.
4
+ # = Active Storage Image \Analyzer
5
+ #
6
+ # This is an abstract base class for image analyzers, which extract width and height from an image blob.
5
7
  #
6
8
  # If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
7
9
  #
8
10
  # Example:
9
11
  #
10
- # ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
12
+ # ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick.new(blob).metadata
11
13
  # # => { 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
14
  class Analyzer::ImageAnalyzer < Analyzer
16
15
  def self.accept?(blob)
17
16
  blob.image?
@@ -26,30 +25,5 @@ module ActiveStorage
26
25
  end
27
26
  end
28
27
  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
28
  end
55
29
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage Video \Analyzer
5
+ #
4
6
  # Extracts the following from a video blob:
5
7
  #
6
8
  # * Width (pixels)
@@ -8,22 +10,24 @@ module ActiveStorage
8
10
  # * Duration (seconds)
9
11
  # * Angle (degrees)
10
12
  # * Display aspect ratio
13
+ # * Audio (true if file has an audio channel, false if not)
14
+ # * Video (true if file has an video channel, false if not)
11
15
  #
12
16
  # Example:
13
17
  #
14
18
  # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
15
- # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
19
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
16
20
  #
17
- # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
21
+ # When a video's angle is 90, -90, 270 or -270 degrees, its width and height are automatically swapped for convenience.
18
22
  #
19
- # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
23
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
20
24
  class Analyzer::VideoAnalyzer < Analyzer
21
25
  def self.accept?(blob)
22
26
  blob.video?
23
27
  end
24
28
 
25
29
  def metadata
26
- { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
30
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio?, video: video? }.compact
27
31
  end
28
32
 
29
33
  private
@@ -49,7 +53,11 @@ module ActiveStorage
49
53
  end
50
54
 
51
55
  def angle
52
- Integer(tags["rotate"]) if tags["rotate"]
56
+ if tags["rotate"]
57
+ Integer(tags["rotate"])
58
+ elsif side_data && side_data[0] && side_data[0]["rotation"]
59
+ Integer(side_data[0]["rotation"])
60
+ end
53
61
  end
54
62
 
55
63
  def display_aspect_ratio
@@ -63,9 +71,16 @@ module ActiveStorage
63
71
  end
64
72
  end
65
73
 
66
-
67
74
  def rotated?
68
- angle == 90 || angle == 270
75
+ angle == 90 || angle == 270 || angle == -90 || angle == -270
76
+ end
77
+
78
+ def audio?
79
+ audio_stream.present?
80
+ end
81
+
82
+ def video?
83
+ video_stream.present?
69
84
  end
70
85
 
71
86
  def computed_height
@@ -86,15 +101,22 @@ module ActiveStorage
86
101
  @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
87
102
  end
88
103
 
89
-
90
104
  def tags
91
105
  @tags ||= video_stream["tags"] || {}
92
106
  end
93
107
 
108
+ def side_data
109
+ @side_data ||= video_stream["side_data_list"] || {}
110
+ end
111
+
94
112
  def video_stream
95
113
  @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
96
114
  end
97
115
 
116
+ def audio_stream
117
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
118
+ end
119
+
98
120
  def streams
99
121
  probe["streams"] || []
100
122
  end
@@ -108,17 +130,19 @@ module ActiveStorage
108
130
  end
109
131
 
110
132
  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)
133
+ instrument(File.basename(ffprobe_path)) do
134
+ IO.popen([ ffprobe_path,
135
+ "-print_format", "json",
136
+ "-show_streams",
137
+ "-show_format",
138
+ "-v", "error",
139
+ file.path
140
+ ]) do |output|
141
+ JSON.parse(output.read)
142
+ end
119
143
  end
120
144
  rescue Errno::ENOENT
121
- logger.info "Skipping video analysis because FFmpeg isn't installed"
145
+ logger.info "Skipping video analysis because ffprobe isn't installed"
122
146
  {}
123
147
  end
124
148
 
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage \Analyzer
5
+ #
4
6
  # 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.
7
+ # ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
6
8
  class Analyzer
7
9
  attr_reader :blob
8
10
 
@@ -29,16 +31,20 @@ module ActiveStorage
29
31
 
30
32
  private
31
33
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
32
- def download_blob_to_tempfile(&block) #:doc:
34
+ def download_blob_to_tempfile(&block) # :doc:
33
35
  blob.open tmpdir: tmpdir, &block
34
36
  end
35
37
 
36
- def logger #:doc:
38
+ def logger # :doc:
37
39
  ActiveStorage.logger
38
40
  end
39
41
 
40
- def tmpdir #:doc:
42
+ def tmpdir # :doc:
41
43
  Dir.tmpdir
42
44
  end
45
+
46
+ def instrument(analyzer, &block) # :doc:
47
+ ActiveSupport::Notifications.instrument("analyze.active_storage", analyzer: analyzer, &block)
48
+ end
43
49
  end
44
50
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Attached::Changes::CreateMany #:nodoc:
5
- attr_reader :name, :record, :attachables
4
+ class Attached::Changes::CreateMany # :nodoc:
5
+ attr_reader :name, :record, :attachables, :pending_uploads
6
6
 
7
- def initialize(name, record, attachables)
7
+ def initialize(name, record, attachables, pending_uploads: [])
8
8
  @name, @record, @attachables = name, record, Array(attachables)
9
9
  blobs.each(&:identify_without_saving)
10
+ @pending_uploads = Array(pending_uploads) + subchanges_without_blobs
11
+ attachments
10
12
  end
11
13
 
12
14
  def attachments
@@ -18,7 +20,7 @@ module ActiveStorage
18
20
  end
19
21
 
20
22
  def upload
21
- subchanges.each(&:upload)
23
+ pending_uploads.each(&:upload)
22
24
  end
23
25
 
24
26
  def save
@@ -35,13 +37,20 @@ module ActiveStorage
35
37
  ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
36
38
  end
37
39
 
40
+ def subchanges_without_blobs
41
+ subchanges.reject { |subchange| subchange.attachable.is_a?(ActiveStorage::Blob) }
42
+ end
38
43
 
39
44
  def assign_associated_attachments
40
- record.public_send("#{name}_attachments=", attachments)
45
+ record.public_send("#{name}_attachments=", persisted_or_new_attachments)
41
46
  end
42
47
 
43
48
  def reset_associated_blobs
44
49
  record.public_send("#{name}_blobs").reset
45
50
  end
51
+
52
+ def persisted_or_new_attachments
53
+ attachments.select { |attachment| attachment.persisted? || attachment.new_record? }
54
+ end
46
55
  end
47
56
  end
@@ -4,7 +4,7 @@ require "action_dispatch"
4
4
  require "action_dispatch/http/upload"
5
5
 
6
6
  module ActiveStorage
7
- class Attached::Changes::CreateOne #:nodoc:
7
+ class Attached::Changes::CreateOne # :nodoc:
8
8
  attr_reader :name, :record, :attachable
9
9
 
10
10
  def initialize(name, record, attachable)
@@ -22,10 +22,26 @@ module ActiveStorage
22
22
 
23
23
  def upload
24
24
  case attachable
25
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
25
+ when ActionDispatch::Http::UploadedFile
26
26
  blob.upload_without_unfurling(attachable.open)
27
+ when Rack::Test::UploadedFile
28
+ blob.upload_without_unfurling(
29
+ attachable.respond_to?(:open) ? attachable.open : attachable
30
+ )
27
31
  when Hash
28
32
  blob.upload_without_unfurling(attachable.fetch(:io))
33
+ when File
34
+ blob.upload_without_unfurling(attachable)
35
+ when Pathname
36
+ blob.upload_without_unfurling(attachable.open)
37
+ when ActiveStorage::Blob
38
+ when String
39
+ else
40
+ raise(
41
+ ArgumentError,
42
+ "Could not upload: expected attachable, " \
43
+ "got #{attachable.inspect}"
44
+ )
29
45
  end
30
46
  end
31
47
 
@@ -53,7 +69,7 @@ module ActiveStorage
53
69
  case attachable
54
70
  when ActiveStorage::Blob
55
71
  attachable
56
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
72
+ when ActionDispatch::Http::UploadedFile
57
73
  ActiveStorage::Blob.build_after_unfurling(
58
74
  io: attachable.open,
59
75
  filename: attachable.original_filename,
@@ -61,6 +77,14 @@ module ActiveStorage
61
77
  record: record,
62
78
  service_name: attachment_service_name
63
79
  )
80
+ when Rack::Test::UploadedFile
81
+ ActiveStorage::Blob.build_after_unfurling(
82
+ io: attachable.respond_to?(:open) ? attachable.open : attachable,
83
+ filename: attachable.original_filename,
84
+ content_type: attachable.content_type,
85
+ record: record,
86
+ service_name: attachment_service_name
87
+ )
64
88
  when Hash
65
89
  ActiveStorage::Blob.build_after_unfurling(
66
90
  **attachable.reverse_merge(
@@ -70,8 +94,26 @@ module ActiveStorage
70
94
  )
71
95
  when String
72
96
  ActiveStorage::Blob.find_signed!(attachable, record: record)
97
+ when File
98
+ ActiveStorage::Blob.build_after_unfurling(
99
+ io: attachable,
100
+ filename: File.basename(attachable),
101
+ record: record,
102
+ service_name: attachment_service_name
103
+ )
104
+ when Pathname
105
+ ActiveStorage::Blob.build_after_unfurling(
106
+ io: attachable.open,
107
+ filename: File.basename(attachable),
108
+ record: record,
109
+ service_name: attachment_service_name
110
+ )
73
111
  else
74
- raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
112
+ raise(
113
+ ArgumentError,
114
+ "Could not find or build blob: expected attachable, " \
115
+ "got #{attachable.inspect}"
116
+ )
75
117
  end
76
118
  end
77
119