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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -178
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +7 -7
  5. data/app/assets/javascripts/activestorage.esm.js +10 -18
  6. data/app/assets/javascripts/activestorage.js +11 -17
  7. data/app/controllers/active_storage/base_controller.rb +1 -1
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +2 -0
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +1 -7
  10. data/app/controllers/active_storage/disk_controller.rb +4 -2
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +3 -0
  12. data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
  13. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  14. data/app/controllers/concerns/active_storage/streaming.rb +1 -0
  15. data/app/javascript/activestorage/blob_record.js +6 -10
  16. data/app/javascript/activestorage/direct_upload.js +3 -4
  17. data/app/javascript/activestorage/direct_upload_controller.js +1 -9
  18. data/app/javascript/activestorage/index.js +3 -1
  19. data/app/jobs/active_storage/analyze_job.rb +1 -1
  20. data/app/jobs/active_storage/mirror_job.rb +1 -1
  21. data/app/jobs/active_storage/purge_job.rb +1 -1
  22. data/app/jobs/active_storage/transform_job.rb +12 -0
  23. data/app/models/active_storage/attachment.rb +88 -14
  24. data/app/models/active_storage/blob/analyzable.rb +4 -3
  25. data/app/models/active_storage/blob/identifiable.rb +1 -0
  26. data/app/models/active_storage/blob/representable.rb +7 -3
  27. data/app/models/active_storage/blob.rb +27 -47
  28. data/app/models/active_storage/current.rb +0 -10
  29. data/app/models/active_storage/filename.rb +2 -0
  30. data/app/models/active_storage/named_variant.rb +21 -0
  31. data/app/models/active_storage/preview.rb +5 -3
  32. data/app/models/active_storage/variant.rb +11 -10
  33. data/app/models/active_storage/variant_with_record.rb +20 -8
  34. data/app/models/active_storage/variation.rb +6 -4
  35. data/config/routes.rb +6 -4
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
  37. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +4 -0
  38. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +2 -0
  39. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +2 -0
  40. data/lib/active_storage/analyzer/audio_analyzer.rb +17 -5
  41. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +9 -7
  42. data/lib/active_storage/analyzer/image_analyzer/vips.rb +9 -7
  43. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  44. data/lib/active_storage/analyzer/video_analyzer.rb +15 -6
  45. data/lib/active_storage/analyzer.rb +2 -0
  46. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  47. data/lib/active_storage/attached/changes/create_one.rb +45 -3
  48. data/lib/active_storage/attached/many.rb +5 -4
  49. data/lib/active_storage/attached/model.rb +66 -43
  50. data/lib/active_storage/attached/one.rb +5 -4
  51. data/lib/active_storage/attached.rb +2 -0
  52. data/lib/active_storage/deprecator.rb +7 -0
  53. data/lib/active_storage/engine.rb +31 -9
  54. data/lib/active_storage/errors.rb +0 -3
  55. data/lib/active_storage/fixture_set.rb +7 -8
  56. data/lib/active_storage/gem_version.rb +2 -2
  57. data/lib/active_storage/log_subscriber.rb +12 -0
  58. data/lib/active_storage/previewer/video_previewer.rb +2 -0
  59. data/lib/active_storage/previewer.rb +8 -1
  60. data/lib/active_storage/reflection.rb +3 -3
  61. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  62. data/lib/active_storage/service/disk_service.rb +2 -0
  63. data/lib/active_storage/service/gcs_service.rb +11 -20
  64. data/lib/active_storage/service/mirror_service.rb +10 -5
  65. data/lib/active_storage/service/s3_service.rb +2 -0
  66. data/lib/active_storage/service.rb +4 -2
  67. data/lib/active_storage/transformers/image_processing_transformer.rb +65 -0
  68. data/lib/active_storage/transformers/transformer.rb +2 -0
  69. data/lib/active_storage/version.rb +1 -1
  70. data/lib/active_storage.rb +310 -4
  71. metadata +21 -32
  72. 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
- # We use constant paths in the following include calls to avoid a gotcha of
19
- # classic mode: If the parent application defines a top-level Analyzable, for
20
- # example, and ActiveStorage::Blob::Analyzable is not yet loaded, a bare
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 `has_secure_token`
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
- ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
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-m/image_processing] gem for the actual transformations
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-m/image_processing/blob/master/doc/minimagick.md#methods]
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-m/image_processing/blob/master/doc/vips.md#methods]
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 +link_to variant+ or +redirect_to variant+) to get the stable URL
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 `ActiveStorage.track_variants` is enabled.
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 process
19
- transform_blob { |image| create_or_find_record(image: image) } unless processed?
21
+ def image
22
+ record&.image
20
23
  end
21
24
 
22
- def processed?
23
- record.present?
25
+ def filename
26
+ ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
24
27
  end
25
28
 
26
- def image
27
- record&.image
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 "mini_mime"
3
+ require "marcel"
4
4
 
5
+ # = Active Storage \Variation
6
+ #
5
7
  # A set of transformations that can be applied to a blob to create a variant. This class is exposed via
6
8
  # the ActiveStorage::Blob#variant method and should rarely be used directly.
7
9
  #
@@ -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-m/image_processing] commands.
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 MiniMime.lookup_by_extension(format.to_s).nil?
64
+ if Marcel::Magic.by_extension(format.to_s).nil?
63
65
  raise ArgumentError, "Invalid variant format (#{format.inspect})"
64
66
  end
65
67
  end
66
68
  end
67
69
 
68
70
  def content_type
69
- MiniMime.lookup_by_extension(format.to_s).content_type
71
+ Marcel::MimeType.for(extension: format.to_s)
70
72
  end
71
73
 
72
74
  # Returns a signed key for all the +transformations+ that this variation was instantiated with.
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[5.2]
1
+ class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  # Use Active Record's configured type for primary and foreign keys
4
4
  primary_key_type, foreign_key_type = primary_and_foreign_key_types
@@ -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,5 +1,7 @@
1
1
  class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
2
2
  def change
3
+ return unless table_exists?(:active_storage_blobs)
4
+
3
5
  change_column_null(:active_storage_blobs, :checksum, true)
4
6
  end
5
7
  end
@@ -1,21 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- # Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
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 FFmpeg isn't installed"
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
- download_blob_to_tempfile do |file|
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
- download_blob_to_tempfile do |file|
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
- Integer(tags["rotate"]) if tags["rotate"]
56
+ if tags["rotate"]
57
+ Integer(tags["rotate"])
58
+ elsif side_data && side_data[0] && side_data[0]["rotation"]
59
+ Integer(side_data[0]["rotation"])
60
+ end
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 FFmpeg isn't installed"
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