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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -276
- data/MIT-LICENSE +1 -1
- data/README.md +29 -15
- data/app/assets/javascripts/activestorage.esm.js +848 -0
- data/app/assets/javascripts/activestorage.js +263 -376
- data/app/controllers/active_storage/base_controller.rb +0 -9
- data/app/controllers/active_storage/blobs/proxy_controller.rb +16 -4
- data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
- data/app/controllers/active_storage/disk_controller.rb +5 -2
- data/app/controllers/active_storage/representations/base_controller.rb +5 -1
- data/app/controllers/active_storage/representations/proxy_controller.rb +8 -3
- data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
- data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
- data/app/controllers/concerns/active_storage/file_server.rb +4 -1
- data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
- data/app/controllers/concerns/active_storage/set_current.rb +3 -3
- data/app/controllers/concerns/active_storage/streaming.rb +66 -0
- data/app/javascript/activestorage/blob_record.js +4 -1
- data/app/javascript/activestorage/direct_upload.js +3 -2
- data/app/javascript/activestorage/index.js +3 -1
- data/app/javascript/activestorage/ujs.js +1 -1
- data/app/jobs/active_storage/analyze_job.rb +1 -1
- data/app/jobs/active_storage/mirror_job.rb +1 -1
- data/app/jobs/active_storage/purge_job.rb +1 -1
- data/app/jobs/active_storage/transform_job.rb +12 -0
- data/app/models/active_storage/attachment.rb +111 -4
- data/app/models/active_storage/blob/analyzable.rb +4 -3
- data/app/models/active_storage/blob/identifiable.rb +1 -0
- data/app/models/active_storage/blob/representable.rb +14 -8
- data/app/models/active_storage/blob.rb +93 -57
- data/app/models/active_storage/current.rb +2 -2
- data/app/models/active_storage/filename.rb +2 -0
- data/app/models/active_storage/named_variant.rb +21 -0
- data/app/models/active_storage/preview.rb +11 -7
- data/app/models/active_storage/record.rb +1 -1
- data/app/models/active_storage/variant.rb +10 -12
- data/app/models/active_storage/variant_record.rb +2 -0
- data/app/models/active_storage/variant_with_record.rb +28 -12
- data/app/models/active_storage/variation.rb +7 -5
- data/config/routes.rb +12 -10
- data/db/migrate/20170806125915_create_active_storage_tables.rb +15 -6
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +77 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +41 -0
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +51 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +4 -30
- data/lib/active_storage/analyzer/video_analyzer.rb +41 -17
- data/lib/active_storage/analyzer.rb +10 -4
- data/lib/active_storage/attached/changes/create_many.rb +14 -5
- data/lib/active_storage/attached/changes/create_one.rb +46 -4
- data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_one.rb +1 -1
- data/lib/active_storage/attached/changes/detach_many.rb +18 -0
- data/lib/active_storage/attached/changes/detach_one.rb +24 -0
- data/lib/active_storage/attached/changes/purge_many.rb +27 -0
- data/lib/active_storage/attached/changes/purge_one.rb +27 -0
- data/lib/active_storage/attached/changes.rb +7 -1
- data/lib/active_storage/attached/many.rb +32 -19
- data/lib/active_storage/attached/model.rb +80 -29
- data/lib/active_storage/attached/one.rb +37 -31
- data/lib/active_storage/attached.rb +2 -0
- data/lib/active_storage/deprecator.rb +7 -0
- data/lib/active_storage/downloader.rb +4 -4
- data/lib/active_storage/engine.rb +55 -7
- data/lib/active_storage/fixture_set.rb +75 -0
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/log_subscriber.rb +12 -0
- data/lib/active_storage/previewer.rb +12 -5
- data/lib/active_storage/reflection.rb +12 -2
- data/lib/active_storage/service/azure_storage_service.rb +30 -6
- data/lib/active_storage/service/configurator.rb +1 -1
- data/lib/active_storage/service/disk_service.rb +26 -19
- data/lib/active_storage/service/gcs_service.rb +100 -11
- data/lib/active_storage/service/mirror_service.rb +12 -7
- data/lib/active_storage/service/registry.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +39 -15
- data/lib/active_storage/service.rb +17 -7
- data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
- data/lib/active_storage/transformers/transformer.rb +3 -1
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +22 -2
- metadata +30 -30
- 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 "
|
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],
|
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
|
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
|
-
|
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
|
-
|
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(
|
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[
|
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
|
14
|
-
|
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
|
-
|
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:
|
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
|
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,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
|
-
#
|
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
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
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::
|
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)
|
34
|
+
def download_blob_to_tempfile(&block) # :doc:
|
33
35
|
blob.open tmpdir: tmpdir, &block
|
34
36
|
end
|
35
37
|
|
36
|
-
def logger
|
38
|
+
def logger # :doc:
|
37
39
|
ActiveStorage.logger
|
38
40
|
end
|
39
41
|
|
40
|
-
def tmpdir
|
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
|
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
|
-
|
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=",
|
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
|
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
|
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
|
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
|
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
|
|