activestorage 7.0.8.1 → 7.1.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -281
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -6
  5. data/app/assets/javascripts/activestorage.esm.js +11 -7
  6. data/app/assets/javascripts/activestorage.js +12 -6
  7. data/app/controllers/active_storage/disk_controller.rb +4 -2
  8. data/app/controllers/active_storage/representations/proxy_controller.rb +1 -1
  9. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  10. data/app/javascript/activestorage/blob_record.js +4 -1
  11. data/app/javascript/activestorage/direct_upload.js +3 -2
  12. data/app/javascript/activestorage/index.js +3 -1
  13. data/app/javascript/activestorage/ujs.js +3 -3
  14. data/app/jobs/active_storage/analyze_job.rb +1 -1
  15. data/app/jobs/active_storage/mirror_job.rb +1 -1
  16. data/app/jobs/active_storage/purge_job.rb +1 -1
  17. data/app/jobs/active_storage/transform_job.rb +12 -0
  18. data/app/models/active_storage/attachment.rb +90 -15
  19. data/app/models/active_storage/blob/analyzable.rb +4 -3
  20. data/app/models/active_storage/blob/identifiable.rb +1 -0
  21. data/app/models/active_storage/blob/representable.rb +7 -3
  22. data/app/models/active_storage/blob/servable.rb +22 -0
  23. data/app/models/active_storage/blob.rb +31 -67
  24. data/app/models/active_storage/current.rb +0 -10
  25. data/app/models/active_storage/filename.rb +2 -0
  26. data/app/models/active_storage/named_variant.rb +21 -0
  27. data/app/models/active_storage/preview.rb +11 -4
  28. data/app/models/active_storage/variant.rb +10 -7
  29. data/app/models/active_storage/variant_record.rb +0 -2
  30. data/app/models/active_storage/variant_with_record.rb +21 -7
  31. data/app/models/active_storage/variation.rb +5 -3
  32. data/config/routes.rb +6 -4
  33. data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
  34. data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
  35. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  36. data/lib/active_storage/analyzer/video_analyzer.rb +3 -1
  37. data/lib/active_storage/analyzer.rb +2 -0
  38. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  39. data/lib/active_storage/attached/changes/create_one.rb +45 -3
  40. data/lib/active_storage/attached/many.rb +5 -4
  41. data/lib/active_storage/attached/model.rb +72 -43
  42. data/lib/active_storage/attached/one.rb +5 -4
  43. data/lib/active_storage/attached.rb +2 -0
  44. data/lib/active_storage/deprecator.rb +7 -0
  45. data/lib/active_storage/engine.rb +11 -7
  46. data/lib/active_storage/fixture_set.rb +7 -1
  47. data/lib/active_storage/gem_version.rb +4 -4
  48. data/lib/active_storage/log_subscriber.rb +12 -0
  49. data/lib/active_storage/previewer.rb +8 -1
  50. data/lib/active_storage/reflection.rb +3 -3
  51. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  52. data/lib/active_storage/service/disk_service.rb +2 -0
  53. data/lib/active_storage/service/gcs_service.rb +11 -20
  54. data/lib/active_storage/service/mirror_service.rb +10 -5
  55. data/lib/active_storage/service/s3_service.rb +2 -0
  56. data/lib/active_storage/service.rb +4 -2
  57. data/lib/active_storage/transformers/transformer.rb +2 -0
  58. data/lib/active_storage/version.rb +1 -1
  59. data/lib/active_storage.rb +19 -3
  60. metadata +17 -27
@@ -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,26 +17,10 @@
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
36
-
37
- self.table_name = "active_storage_blobs"
20
+ include Analyzable
21
+ include Identifiable
22
+ include Representable
23
+ include Servable
38
24
 
39
25
  MINIMUM_TOKEN_LENGTH = 28
40
26
 
@@ -44,14 +30,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
44
30
  class_attribute :services, default: {}
45
31
  class_attribute :service, instance_accessor: false
46
32
 
33
+ ##
34
+ # :method:
35
+ #
36
+ # Returns the associated ActiveStorage::Attachment instances.
47
37
  has_many :attachments
48
38
 
39
+ ##
40
+ # :singleton-method:
41
+ #
42
+ # Returns the blobs that aren't attached to any record.
49
43
  scope :unattached, -> { where.missing(:attachments) }
50
44
 
51
45
  after_initialize do
52
46
  self.service_name ||= self.class.service&.name
53
47
  end
54
48
 
49
+ after_update :touch_attachment_records
50
+
55
51
  after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
56
52
 
57
53
  before_destroy(prepend: true) do
@@ -141,7 +137,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
141
137
 
142
138
  def scope_for_strict_loading # :nodoc:
143
139
  if strict_loading_by_default? && ActiveStorage.track_variants
144
- includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
140
+ includes(
141
+ variant_records: { image_attachment: :blob },
142
+ preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
143
+ )
145
144
  else
146
145
  all
147
146
  end
@@ -161,12 +160,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
161
160
  end
162
161
 
163
162
  # 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)
163
+ def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
165
164
  super
166
165
  end
167
166
 
168
167
  # 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.
168
+ # secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
170
169
  # This key is not intended to be revealed directly to the user.
171
170
  # Always refer to blobs using the signed_id or a verified form of the key.
172
171
  def key
@@ -229,16 +228,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
229
228
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
230
229
  end
231
230
 
232
- def content_type_for_serving # :nodoc:
233
- forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
234
- end
235
-
236
- def forced_disposition_for_serving # :nodoc:
237
- if forcibly_serve_as_binary? || !allowed_inline?
238
- :attachment
239
- end
240
- end
241
-
242
231
 
243
232
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
244
233
  # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
@@ -309,7 +298,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
309
298
  end
310
299
 
311
300
  def mirror_later # :nodoc:
312
- ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
301
+ service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
313
302
  end
314
303
 
315
304
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
@@ -340,33 +329,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
340
329
  services.fetch(service_name)
341
330
  end
342
331
 
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
332
  private
369
333
  def compute_checksum_in_chunks(io)
334
+ raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
335
+
370
336
  OpenSSL::Digest::MD5.new.tap do |checksum|
371
337
  while chunk = io.read(5.megabytes)
372
338
  checksum << chunk
@@ -380,14 +346,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
380
346
  Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
381
347
  end
382
348
 
383
- def forcibly_serve_as_binary?
384
- ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
385
- end
386
-
387
- def allowed_inline?
388
- ActiveStorage.content_types_allowed_inline.include?(content_type)
389
- end
390
-
391
349
  def web_image?
392
350
  ActiveStorage.web_image_content_types.include?(content_type)
393
351
  end
@@ -402,6 +360,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
402
360
  end
403
361
  end
404
362
 
363
+ def touch_attachment_records
364
+ attachments.includes(:record).each do |attachment|
365
+ attachment.touch
366
+ end
367
+ end
368
+
405
369
  def update_service_metadata
406
370
  service.update_metadata key, **service_metadata if service_metadata.any?
407
371
  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,24 +22,28 @@
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
34
+ include ActiveStorage::Blob::Servable
35
+
32
36
  class UnprocessedError < StandardError; end
33
37
 
38
+ delegate :filename, :content_type, to: :variant
39
+
34
40
  attr_reader :blob, :variation
35
41
 
36
42
  def initialize(blob, variation_or_variation_key)
37
43
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
38
44
  end
39
45
 
40
- # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
46
+ # Processes the preview if it has not been processed yet. Returns the receiving +ActiveStorage::Preview+ instance for convenience:
41
47
  #
42
48
  # blob.preview(resize_to_limit: [100, 100]).processed.url
43
49
  #
@@ -45,6 +51,7 @@ class ActiveStorage::Preview
45
51
  # image is stored with the blob, it is only generated once.
46
52
  def processed
47
53
  process unless processed?
54
+ variant.processed
48
55
  self
49
56
  end
50
57
 
@@ -1,5 +1,7 @@
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.
@@ -51,6 +53,8 @@
51
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
56
+ include ActiveStorage::Blob::Servable
57
+
54
58
  attr_reader :blob, :variation
55
59
  delegate :service, to: :blob
56
60
  delegate :content_type, to: :variation
@@ -72,7 +76,7 @@ class ActiveStorage::Variant
72
76
 
73
77
  # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
74
78
  #
75
- # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
79
+ # 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
80
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
77
81
  # for its redirection.
78
82
  def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
@@ -89,17 +93,16 @@ class ActiveStorage::Variant
89
93
  ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
90
94
  end
91
95
 
92
- alias_method :content_type_for_serving, :content_type
93
-
94
- def forced_disposition_for_serving # :nodoc:
95
- nil
96
- end
97
-
98
96
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
99
97
  def image
100
98
  self
101
99
  end
102
100
 
101
+ # Deletes variant file from service.
102
+ def destroy
103
+ service.delete(key)
104
+ end
105
+
103
106
  private
104
107
  def processed?
105
108
  service.exist?(key)
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ActiveStorage::VariantRecord < ActiveStorage::Record
4
- self.table_name = "active_storage_variant_records"
5
-
6
4
  belongs_to :blob
7
5
  has_one_attached :image
8
6
  end
@@ -1,35 +1,49 @@
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
6
  # ActiveStorage::VariantRecord. This is only used if +ActiveStorage.track_variants+ is enabled.
5
7
  class ActiveStorage::VariantWithRecord
8
+ include ActiveStorage::Blob::Servable
9
+
6
10
  attr_reader :blob, :variation
7
11
  delegate :service, to: :blob
12
+ delegate :content_type, to: :variation
8
13
 
9
14
  def initialize(blob, variation)
10
15
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
11
16
  end
12
17
 
13
18
  def processed
14
- process
19
+ process unless processed?
15
20
  self
16
21
  end
17
22
 
18
- def process
19
- transform_blob { |image| create_or_find_record(image: image) } unless processed?
23
+ def image
24
+ record&.image
20
25
  end
21
26
 
22
- def processed?
23
- record.present?
27
+ def filename
28
+ ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
24
29
  end
25
30
 
26
- def image
27
- record&.image
31
+ # Destroys record and deletes file from service.
32
+ def destroy
33
+ record&.destroy
28
34
  end
29
35
 
30
36
  delegate :key, :url, :download, to: :image, allow_nil: true
31
37
 
32
38
  private
39
+ def processed?
40
+ record.present?
41
+ end
42
+
43
+ def process
44
+ transform_blob { |image| create_or_find_record(image: image) }
45
+ end
46
+
33
47
  def transform_blob
34
48
  blob.open do |input|
35
49
  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
  #
@@ -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,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
@@ -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)
@@ -18,7 +20,7 @@ module ActiveStorage
18
20
  #
19
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?
@@ -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
@@ -2,11 +2,12 @@
2
2
 
3
3
  module ActiveStorage
4
4
  class Attached::Changes::CreateMany # :nodoc:
5
- attr_reader :name, :record, :attachables
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
10
11
  attachments
11
12
  end
12
13
 
@@ -19,7 +20,7 @@ module ActiveStorage
19
20
  end
20
21
 
21
22
  def upload
22
- subchanges.each(&:upload)
23
+ pending_uploads.each(&:upload)
23
24
  end
24
25
 
25
26
  def save
@@ -36,6 +37,10 @@ module ActiveStorage
36
37
  ActiveStorage::Attached::Changes::CreateOneOfMany.new(name, record, attachable)
37
38
  end
38
39
 
40
+ def subchanges_without_blobs
41
+ subchanges.reject { |subchange| subchange.attachable.is_a?(ActiveStorage::Blob) }
42
+ end
43
+
39
44
  def assign_associated_attachments
40
45
  record.public_send("#{name}_attachments=", persisted_or_new_attachments)
41
46
  end
@@ -22,10 +22,26 @@ module ActiveStorage
22
22
 
23
23
  def upload
24
24
  case attachable
25
- when ActionDispatch::Http::UploadedFile, Rack::Test::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, Rack::Test::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 ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
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