activestorage 7.0.0 → 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 +158 -178
- data/MIT-LICENSE +1 -1
- data/README.md +7 -7
- data/app/assets/javascripts/activestorage.esm.js +10 -18
- data/app/assets/javascripts/activestorage.js +11 -17
- data/app/controllers/active_storage/base_controller.rb +1 -1
- data/app/controllers/active_storage/blobs/proxy_controller.rb +2 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +1 -7
- data/app/controllers/active_storage/disk_controller.rb +4 -2
- data/app/controllers/active_storage/representations/proxy_controller.rb +3 -0
- 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/streaming.rb +1 -0
- data/app/javascript/activestorage/blob_record.js +6 -10
- data/app/javascript/activestorage/direct_upload.js +3 -4
- data/app/javascript/activestorage/direct_upload_controller.js +1 -9
- data/app/javascript/activestorage/index.js +3 -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 +88 -14
- 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 +7 -3
- data/app/models/active_storage/blob.rb +27 -47
- data/app/models/active_storage/current.rb +0 -10
- 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 +5 -3
- data/app/models/active_storage/variant.rb +11 -10
- data/app/models/active_storage/variant_with_record.rb +20 -8
- data/app/models/active_storage/variation.rb +6 -4
- data/config/routes.rb +6 -4
- data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +4 -0
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +2 -0
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +2 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +17 -5
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +9 -7
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +9 -7
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +15 -6
- data/lib/active_storage/analyzer.rb +2 -0
- data/lib/active_storage/attached/changes/create_many.rb +8 -3
- data/lib/active_storage/attached/changes/create_one.rb +45 -3
- data/lib/active_storage/attached/many.rb +5 -4
- data/lib/active_storage/attached/model.rb +66 -43
- data/lib/active_storage/attached/one.rb +5 -4
- data/lib/active_storage/attached.rb +2 -0
- data/lib/active_storage/deprecator.rb +7 -0
- data/lib/active_storage/engine.rb +31 -9
- data/lib/active_storage/errors.rb +0 -3
- data/lib/active_storage/fixture_set.rb +7 -8
- data/lib/active_storage/gem_version.rb +2 -2
- data/lib/active_storage/log_subscriber.rb +12 -0
- data/lib/active_storage/previewer/video_previewer.rb +2 -0
- data/lib/active_storage/previewer.rb +8 -1
- data/lib/active_storage/reflection.rb +3 -3
- data/lib/active_storage/service/azure_storage_service.rb +2 -0
- data/lib/active_storage/service/disk_service.rb +2 -0
- data/lib/active_storage/service/gcs_service.rb +11 -20
- data/lib/active_storage/service/mirror_service.rb +10 -5
- data/lib/active_storage/service/s3_service.rb +2 -0
- data/lib/active_storage/service.rb +4 -2
- data/lib/active_storage/transformers/image_processing_transformer.rb +65 -0
- data/lib/active_storage/transformers/transformer.rb +2 -0
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +310 -4
- metadata +21 -32
- data/lib/active_storage/direct_upload_token.rb +0 -59
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# = Active Storage \Blob
|
4
|
+
#
|
3
5
|
# A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
|
4
6
|
# Blobs can be created in two ways:
|
5
7
|
#
|
@@ -15,24 +17,9 @@
|
|
15
17
|
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
|
16
18
|
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
|
17
19
|
class ActiveStorage::Blob < ActiveStorage::Record
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
# include Analyzable
|
23
|
-
#
|
24
|
-
# would resolve to the top-level one, const_missing would not be triggered,
|
25
|
-
# and therefore ActiveStorage::Blob::Analyzable would not be autoloaded.
|
26
|
-
#
|
27
|
-
# By using qualified names, we ensure const_missing is invoked if needed.
|
28
|
-
# Please, note that Ruby 2.5 or newer is required, so Object is not checked
|
29
|
-
# when looking up the ancestors of ActiveStorage::Blob.
|
30
|
-
#
|
31
|
-
# Zeitwerk mode does not have this gotcha. If we ever drop classic mode, this
|
32
|
-
# can be simplified, bare constant names would just work.
|
33
|
-
include ActiveStorage::Blob::Analyzable
|
34
|
-
include ActiveStorage::Blob::Identifiable
|
35
|
-
include ActiveStorage::Blob::Representable
|
20
|
+
include Analyzable
|
21
|
+
include Identifiable
|
22
|
+
include Representable
|
36
23
|
|
37
24
|
self.table_name = "active_storage_blobs"
|
38
25
|
|
@@ -44,14 +31,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
44
31
|
class_attribute :services, default: {}
|
45
32
|
class_attribute :service, instance_accessor: false
|
46
33
|
|
34
|
+
##
|
35
|
+
# :method:
|
36
|
+
#
|
37
|
+
# Returns the associated +ActiveStorage::Attachment+s.
|
47
38
|
has_many :attachments
|
48
39
|
|
40
|
+
##
|
41
|
+
# :singleton-method:
|
42
|
+
#
|
43
|
+
# Returns the blobs that aren't attached to any record.
|
49
44
|
scope :unattached, -> { where.missing(:attachments) }
|
50
45
|
|
51
46
|
after_initialize do
|
52
47
|
self.service_name ||= self.class.service&.name
|
53
48
|
end
|
54
49
|
|
50
|
+
after_update :touch_attachment_records
|
51
|
+
|
55
52
|
after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
|
56
53
|
|
57
54
|
before_destroy(prepend: true) do
|
@@ -120,7 +117,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
120
117
|
# To prevent problems with case-insensitive filesystems, especially in combination
|
121
118
|
# with databases which treat indices as case-sensitive, all blob keys generated are going
|
122
119
|
# to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
|
123
|
-
# the same or higher amount of entropy as in the base-58 encoding used by
|
120
|
+
# the same or higher amount of entropy as in the base-58 encoding used by +has_secure_token+
|
124
121
|
# the number of bytes used is increased to 28 from the standard 24
|
125
122
|
def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
|
126
123
|
SecureRandom.base36(length)
|
@@ -161,12 +158,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
161
158
|
end
|
162
159
|
|
163
160
|
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
|
164
|
-
def signed_id(purpose: :blob_id, expires_in: nil)
|
161
|
+
def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
|
165
162
|
super
|
166
163
|
end
|
167
164
|
|
168
165
|
# Returns the key pointing to the file on the service that's associated with this blob. The key is the
|
169
|
-
# secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
|
166
|
+
# secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
|
170
167
|
# This key is not intended to be revealed directly to the user.
|
171
168
|
# Always refer to blobs using the signed_id or a verified form of the key.
|
172
169
|
def key
|
@@ -309,7 +306,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
309
306
|
end
|
310
307
|
|
311
308
|
def mirror_later # :nodoc:
|
312
|
-
|
309
|
+
service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
|
313
310
|
end
|
314
311
|
|
315
312
|
# Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
|
@@ -340,33 +337,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
340
337
|
services.fetch(service_name)
|
341
338
|
end
|
342
339
|
|
343
|
-
def content_type=(value)
|
344
|
-
unless ActiveStorage.silence_invalid_content_types_warning
|
345
|
-
if INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7.include?(value)
|
346
|
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
347
|
-
#{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
|
348
|
-
If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.variable_content_types`.
|
349
|
-
Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
|
350
|
-
MSG
|
351
|
-
end
|
352
|
-
|
353
|
-
if INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7.include?(value)
|
354
|
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
355
|
-
#{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
|
356
|
-
If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.content_types_to_serve_as_binary`.
|
357
|
-
Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
|
358
|
-
MSG
|
359
|
-
end
|
360
|
-
end
|
361
|
-
|
362
|
-
super
|
363
|
-
end
|
364
|
-
|
365
|
-
INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7 = ["image/jpg", "image/pjpeg", "image/bmp"]
|
366
|
-
INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7 = ["text/javascript"]
|
367
|
-
|
368
340
|
private
|
369
341
|
def compute_checksum_in_chunks(io)
|
342
|
+
raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
|
343
|
+
|
370
344
|
OpenSSL::Digest::MD5.new.tap do |checksum|
|
371
345
|
while chunk = io.read(5.megabytes)
|
372
346
|
checksum << chunk
|
@@ -402,6 +376,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
|
|
402
376
|
end
|
403
377
|
end
|
404
378
|
|
379
|
+
def touch_attachment_records
|
380
|
+
attachments.includes(:record).each do |attachment|
|
381
|
+
attachment.touch
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
405
385
|
def update_service_metadata
|
406
386
|
service.update_metadata key, **service_metadata if service_metadata.any?
|
407
387
|
end
|
@@ -2,14 +2,4 @@
|
|
2
2
|
|
3
3
|
class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
|
4
4
|
attribute :url_options
|
5
|
-
|
6
|
-
def host=(host)
|
7
|
-
ActiveSupport::Deprecation.warn("ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options=")
|
8
|
-
self.url_options = { host: host }
|
9
|
-
end
|
10
|
-
|
11
|
-
def host
|
12
|
-
ActiveSupport::Deprecation.warn("ActiveStorage::Current.host is deprecated, instead use ActiveStorage::Current.url_options")
|
13
|
-
self.url_options&.dig(:host)
|
14
|
-
end
|
15
5
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# = Active Storage \Filename
|
4
|
+
#
|
3
5
|
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
|
4
6
|
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
|
5
7
|
class ActiveStorage::Filename
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::NamedVariant # :nodoc:
|
4
|
+
attr_reader :transformations, :preprocessed
|
5
|
+
|
6
|
+
def initialize(transformations)
|
7
|
+
@preprocessed = transformations[:preprocessed]
|
8
|
+
@transformations = transformations.except(:preprocessed)
|
9
|
+
end
|
10
|
+
|
11
|
+
def preprocessed?(record)
|
12
|
+
case preprocessed
|
13
|
+
when Symbol
|
14
|
+
record.send(preprocessed)
|
15
|
+
when Proc
|
16
|
+
preprocessed.call(record)
|
17
|
+
else
|
18
|
+
preprocessed
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# = Active Storage \Preview
|
4
|
+
#
|
3
5
|
# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
|
4
6
|
# extracting its first frame, and a PDF blob can be previewed by extracting its first page.
|
5
7
|
#
|
@@ -10,7 +12,7 @@
|
|
10
12
|
# documentation for more details on what's required of previewers.
|
11
13
|
#
|
12
14
|
# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
|
13
|
-
# first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
|
15
|
+
# first previewer for which +accept?+ returns true when given the blob. In a \Rails application, add or remove previewers
|
14
16
|
# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
|
15
17
|
#
|
16
18
|
# Rails.application.config.active_storage.previewers
|
@@ -20,13 +22,13 @@
|
|
20
22
|
# Rails.application.config.active_storage.previewers << DOCXPreviewer
|
21
23
|
# # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
|
22
24
|
#
|
23
|
-
# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
|
25
|
+
# Outside of a \Rails application, modify +ActiveStorage.previewers+ instead.
|
24
26
|
#
|
25
27
|
# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
|
26
28
|
# {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
|
27
29
|
# and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
|
28
30
|
#
|
29
|
-
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
|
31
|
+
# These libraries are not provided by \Rails. You must install them yourself to use the built-in previewers. Before you
|
30
32
|
# install and use third-party software, make sure you understand the licensing implications of doing so.
|
31
33
|
class ActiveStorage::Preview
|
32
34
|
class UnprocessedError < StandardError; end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# = Active Storage \Variant
|
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.
|
6
8
|
#
|
7
|
-
# Variants rely on {ImageProcessing}[https://github.com/janko
|
9
|
+
# Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
|
8
10
|
# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
|
9
11
|
# default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
|
10
12
|
# {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
|
@@ -46,9 +48,9 @@
|
|
46
48
|
#
|
47
49
|
# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
|
48
50
|
#
|
49
|
-
# * {ImageProcessing::MiniMagick}[https://github.com/janko
|
51
|
+
# * {ImageProcessing::MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md#methods]
|
50
52
|
# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
|
51
|
-
# * {ImageProcessing::Vips}[https://github.com/janko
|
53
|
+
# * {ImageProcessing::Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md#methods]
|
52
54
|
# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
|
53
55
|
class ActiveStorage::Variant
|
54
56
|
attr_reader :blob, :variation
|
@@ -72,7 +74,7 @@ class ActiveStorage::Variant
|
|
72
74
|
|
73
75
|
# Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
|
74
76
|
#
|
75
|
-
# Use <tt>url_for(variant)</tt> (or the implied form, like
|
77
|
+
# Use <tt>url_for(variant)</tt> (or the implied form, like <tt>link_to variant</tt> or <tt>redirect_to variant</tt>) to get the stable URL
|
76
78
|
# for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
|
77
79
|
# for its redirection.
|
78
80
|
def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
|
@@ -89,17 +91,16 @@ class ActiveStorage::Variant
|
|
89
91
|
ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
|
90
92
|
end
|
91
93
|
|
92
|
-
alias_method :content_type_for_serving, :content_type
|
93
|
-
|
94
|
-
def forced_disposition_for_serving # :nodoc:
|
95
|
-
nil
|
96
|
-
end
|
97
|
-
|
98
94
|
# Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
|
99
95
|
def image
|
100
96
|
self
|
101
97
|
end
|
102
98
|
|
99
|
+
# Deletes variant file from service.
|
100
|
+
def destroy
|
101
|
+
service.delete(key)
|
102
|
+
end
|
103
|
+
|
103
104
|
private
|
104
105
|
def processed?
|
105
106
|
service.exist?(key)
|
@@ -1,35 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# = Active Storage \Variant With Record
|
4
|
+
#
|
3
5
|
# Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an
|
4
|
-
# ActiveStorage::VariantRecord. This is only used if
|
6
|
+
# ActiveStorage::VariantRecord. This is only used if +ActiveStorage.track_variants+ is enabled.
|
5
7
|
class ActiveStorage::VariantWithRecord
|
6
8
|
attr_reader :blob, :variation
|
7
9
|
delegate :service, to: :blob
|
10
|
+
delegate :content_type, to: :variation
|
8
11
|
|
9
12
|
def initialize(blob, variation)
|
10
13
|
@blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
|
11
14
|
end
|
12
15
|
|
13
16
|
def processed
|
14
|
-
process
|
17
|
+
process unless processed?
|
15
18
|
self
|
16
19
|
end
|
17
20
|
|
18
|
-
def
|
19
|
-
|
21
|
+
def image
|
22
|
+
record&.image
|
20
23
|
end
|
21
24
|
|
22
|
-
def
|
23
|
-
|
25
|
+
def filename
|
26
|
+
ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
|
24
27
|
end
|
25
28
|
|
26
|
-
|
27
|
-
|
29
|
+
# Destroys record and deletes file from service.
|
30
|
+
def destroy
|
31
|
+
record&.destroy
|
28
32
|
end
|
29
33
|
|
30
34
|
delegate :key, :url, :download, to: :image, allow_nil: true
|
31
35
|
|
32
36
|
private
|
37
|
+
def processed?
|
38
|
+
record.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
def process
|
42
|
+
transform_blob { |image| create_or_find_record(image: image) }
|
43
|
+
end
|
44
|
+
|
33
45
|
def transform_blob
|
34
46
|
blob.open do |input|
|
35
47
|
variation.transform(input) do |output|
|
@@ -1,7 +1,9 @@
|
|
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
|
#
|
@@ -10,7 +12,7 @@ require "mini_mime"
|
|
10
12
|
#
|
11
13
|
# ActiveStorage::Variation.new(resize_to_limit: [100, 100], colourspace: "b-w", rotate: "-90", saver: { trim: true })
|
12
14
|
#
|
13
|
-
# The options map directly to {ImageProcessing}[https://github.com/janko
|
15
|
+
# The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands.
|
14
16
|
class ActiveStorage::Variation
|
15
17
|
attr_reader :transformations
|
16
18
|
|
@@ -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.
|
data/config/routes.rb
CHANGED
@@ -32,16 +32,17 @@ Rails.application.routes.draw do
|
|
32
32
|
|
33
33
|
direct :rails_storage_proxy do |model, options|
|
34
34
|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
|
35
|
+
expires_at = options.delete(:expires_at)
|
35
36
|
|
36
37
|
if model.respond_to?(:signed_id)
|
37
38
|
route_for(
|
38
39
|
:rails_service_blob_proxy,
|
39
|
-
model.signed_id(expires_in: expires_in),
|
40
|
+
model.signed_id(expires_in: expires_in, expires_at: expires_at),
|
40
41
|
model.filename,
|
41
42
|
options
|
42
43
|
)
|
43
44
|
else
|
44
|
-
signed_blob_id = model.blob.signed_id(expires_in: expires_in)
|
45
|
+
signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at)
|
45
46
|
variation_key = model.variation.key
|
46
47
|
filename = model.blob.filename
|
47
48
|
|
@@ -57,16 +58,17 @@ Rails.application.routes.draw do
|
|
57
58
|
|
58
59
|
direct :rails_storage_redirect do |model, options|
|
59
60
|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
|
61
|
+
expires_at = options.delete(:expires_at)
|
60
62
|
|
61
63
|
if model.respond_to?(:signed_id)
|
62
64
|
route_for(
|
63
65
|
:rails_service_blob,
|
64
|
-
model.signed_id(expires_in: expires_in),
|
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(expires_in: expires_in)
|
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
|
@@ -1,5 +1,7 @@
|
|
1
1
|
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
2
2
|
def up
|
3
|
+
return unless table_exists?(:active_storage_blobs)
|
4
|
+
|
3
5
|
unless column_exists?(:active_storage_blobs, :service_name)
|
4
6
|
add_column :active_storage_blobs, :service_name, :string
|
5
7
|
|
@@ -12,6 +14,8 @@ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
|
12
14
|
end
|
13
15
|
|
14
16
|
def down
|
17
|
+
return unless table_exists?(:active_storage_blobs)
|
18
|
+
|
15
19
|
remove_column :active_storage_blobs, :service_name
|
16
20
|
end
|
17
21
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
2
2
|
def change
|
3
|
+
return unless table_exists?(:active_storage_blobs)
|
4
|
+
|
3
5
|
# Use Active Record's configured type for primary key
|
4
6
|
create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
|
5
7
|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
@@ -1,21 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveStorage
|
4
|
-
#
|
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.
|
5
7
|
#
|
6
8
|
# Example:
|
7
9
|
#
|
8
10
|
# ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
|
9
|
-
# # => { duration: 5.0, bit_rate: 320340 }
|
11
|
+
# # => { duration: 5.0, bit_rate: 320340, sample_rate: 44100, tags: { encoder: "Lavc57.64", ... } }
|
10
12
|
#
|
11
|
-
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
|
13
|
+
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
|
12
14
|
class Analyzer::AudioAnalyzer < Analyzer
|
13
15
|
def self.accept?(blob)
|
14
16
|
blob.audio?
|
15
17
|
end
|
16
18
|
|
17
19
|
def metadata
|
18
|
-
{ duration: duration, bit_rate: bit_rate }.compact
|
20
|
+
{ duration: duration, bit_rate: bit_rate, sample_rate: sample_rate, tags: tags }.compact
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
@@ -29,6 +31,16 @@ module ActiveStorage
|
|
29
31
|
Integer(bit_rate) if bit_rate
|
30
32
|
end
|
31
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
|
+
|
32
44
|
def audio_stream
|
33
45
|
@audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
|
34
46
|
end
|
@@ -54,7 +66,7 @@ module ActiveStorage
|
|
54
66
|
end
|
55
67
|
end
|
56
68
|
rescue Errno::ENOENT
|
57
|
-
logger.info "Skipping audio analysis because
|
69
|
+
logger.info "Skipping audio analysis because ffprobe isn't installed"
|
58
70
|
{}
|
59
71
|
end
|
60
72
|
|
@@ -10,9 +10,14 @@ module ActiveStorage
|
|
10
10
|
|
11
11
|
private
|
12
12
|
def read_image
|
13
|
-
|
13
|
+
begin
|
14
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
|
15
19
|
|
20
|
+
download_blob_to_tempfile do |file|
|
16
21
|
image = instrument("mini_magick") do
|
17
22
|
MiniMagick::Image.new(file.path)
|
18
23
|
end
|
@@ -23,13 +28,10 @@ module ActiveStorage
|
|
23
28
|
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
|
24
29
|
{}
|
25
30
|
end
|
31
|
+
rescue MiniMagick::Error => error
|
32
|
+
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
|
33
|
+
{}
|
26
34
|
end
|
27
|
-
rescue LoadError
|
28
|
-
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
29
|
-
{}
|
30
|
-
rescue MiniMagick::Error => error
|
31
|
-
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
|
32
|
-
{}
|
33
35
|
end
|
34
36
|
|
35
37
|
def rotated_image?(image)
|
@@ -10,9 +10,14 @@ module ActiveStorage
|
|
10
10
|
|
11
11
|
private
|
12
12
|
def read_image
|
13
|
-
|
13
|
+
begin
|
14
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
|
15
19
|
|
20
|
+
download_blob_to_tempfile do |file|
|
16
21
|
image = instrument("vips") do
|
17
22
|
::Vips::Image.new_from_file(file.path, access: :sequential)
|
18
23
|
end
|
@@ -23,13 +28,10 @@ module ActiveStorage
|
|
23
28
|
logger.info "Skipping image analysis because Vips doesn't support the file"
|
24
29
|
{}
|
25
30
|
end
|
31
|
+
rescue ::Vips::Error => error
|
32
|
+
logger.error "Skipping image analysis due to an Vips error: #{error.message}"
|
33
|
+
{}
|
26
34
|
end
|
27
|
-
rescue LoadError
|
28
|
-
logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
|
29
|
-
{}
|
30
|
-
rescue ::Vips::Error => error
|
31
|
-
logger.error "Skipping image analysis due to an Vips error: #{error.message}"
|
32
|
-
{}
|
33
35
|
end
|
34
36
|
|
35
37
|
ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveStorage
|
4
|
+
# = Active Storage Image \Analyzer
|
5
|
+
#
|
4
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.
|
@@ -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)
|
@@ -16,9 +18,9 @@ module ActiveStorage
|
|
16
18
|
# ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
|
17
19
|
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
|
18
20
|
#
|
19
|
-
# 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.
|
20
22
|
#
|
21
|
-
# 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.
|
22
24
|
class Analyzer::VideoAnalyzer < Analyzer
|
23
25
|
def self.accept?(blob)
|
24
26
|
blob.video?
|
@@ -51,7 +53,11 @@ module ActiveStorage
|
|
51
53
|
end
|
52
54
|
|
53
55
|
def angle
|
54
|
-
|
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
|
55
61
|
end
|
56
62
|
|
57
63
|
def display_aspect_ratio
|
@@ -66,7 +72,7 @@ module ActiveStorage
|
|
66
72
|
end
|
67
73
|
|
68
74
|
def rotated?
|
69
|
-
angle == 90 || angle == 270
|
75
|
+
angle == 90 || angle == 270 || angle == -90 || angle == -270
|
70
76
|
end
|
71
77
|
|
72
78
|
def audio?
|
@@ -95,11 +101,14 @@ module ActiveStorage
|
|
95
101
|
@display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
|
96
102
|
end
|
97
103
|
|
98
|
-
|
99
104
|
def tags
|
100
105
|
@tags ||= video_stream["tags"] || {}
|
101
106
|
end
|
102
107
|
|
108
|
+
def side_data
|
109
|
+
@side_data ||= video_stream["side_data_list"] || {}
|
110
|
+
end
|
111
|
+
|
103
112
|
def video_stream
|
104
113
|
@video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
|
105
114
|
end
|
@@ -133,7 +142,7 @@ module ActiveStorage
|
|
133
142
|
end
|
134
143
|
end
|
135
144
|
rescue Errno::ENOENT
|
136
|
-
logger.info "Skipping video analysis because
|
145
|
+
logger.info "Skipping video analysis because ffprobe isn't installed"
|
137
146
|
{}
|
138
147
|
end
|
139
148
|
|
@@ -1,6 +1,8 @@
|
|
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
7
|
# ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
|
6
8
|
class Analyzer
|