activestorage 6.0.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.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +198 -0
- data/MIT-LICENSE +20 -0
- data/README.md +162 -0
- data/app/assets/javascripts/activestorage.js +942 -0
- data/app/controllers/active_storage/base_controller.rb +8 -0
- data/app/controllers/active_storage/blobs_controller.rb +14 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
- data/app/controllers/active_storage/disk_controller.rb +66 -0
- data/app/controllers/active_storage/representations_controller.rb +14 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +73 -0
- data/app/javascript/activestorage/blob_upload.js +35 -0
- data/app/javascript/activestorage/direct_upload.js +48 -0
- data/app/javascript/activestorage/direct_upload_controller.js +67 -0
- data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/activestorage/helpers.js +51 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +86 -0
- data/app/jobs/active_storage/analyze_job.rb +12 -0
- data/app/jobs/active_storage/base_job.rb +4 -0
- data/app/jobs/active_storage/purge_job.rb +13 -0
- data/app/models/active_storage/attachment.rb +50 -0
- data/app/models/active_storage/blob.rb +278 -0
- data/app/models/active_storage/blob/analyzable.rb +57 -0
- data/app/models/active_storage/blob/identifiable.rb +31 -0
- data/app/models/active_storage/blob/representable.rb +93 -0
- data/app/models/active_storage/current.rb +5 -0
- data/app/models/active_storage/filename.rb +77 -0
- data/app/models/active_storage/preview.rb +89 -0
- data/app/models/active_storage/variant.rb +131 -0
- data/app/models/active_storage/variation.rb +80 -0
- data/config/routes.rb +32 -0
- data/db/migrate/20170806125915_create_active_storage_tables.rb +26 -0
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
- data/lib/active_storage.rb +73 -0
- data/lib/active_storage/analyzer.rb +38 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +52 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +118 -0
- data/lib/active_storage/attached.rb +25 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +69 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +27 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/many.rb +65 -0
- data/lib/active_storage/attached/model.rb +147 -0
- data/lib/active_storage/attached/one.rb +79 -0
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/downloading.rb +47 -0
- data/lib/active_storage/engine.rb +149 -0
- data/lib/active_storage/errors.rb +26 -0
- data/lib/active_storage/gem_version.rb +17 -0
- data/lib/active_storage/log_subscriber.rb +58 -0
- data/lib/active_storage/previewer.rb +84 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +36 -0
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +35 -0
- data/lib/active_storage/previewer/video_previewer.rb +26 -0
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +141 -0
- data/lib/active_storage/service/azure_storage_service.rb +165 -0
- data/lib/active_storage/service/configurator.rb +34 -0
- data/lib/active_storage/service/disk_service.rb +166 -0
- data/lib/active_storage/service/gcs_service.rb +141 -0
- data/lib/active_storage/service/mirror_service.rb +55 -0
- data/lib/active_storage/service/s3_service.rb +116 -0
- data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
- data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
- data/lib/active_storage/transformers/transformer.rb +42 -0
- data/lib/active_storage/version.rb +10 -0
- data/lib/tasks/activestorage.rake +22 -0
- metadata +174 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A set of transformations that can be applied to a blob to create a variant. This class is exposed via
|
4
|
+
# the ActiveStorage::Blob#variant method and should rarely be used directly.
|
5
|
+
#
|
6
|
+
# In case you do need to use this directly, it's instantiated using a hash of transformations where
|
7
|
+
# the key is the command and the value is the arguments. Example:
|
8
|
+
#
|
9
|
+
# ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
|
10
|
+
#
|
11
|
+
# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
|
12
|
+
class ActiveStorage::Variation
|
13
|
+
attr_reader :transformations
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Returns a Variation instance based on the given variator. If the variator is a Variation, it is
|
17
|
+
# returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
|
18
|
+
# it is assumed to be a transformations Hash and is passed directly to the constructor.
|
19
|
+
def wrap(variator)
|
20
|
+
case variator
|
21
|
+
when self
|
22
|
+
variator
|
23
|
+
when String
|
24
|
+
decode variator
|
25
|
+
else
|
26
|
+
new variator
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a Variation instance with the transformations that were encoded by +encode+.
|
31
|
+
def decode(key)
|
32
|
+
new ActiveStorage.verifier.verify(key, purpose: :variation)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a signed key for the +transformations+, which can be used to refer to a specific
|
36
|
+
# variation in a URL or combined key (like <tt>ActiveStorage::Variant#key</tt>).
|
37
|
+
def encode(transformations)
|
38
|
+
ActiveStorage.verifier.generate(transformations, purpose: :variation)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(transformations)
|
43
|
+
@transformations = transformations.deep_symbolize_keys
|
44
|
+
end
|
45
|
+
|
46
|
+
# Accepts a File object, performs the +transformations+ against it, and
|
47
|
+
# saves the transformed image into a temporary file. If +format+ is specified
|
48
|
+
# it will be the format of the result image, otherwise the result image
|
49
|
+
# retains the source format.
|
50
|
+
def transform(file, format: nil, &block)
|
51
|
+
ActiveSupport::Notifications.instrument("transform.active_storage") do
|
52
|
+
transformer.transform(file, format: format, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a signed key for all the +transformations+ that this variation was instantiated with.
|
57
|
+
def key
|
58
|
+
self.class.encode(transformations)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def transformer
|
63
|
+
if ActiveStorage.variant_processor
|
64
|
+
begin
|
65
|
+
require "image_processing"
|
66
|
+
rescue LoadError
|
67
|
+
ActiveSupport::Deprecation.warn <<~WARNING.squish
|
68
|
+
Generating image variants will require the image_processing gem in Rails 6.1.
|
69
|
+
Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
|
70
|
+
WARNING
|
71
|
+
|
72
|
+
ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
|
73
|
+
else
|
74
|
+
ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.routes.draw do
|
4
|
+
scope ActiveStorage.routes_prefix do
|
5
|
+
get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
|
6
|
+
|
7
|
+
get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
|
8
|
+
|
9
|
+
get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
|
10
|
+
put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
|
11
|
+
post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
|
12
|
+
end
|
13
|
+
|
14
|
+
direct :rails_representation do |representation, options|
|
15
|
+
signed_blob_id = representation.blob.signed_id
|
16
|
+
variation_key = representation.variation.key
|
17
|
+
filename = representation.blob.filename
|
18
|
+
|
19
|
+
route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
|
23
|
+
resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
|
24
|
+
|
25
|
+
|
26
|
+
direct :rails_blob do |blob, options|
|
27
|
+
route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
|
31
|
+
resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :active_storage_blobs do |t|
|
4
|
+
t.string :key, null: false
|
5
|
+
t.string :filename, null: false
|
6
|
+
t.string :content_type
|
7
|
+
t.text :metadata
|
8
|
+
t.bigint :byte_size, null: false
|
9
|
+
t.string :checksum, null: false
|
10
|
+
t.datetime :created_at, null: false
|
11
|
+
|
12
|
+
t.index [ :key ], unique: true
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :active_storage_attachments do |t|
|
16
|
+
t.string :name, null: false
|
17
|
+
t.references :record, null: false, polymorphic: true, index: false
|
18
|
+
t.references :blob, null: false
|
19
|
+
|
20
|
+
t.datetime :created_at, null: false
|
21
|
+
|
22
|
+
t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
|
23
|
+
t.foreign_key :active_storage_blobs, column: :blob_id
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
|
2
|
+
def up
|
3
|
+
return if foreign_key_exists?(:active_storage_attachments, column: :blob_id)
|
4
|
+
|
5
|
+
if table_exists?(:active_storage_blobs)
|
6
|
+
add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
# a copy of this software and associated documentation files (the
|
8
|
+
# "Software"), to deal in the Software without restriction, including
|
9
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
# the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
#++
|
25
|
+
|
26
|
+
require "active_record"
|
27
|
+
require "active_support"
|
28
|
+
require "active_support/rails"
|
29
|
+
require "active_support/core_ext/numeric/time"
|
30
|
+
|
31
|
+
require "active_storage/version"
|
32
|
+
require "active_storage/errors"
|
33
|
+
|
34
|
+
require "marcel"
|
35
|
+
|
36
|
+
module ActiveStorage
|
37
|
+
extend ActiveSupport::Autoload
|
38
|
+
|
39
|
+
autoload :Attached
|
40
|
+
autoload :Service
|
41
|
+
autoload :Previewer
|
42
|
+
autoload :Analyzer
|
43
|
+
|
44
|
+
mattr_accessor :logger
|
45
|
+
mattr_accessor :verifier
|
46
|
+
mattr_accessor :variant_processor, default: :mini_magick
|
47
|
+
|
48
|
+
mattr_accessor :queues, default: {}
|
49
|
+
|
50
|
+
mattr_accessor :previewers, default: []
|
51
|
+
mattr_accessor :analyzers, default: []
|
52
|
+
|
53
|
+
mattr_accessor :paths, default: {}
|
54
|
+
|
55
|
+
mattr_accessor :variable_content_types, default: []
|
56
|
+
mattr_accessor :binary_content_type, default: "application/octet-stream"
|
57
|
+
mattr_accessor :content_types_to_serve_as_binary, default: []
|
58
|
+
mattr_accessor :content_types_allowed_inline, default: []
|
59
|
+
|
60
|
+
mattr_accessor :service_urls_expire_in, default: 5.minutes
|
61
|
+
|
62
|
+
mattr_accessor :routes_prefix, default: "/rails/active_storage"
|
63
|
+
|
64
|
+
mattr_accessor :replace_on_assign_to_many, default: false
|
65
|
+
|
66
|
+
module Transformers
|
67
|
+
extend ActiveSupport::Autoload
|
68
|
+
|
69
|
+
autoload :Transformer
|
70
|
+
autoload :ImageProcessingTransformer
|
71
|
+
autoload :MiniMagickTransformer
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
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.
|
6
|
+
class Analyzer
|
7
|
+
attr_reader :blob
|
8
|
+
|
9
|
+
# Implement this method in a concrete subclass. Have it return true when given a blob from which
|
10
|
+
# the analyzer can extract metadata.
|
11
|
+
def self.accept?(blob)
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(blob)
|
16
|
+
@blob = blob
|
17
|
+
end
|
18
|
+
|
19
|
+
# Override this method in a concrete subclass. Have it return a Hash of metadata.
|
20
|
+
def metadata
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
# Downloads the blob to a tempfile on disk. Yields the tempfile.
|
26
|
+
def download_blob_to_tempfile(&block) #:doc:
|
27
|
+
blob.open tmpdir: tmpdir, &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def logger #:doc:
|
31
|
+
ActiveStorage.logger
|
32
|
+
end
|
33
|
+
|
34
|
+
def tmpdir #:doc:
|
35
|
+
Dir.tmpdir
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
# Extracts width and height in pixels from an image blob.
|
5
|
+
#
|
6
|
+
# If the image contains EXIF data indicating its angle is 90 or 270 degrees, its width and height are swapped for convenience.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# ActiveStorage::Analyzer::ImageAnalyzer.new(blob).metadata
|
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
|
+
class Analyzer::ImageAnalyzer < Analyzer
|
16
|
+
def self.accept?(blob)
|
17
|
+
blob.image?
|
18
|
+
end
|
19
|
+
|
20
|
+
def metadata
|
21
|
+
read_image do |image|
|
22
|
+
if rotated_image?(image)
|
23
|
+
{ width: image.height, height: image.width }
|
24
|
+
else
|
25
|
+
{ width: image.width, height: image.height }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
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
|
+
end
|
47
|
+
|
48
|
+
def rotated_image?(image)
|
49
|
+
%w[ RightTop LeftBottom ].include?(image["%[orientation]"])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
# Extracts the following from a video blob:
|
5
|
+
#
|
6
|
+
# * Width (pixels)
|
7
|
+
# * Height (pixels)
|
8
|
+
# * Duration (seconds)
|
9
|
+
# * Angle (degrees)
|
10
|
+
# * Display aspect ratio
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
|
15
|
+
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
|
16
|
+
#
|
17
|
+
# When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
|
18
|
+
#
|
19
|
+
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
|
20
|
+
class Analyzer::VideoAnalyzer < Analyzer
|
21
|
+
def self.accept?(blob)
|
22
|
+
blob.video?
|
23
|
+
end
|
24
|
+
|
25
|
+
def metadata
|
26
|
+
{ width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio }.compact
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def width
|
31
|
+
if rotated?
|
32
|
+
computed_height || encoded_height
|
33
|
+
else
|
34
|
+
encoded_width
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def height
|
39
|
+
if rotated?
|
40
|
+
encoded_width
|
41
|
+
else
|
42
|
+
computed_height || encoded_height
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def duration
|
47
|
+
Float(video_stream["duration"]) if video_stream["duration"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def angle
|
51
|
+
Integer(tags["rotate"]) if tags["rotate"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def display_aspect_ratio
|
55
|
+
if descriptor = video_stream["display_aspect_ratio"]
|
56
|
+
if terms = descriptor.split(":", 2)
|
57
|
+
numerator = Integer(terms[0])
|
58
|
+
denominator = Integer(terms[1])
|
59
|
+
|
60
|
+
[numerator, denominator] unless numerator == 0
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def rotated?
|
67
|
+
angle == 90 || angle == 270
|
68
|
+
end
|
69
|
+
|
70
|
+
def computed_height
|
71
|
+
if encoded_width && display_height_scale
|
72
|
+
encoded_width * display_height_scale
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def encoded_width
|
77
|
+
@encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def encoded_height
|
81
|
+
@encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
|
82
|
+
end
|
83
|
+
|
84
|
+
def display_height_scale
|
85
|
+
@display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def tags
|
90
|
+
@tags ||= video_stream["tags"] || {}
|
91
|
+
end
|
92
|
+
|
93
|
+
def video_stream
|
94
|
+
@video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
|
95
|
+
end
|
96
|
+
|
97
|
+
def streams
|
98
|
+
probe["streams"] || []
|
99
|
+
end
|
100
|
+
|
101
|
+
def probe
|
102
|
+
download_blob_to_tempfile { |file| probe_from(file) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def probe_from(file)
|
106
|
+
IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
|
107
|
+
JSON.parse(output.read)
|
108
|
+
end
|
109
|
+
rescue Errno::ENOENT
|
110
|
+
logger.info "Skipping video analysis because FFmpeg isn't installed"
|
111
|
+
{}
|
112
|
+
end
|
113
|
+
|
114
|
+
def ffprobe_path
|
115
|
+
ActiveStorage.paths[:ffprobe] || "ffprobe"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|