activestorage 5.2.0.beta2 → 5.2.0.rc1

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 (43) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +25 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +15 -1
  5. data/app/assets/javascripts/activestorage.js +1 -1
  6. data/app/controllers/active_storage/blobs_controller.rb +4 -6
  7. data/app/controllers/active_storage/previews_controller.rb +4 -6
  8. data/app/controllers/active_storage/variants_controller.rb +4 -6
  9. data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
  10. data/app/javascript/activestorage/blob_record.js +17 -3
  11. data/app/javascript/activestorage/helpers.js +10 -1
  12. data/app/models/active_storage/attachment.rb +5 -1
  13. data/app/models/active_storage/blob.rb +15 -134
  14. data/app/models/active_storage/blob/analyzable.rb +57 -0
  15. data/app/models/active_storage/blob/identifiable.rb +11 -0
  16. data/app/models/active_storage/blob/representable.rb +93 -0
  17. data/app/models/active_storage/filename.rb +1 -1
  18. data/app/models/active_storage/filename/parameters.rb +1 -1
  19. data/app/models/active_storage/identification.rb +38 -0
  20. data/app/models/active_storage/variant.rb +51 -5
  21. data/app/models/active_storage/variation.rb +19 -5
  22. data/config/routes.rb +7 -7
  23. data/lib/active_storage.rb +8 -1
  24. data/lib/active_storage/analyzer.rb +1 -1
  25. data/lib/active_storage/analyzer/image_analyzer.rb +1 -2
  26. data/lib/active_storage/analyzer/video_analyzer.rb +51 -10
  27. data/lib/active_storage/attached/macros.rb +4 -4
  28. data/lib/active_storage/downloading.rb +16 -4
  29. data/lib/active_storage/engine.rb +18 -1
  30. data/lib/active_storage/errors.rb +7 -0
  31. data/lib/active_storage/gem_version.rb +1 -1
  32. data/lib/active_storage/log_subscriber.rb +4 -0
  33. data/lib/active_storage/previewer.rb +21 -5
  34. data/lib/active_storage/previewer/pdf_previewer.rb +10 -1
  35. data/lib/active_storage/previewer/video_previewer.rb +5 -1
  36. data/lib/active_storage/service.rb +8 -3
  37. data/lib/active_storage/service/azure_storage_service.rb +24 -8
  38. data/lib/active_storage/service/disk_service.rb +26 -24
  39. data/lib/active_storage/service/gcs_service.rb +21 -7
  40. data/lib/active_storage/service/mirror_service.rb +5 -0
  41. data/lib/active_storage/service/s3_service.rb +13 -7
  42. data/lib/tasks/activestorage.rake +5 -1
  43. metadata +43 -9
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_storage/analyzer/null_analyzer"
4
+
5
+ module ActiveStorage::Blob::Analyzable
6
+ # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
7
+ # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
8
+ # ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
9
+ # libraries they require.
10
+ #
11
+ # To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
12
+ # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
13
+ # metadata is extracted from it.
14
+ #
15
+ # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
16
+ # in an initializer:
17
+ #
18
+ # # Add a custom analyzer for Microsoft Office documents:
19
+ # Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
20
+ #
21
+ # # Remove the built-in video analyzer:
22
+ # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
23
+ #
24
+ # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
25
+ #
26
+ # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
27
+ # analyzed via #analyze_later when they're attached for the first time.
28
+ def analyze
29
+ update! metadata: metadata.merge(extract_metadata_via_analyzer)
30
+ end
31
+
32
+ # Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
33
+ #
34
+ # This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
35
+ # again (e.g. if you add a new analyzer or modify an existing one).
36
+ def analyze_later
37
+ ActiveStorage::AnalyzeJob.perform_later(self)
38
+ end
39
+
40
+ # Returns true if the blob has been analyzed.
41
+ def analyzed?
42
+ analyzed
43
+ end
44
+
45
+ private
46
+ def extract_metadata_via_analyzer
47
+ analyzer.metadata.merge(analyzed: true)
48
+ end
49
+
50
+ def analyzer
51
+ analyzer_class.new(self)
52
+ end
53
+
54
+ def analyzer_class
55
+ ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
56
+ end
57
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::Blob::Identifiable
4
+ def identify
5
+ ActiveStorage::Identification.new(self).apply
6
+ end
7
+
8
+ def identified?
9
+ identified
10
+ end
11
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::Blob::Representable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_one_attached :preview_image
8
+ end
9
+
10
+ # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
11
+ # files, and it allows any image to be transformed for size, colors, and the like. Example:
12
+ #
13
+ # avatar.variant(resize: "100x100").processed.service_url
14
+ #
15
+ # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
16
+ # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
17
+ #
18
+ # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
19
+ # specific variant that can be created by a controller on-demand. Like so:
20
+ #
21
+ # <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
22
+ #
23
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
24
+ # can then produce on-demand.
25
+ #
26
+ # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
27
+ # variable, call ActiveStorage::Blob#variable?.
28
+ def variant(transformations)
29
+ if variable?
30
+ ActiveStorage::Variant.new(self, ActiveStorage::Variation.wrap(transformations))
31
+ else
32
+ raise ActiveStorage::InvariableError
33
+ end
34
+ end
35
+
36
+ # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
37
+ def variable?
38
+ ActiveStorage.variable_content_types.include?(content_type)
39
+ end
40
+
41
+
42
+ # Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated
43
+ # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
44
+ # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
45
+ #
46
+ # blob.preview(resize: "100x100").processed.service_url
47
+ #
48
+ # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
49
+ # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
50
+ # how to use the built-in version:
51
+ #
52
+ # <%= image_tag video.preview(resize: "100x100") %>
53
+ #
54
+ # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
55
+ # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
56
+ def preview(transformations)
57
+ if previewable?
58
+ ActiveStorage::Preview.new(self, ActiveStorage::Variation.wrap(transformations))
59
+ else
60
+ raise ActiveStorage::UnpreviewableError
61
+ end
62
+ end
63
+
64
+ # Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents.
65
+ def previewable?
66
+ ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
67
+ end
68
+
69
+
70
+ # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
71
+ #
72
+ # blob.representation(resize: "100x100").processed.service_url
73
+ #
74
+ # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
75
+ # ActiveStorage::Blob#representable? to determine whether a blob is representable.
76
+ #
77
+ # See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information.
78
+ def representation(transformations)
79
+ case
80
+ when previewable?
81
+ preview transformations
82
+ when variable?
83
+ variant transformations
84
+ else
85
+ raise ActiveStorage::UnrepresentableError
86
+ end
87
+ end
88
+
89
+ # Returns true if the blob is variable or previewable.
90
+ def representable?
91
+ variable? || previewable?
92
+ end
93
+ end
@@ -50,7 +50,7 @@ class ActiveStorage::Filename
50
50
  @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
51
51
  end
52
52
 
53
- def parameters
53
+ def parameters #:nodoc:
54
54
  Parameters.new self
55
55
  end
56
56
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Filename::Parameters
3
+ class ActiveStorage::Filename::Parameters #:nodoc:
4
4
  attr_reader :filename
5
5
 
6
6
  def initialize(filename)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::Identification
4
+ attr_reader :blob
5
+
6
+ def initialize(blob)
7
+ @blob = blob
8
+ end
9
+
10
+ def apply
11
+ blob.update!(content_type: content_type, identified: true) unless blob.identified?
12
+ end
13
+
14
+ private
15
+ def content_type
16
+ Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type)
17
+ end
18
+
19
+
20
+ def identifiable_chunk
21
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
22
+ client.get(uri, "Range" => "0-4096").body
23
+ end
24
+ end
25
+
26
+ def uri
27
+ @uri ||= URI.parse(blob.service_url)
28
+ end
29
+
30
+
31
+ def filename
32
+ blob.filename.to_s
33
+ end
34
+
35
+ def declared_content_type
36
+ blob.content_type
37
+ end
38
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_storage/downloading"
4
+
3
5
  # Image blobs can have variants that are the result of a set of transformations applied to the original.
4
6
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
5
7
  # original.
@@ -16,7 +18,7 @@
16
18
  # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
17
19
  # by Active Storage like so:
18
20
  #
19
- # <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %>
21
+ # <%= image_tag Current.user.avatar.variant(resize: "100x100") %>
20
22
  #
21
23
  # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController
22
24
  # can then produce on-demand.
@@ -35,6 +37,10 @@
35
37
  #
36
38
  # avatar.variant(resize: "100x100", monochrome: true, flip: "-90")
37
39
  class ActiveStorage::Variant
40
+ include ActiveStorage::Downloading
41
+
42
+ WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
43
+
38
44
  attr_reader :blob, :variation
39
45
  delegate :service, to: :blob
40
46
 
@@ -62,7 +68,7 @@ class ActiveStorage::Variant
62
68
  # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method
63
69
  # for its redirection.
64
70
  def service_url(expires_in: service.url_expires_in, disposition: :inline)
65
- service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type
71
+ service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
66
72
  end
67
73
 
68
74
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
@@ -76,11 +82,51 @@ class ActiveStorage::Variant
76
82
  end
77
83
 
78
84
  def process
79
- service.upload key, transform(service.download(blob.key))
85
+ open_image do |image|
86
+ transform image
87
+ format image
88
+ upload image
89
+ end
90
+ end
91
+
92
+
93
+ def filename
94
+ if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
95
+ blob.filename
96
+ else
97
+ ActiveStorage::Filename.new("#{blob.filename.base}.png")
98
+ end
99
+ end
100
+
101
+ def content_type
102
+ blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
103
+ end
104
+
105
+
106
+ def open_image(&block)
107
+ image = download_image
108
+
109
+ begin
110
+ yield image
111
+ ensure
112
+ image.destroy!
113
+ end
80
114
  end
81
115
 
82
- def transform(io)
116
+ def download_image
83
117
  require "mini_magick"
84
- File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path
118
+ MiniMagick::Image.create { |file| download_blob_to(file) }
119
+ end
120
+
121
+ def transform(image)
122
+ variation.transform(image)
123
+ end
124
+
125
+ def format(image)
126
+ image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
127
+ end
128
+
129
+ def upload(image)
130
+ File.open(image.path, "r") { |file| service.upload(key, file) }
85
131
  end
86
132
  end
@@ -46,11 +46,17 @@ class ActiveStorage::Variation
46
46
  # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
47
47
  # and performs the +transformations+ against it. The transformed image instance is then returned.
48
48
  def transform(image)
49
- transformations.each do |(method, argument)|
50
- if eligible_argument?(argument)
51
- image.public_send(method, argument)
52
- else
53
- image.public_send(method)
49
+ ActiveSupport::Notifications.instrument("transform.active_storage") do
50
+ transformations.each do |name, argument_or_subtransformations|
51
+ image.mogrify do |command|
52
+ if name.to_s == "combine_options"
53
+ argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
54
+ pass_transform_argument(command, subtransformation_name, subtransformation_argument)
55
+ end
56
+ else
57
+ pass_transform_argument(command, name, argument_or_subtransformations)
58
+ end
59
+ end
54
60
  end
55
61
  end
56
62
  end
@@ -61,6 +67,14 @@ class ActiveStorage::Variation
61
67
  end
62
68
 
63
69
  private
70
+ def pass_transform_argument(command, method, argument)
71
+ if eligible_argument?(argument)
72
+ command.public_send(method, argument)
73
+ else
74
+ command.public_send(method)
75
+ end
76
+ end
77
+
64
78
  def eligible_argument?(argument)
65
79
  argument.present? && argument != true
66
80
  end
data/config/routes.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob, internal: true
4
+ get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
5
5
 
6
6
  direct :rails_blob do |blob, options|
7
7
  route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
8
8
  end
9
9
 
10
- resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob) }
10
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
11
11
  resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
12
12
 
13
13
 
14
- get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation, internal: true
14
+ get "/rails/active_storage/variants/:signed_blob_id/:variation_key/*filename" => "active_storage/variants#show", as: :rails_blob_variation
15
15
 
16
16
  direct :rails_variant do |variant, options|
17
17
  signed_blob_id = variant.blob.signed_id
@@ -24,7 +24,7 @@ Rails.application.routes.draw do
24
24
  resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_variant, variant, options) }
25
25
 
26
26
 
27
- get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview, internal: true
27
+ get "/rails/active_storage/previews/:signed_blob_id/:variation_key/*filename" => "active_storage/previews#show", as: :rails_blob_preview
28
28
 
29
29
  direct :rails_preview do |preview, options|
30
30
  signed_blob_id = preview.blob.signed_id
@@ -37,7 +37,7 @@ Rails.application.routes.draw do
37
37
  resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_preview, preview, options) }
38
38
 
39
39
 
40
- get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service, internal: true
41
- put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service, internal: true
42
- post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads, internal: true
40
+ get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
41
+ put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
42
+ post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
43
43
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2017 David Heinemeier Hansson
4
+ # Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -26,7 +26,11 @@
26
26
  require "active_record"
27
27
  require "active_support"
28
28
  require "active_support/rails"
29
+
29
30
  require "active_storage/version"
31
+ require "active_storage/errors"
32
+
33
+ require "marcel"
30
34
 
31
35
  module ActiveStorage
32
36
  extend ActiveSupport::Autoload
@@ -41,4 +45,7 @@ module ActiveStorage
41
45
  mattr_accessor :queue
42
46
  mattr_accessor :previewers, default: []
43
47
  mattr_accessor :analyzers, default: []
48
+ mattr_accessor :paths, default: {}
49
+ mattr_accessor :variable_content_types, default: []
50
+ mattr_accessor :content_types_to_serve_as_binary, default: []
44
51
  end
@@ -26,7 +26,7 @@ module ActiveStorage
26
26
  end
27
27
 
28
28
  private
29
- def logger
29
+ def logger #:doc:
30
30
  ActiveStorage.logger
31
31
  end
32
32
  end
@@ -9,8 +9,7 @@ module ActiveStorage
9
9
  # # => { width: 4104, height: 2736 }
10
10
  #
11
11
  # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem. MiniMagick requires
12
- # the {ImageMagick}[http://www.imagemagick.org] system library. These libraries are not provided by Rails; you must
13
- # install them yourself to use this analyzer.
12
+ # the {ImageMagick}[http://www.imagemagick.org] system library.
14
13
  class Analyzer::ImageAnalyzer < Analyzer
15
14
  def self.accept?(blob)
16
15
  blob.image?
@@ -9,31 +9,40 @@ module ActiveStorage
9
9
  # * Height (pixels)
10
10
  # * Duration (seconds)
11
11
  # * Angle (degrees)
12
- # * Aspect ratio
12
+ # * Display aspect ratio
13
13
  #
14
14
  # Example:
15
15
  #
16
16
  # ActiveStorage::VideoAnalyzer.new(blob).metadata
17
- # # => { width: 640, height: 480, duration: 5.0, angle: 0, aspect_ratio: [4, 3] }
17
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
18
18
  #
19
- # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must
20
- # install ffmpeg yourself to use this analyzer.
19
+ # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
20
+ #
21
+ # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
21
22
  class Analyzer::VideoAnalyzer < Analyzer
22
23
  def self.accept?(blob)
23
24
  blob.video?
24
25
  end
25
26
 
26
27
  def metadata
27
- { width: width, height: height, duration: duration, angle: angle, aspect_ratio: aspect_ratio }.compact
28
+ { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
28
29
  end
29
30
 
30
31
  private
31
32
  def width
32
- Integer(video_stream["width"]) if video_stream["width"]
33
+ if rotated?
34
+ computed_height || encoded_height
35
+ else
36
+ encoded_width
37
+ end
33
38
  end
34
39
 
35
40
  def height
36
- Integer(video_stream["height"]) if video_stream["height"]
41
+ if rotated?
42
+ encoded_width
43
+ else
44
+ computed_height || encoded_height
45
+ end
37
46
  end
38
47
 
39
48
  def duration
@@ -44,12 +53,40 @@ module ActiveStorage
44
53
  Integer(tags["rotate"]) if tags["rotate"]
45
54
  end
46
55
 
47
- def aspect_ratio
56
+ def display_aspect_ratio
48
57
  if descriptor = video_stream["display_aspect_ratio"]
49
- descriptor.split(":", 2).collect(&:to_i)
58
+ if terms = descriptor.split(":", 2)
59
+ numerator = Integer(terms[0])
60
+ denominator = Integer(terms[1])
61
+
62
+ [numerator, denominator] unless numerator == 0
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+ def rotated?
69
+ angle == 90 || angle == 270
70
+ end
71
+
72
+ def computed_height
73
+ if encoded_width && display_height_scale
74
+ encoded_width * display_height_scale
50
75
  end
51
76
  end
52
77
 
78
+ def encoded_width
79
+ @encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
80
+ end
81
+
82
+ def encoded_height
83
+ @encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
84
+ end
85
+
86
+ def display_height_scale
87
+ @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
88
+ end
89
+
53
90
 
54
91
  def tags
55
92
  @tags ||= video_stream["tags"] || {}
@@ -68,12 +105,16 @@ module ActiveStorage
68
105
  end
69
106
 
70
107
  def probe_from(file)
71
- IO.popen([ "ffprobe", "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
108
+ IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
72
109
  JSON.parse(output.read)
73
110
  end
74
111
  rescue Errno::ENOENT
75
112
  logger.info "Skipping video analysis because ffmpeg isn't installed"
76
113
  {}
77
114
  end
115
+
116
+ def ffprobe_path
117
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
118
+ end
78
119
  end
79
120
  end