activestorage 7.0.8.7 → 7.2.2.1

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -390
  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/preview_image_job.rb +16 -0
  17. data/app/jobs/active_storage/purge_job.rb +1 -1
  18. data/app/jobs/active_storage/transform_job.rb +12 -0
  19. data/app/models/active_storage/attachment.rb +101 -16
  20. data/app/models/active_storage/blob/analyzable.rb +4 -3
  21. data/app/models/active_storage/blob/identifiable.rb +1 -0
  22. data/app/models/active_storage/blob/representable.rb +15 -3
  23. data/app/models/active_storage/blob/servable.rb +22 -0
  24. data/app/models/active_storage/blob.rb +59 -72
  25. data/app/models/active_storage/current.rb +0 -10
  26. data/app/models/active_storage/filename.rb +2 -4
  27. data/app/models/active_storage/named_variant.rb +21 -0
  28. data/app/models/active_storage/preview.rb +23 -8
  29. data/app/models/active_storage/variant.rb +10 -7
  30. data/app/models/active_storage/variant_record.rb +0 -2
  31. data/app/models/active_storage/variant_with_record.rb +21 -7
  32. data/app/models/active_storage/variation.rb +5 -3
  33. data/config/routes.rb +6 -4
  34. data/db/migrate/20170806125915_create_active_storage_tables.rb +2 -2
  35. data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
  36. data/lib/active_storage/analyzer/image_analyzer/vips.rb +5 -9
  37. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  38. data/lib/active_storage/analyzer/video_analyzer.rb +9 -3
  39. data/lib/active_storage/analyzer.rb +2 -0
  40. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  41. data/lib/active_storage/attached/changes/create_one.rb +51 -4
  42. data/lib/active_storage/attached/changes/create_one_of_many.rb +5 -1
  43. data/lib/active_storage/attached/many.rb +5 -4
  44. data/lib/active_storage/attached/model.rb +96 -60
  45. data/lib/active_storage/attached/one.rb +5 -4
  46. data/lib/active_storage/attached.rb +2 -0
  47. data/lib/active_storage/deprecator.rb +7 -0
  48. data/lib/active_storage/engine.rb +7 -9
  49. data/lib/active_storage/fixture_set.rb +7 -1
  50. data/lib/active_storage/gem_version.rb +4 -4
  51. data/lib/active_storage/log_subscriber.rb +12 -0
  52. data/lib/active_storage/previewer/mupdf_previewer.rb +6 -2
  53. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +6 -2
  54. data/lib/active_storage/previewer/video_previewer.rb +1 -1
  55. data/lib/active_storage/previewer.rb +8 -1
  56. data/lib/active_storage/reflection.rb +3 -3
  57. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  58. data/lib/active_storage/service/disk_service.rb +2 -0
  59. data/lib/active_storage/service/gcs_service.rb +11 -20
  60. data/lib/active_storage/service/mirror_service.rb +10 -5
  61. data/lib/active_storage/service/s3_service.rb +2 -0
  62. data/lib/active_storage/service.rb +4 -2
  63. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  64. data/lib/active_storage/transformers/transformer.rb +2 -0
  65. data/lib/active_storage/version.rb +1 -1
  66. data/lib/active_storage.rb +5 -4
  67. metadata +18 -27
@@ -42,11 +42,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
42
42
  end
43
43
 
44
44
  def decode_verified_key
45
- ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
45
+ key = ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
46
+ key&.deep_symbolize_keys
46
47
  end
47
48
 
48
49
  def decode_verified_token
49
- ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
50
+ token = ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
51
+ token&.deep_symbolize_keys
50
52
  end
51
53
 
52
54
  def acceptable_content?(token)
@@ -12,7 +12,7 @@ class ActiveStorage::Representations::ProxyController < ActiveStorage::Represent
12
12
 
13
13
  def show
14
14
  http_cache_forever public: true do
15
- send_blob_stream @representation.image, disposition: params[:disposition]
15
+ send_blob_stream @representation, disposition: params[:disposition]
16
16
  end
17
17
  end
18
18
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/hash/except"
4
+
3
5
  module ActiveStorage::FileServer # :nodoc:
4
6
  private
5
7
  def serve_file(path, content_type:, disposition:)
6
- Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
8
+ ::Rack::Files.new(nil).serving(request, path).tap do |(status, headers, body)|
7
9
  self.status = status
8
10
  self.response_body = body
9
11
 
@@ -11,6 +13,7 @@ module ActiveStorage::FileServer # :nodoc:
11
13
  response.headers[name] = value
12
14
  end
13
15
 
16
+ response.headers.except!("X-Cascade", "x-cascade") if status == 416
14
17
  response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
15
18
  response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
16
19
  end
@@ -1,7 +1,7 @@
1
1
  import { getMetaValue } from "./helpers"
2
2
 
3
3
  export class BlobRecord {
4
- constructor(file, checksum, url) {
4
+ constructor(file, checksum, url, customHeaders = {}) {
5
5
  this.file = file
6
6
 
7
7
  this.attributes = {
@@ -17,6 +17,9 @@ export class BlobRecord {
17
17
  this.xhr.setRequestHeader("Content-Type", "application/json")
18
18
  this.xhr.setRequestHeader("Accept", "application/json")
19
19
  this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
20
+ Object.keys(customHeaders).forEach((headerKey) => {
21
+ this.xhr.setRequestHeader(headerKey, customHeaders[headerKey])
22
+ })
20
23
 
21
24
  const csrfToken = getMetaValue("csrf-token")
22
25
  if (csrfToken != undefined) {
@@ -5,11 +5,12 @@ import { BlobUpload } from "./blob_upload"
5
5
  let id = 0
6
6
 
7
7
  export class DirectUpload {
8
- constructor(file, url, delegate) {
8
+ constructor(file, url, delegate, customHeaders = {}) {
9
9
  this.id = ++id
10
10
  this.file = file
11
11
  this.url = url
12
12
  this.delegate = delegate
13
+ this.customHeaders = customHeaders
13
14
  }
14
15
 
15
16
  create(callback) {
@@ -19,7 +20,7 @@ export class DirectUpload {
19
20
  return
20
21
  }
21
22
 
22
- const blob = new BlobRecord(this.file, checksum, this.url)
23
+ const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders)
23
24
  notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
24
25
 
25
26
  blob.create(error => {
@@ -1,6 +1,8 @@
1
1
  import { start } from "./ujs"
2
2
  import { DirectUpload } from "./direct_upload"
3
- export { start, DirectUpload }
3
+ import { DirectUploadController } from "./direct_upload_controller"
4
+ import { DirectUploadsController } from "./direct_uploads_controller"
5
+ export { start, DirectUpload, DirectUploadController, DirectUploadsController }
4
6
 
5
7
  function autostart() {
6
8
  if (window.ActiveStorage) {
@@ -15,9 +15,9 @@ export function start() {
15
15
  }
16
16
 
17
17
  function didClick(event) {
18
- const { target } = event
19
- if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
20
- submitButtonsByForm.set(target.form, target)
18
+ const button = event.target.closest("button, input")
19
+ if (button && button.type === "submit" && button.form) {
20
+ submitButtonsByForm.set(button.form, button)
21
21
  }
22
22
  }
23
23
 
@@ -5,7 +5,7 @@ class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
5
5
  queue_as { ActiveStorage.queues[:analysis] }
6
6
 
7
7
  discard_on ActiveRecord::RecordNotFound
8
- retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
8
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
9
9
 
10
10
  def perform(blob)
11
11
  blob.analyze
@@ -7,7 +7,7 @@ class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
7
7
  queue_as { ActiveStorage.queues[:mirror] }
8
8
 
9
9
  discard_on ActiveStorage::FileNotFoundError
10
- retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
10
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
11
11
 
12
12
  def perform(key, checksum:)
13
13
  ActiveStorage::Blob.service.try(:mirror, key, checksum: checksum)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::PreviewImageJob < ActiveStorage::BaseJob
4
+ queue_as { ActiveStorage.queues[:preview_image] }
5
+
6
+ discard_on ActiveRecord::RecordNotFound, ActiveStorage::UnrepresentableError
7
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
8
+
9
+ def perform(blob, variations)
10
+ blob.preview({}).processed
11
+
12
+ variations.each do |transformations|
13
+ blob.preprocessed(transformations)
14
+ end
15
+ end
16
+ end
@@ -5,7 +5,7 @@ class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
5
5
  queue_as { ActiveStorage.queues[:purge] }
6
6
 
7
7
  discard_on ActiveRecord::RecordNotFound
8
- retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer
8
+ retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :polynomially_longer
9
9
 
10
10
  def perform(blob)
11
11
  blob.purge
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::TransformJob < ActiveStorage::BaseJob
4
+ queue_as { ActiveStorage.queues[:transform] }
5
+
6
+ discard_on ActiveRecord::RecordNotFound, ActiveStorage::UnrepresentableError
7
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
8
+
9
+ def perform(blob, transformations)
10
+ blob.representation(transformations).processed
11
+ end
12
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "active_support/core_ext/module/delegation"
4
4
 
5
+ # = Active Storage \Attachment
6
+ #
5
7
  # Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
6
8
  # but it is possible to associate many different records with the same blob. A foreign-key constraint
7
9
  # on the attachments table prevents blobs from being purged if they’re still attached to any records.
@@ -16,18 +18,34 @@ require "active_support/core_ext/module/delegation"
16
18
  # # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
17
19
  # User.first.avatars.with_all_variant_records
18
20
  class ActiveStorage::Attachment < ActiveStorage::Record
19
- self.table_name = "active_storage_attachments"
21
+ ##
22
+ # :method:
23
+ #
24
+ # Returns the associated record.
25
+ belongs_to :record, polymorphic: true, touch: ActiveStorage.touch_attachment_records
20
26
 
21
- belongs_to :record, polymorphic: true, touch: true
22
- belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
27
+ ##
28
+ # :method:
29
+ #
30
+ # Returns the associated ActiveStorage::Blob.
31
+ belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true, inverse_of: :attachments
23
32
 
24
33
  delegate_missing_to :blob
25
34
  delegate :signed_id, to: :blob
26
35
 
27
- after_create_commit :mirror_blob_later, :analyze_blob_later
36
+ after_create_commit :mirror_blob_later, :analyze_blob_later, :transform_variants_later
28
37
  after_destroy_commit :purge_dependent_blob_later
29
38
 
30
- scope :with_all_variant_records, -> { includes(blob: :variant_records) }
39
+ ##
40
+ # :singleton-method:
41
+ #
42
+ # Eager load all variant records on an attachment at once.
43
+ #
44
+ # User.first.avatars.with_all_variant_records
45
+ scope :with_all_variant_records, -> { includes(blob: {
46
+ variant_records: { image_attachment: :blob },
47
+ preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
48
+ }) }
31
49
 
32
50
  # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
33
51
  def purge
@@ -49,23 +67,61 @@ class ActiveStorage::Attachment < ActiveStorage::Record
49
67
 
50
68
  # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
51
69
  # instance for the attachment with the set of +transformations+ provided.
70
+ # Example:
71
+ #
72
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
73
+ #
74
+ # or if you are using pre-defined variants:
75
+ #
76
+ # avatar.variant(:thumb).processed.url
77
+ #
52
78
  # See ActiveStorage::Blob::Representable#variant for more information.
53
79
  #
54
80
  # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
55
81
  # unknown pre-defined variant of the attachment.
56
82
  def variant(transformations)
57
- case transformations
58
- when Symbol
59
- variant_name = transformations
60
- transformations = variants.fetch(variant_name) do
61
- record_model_name = record.to_model.model_name.name
62
- raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
63
- end
64
- end
65
-
83
+ transformations = transformations_by_name(transformations)
66
84
  blob.variant(transformations)
67
85
  end
68
86
 
87
+ # Returns an ActiveStorage::Preview instance for the attachment with the set
88
+ # of +transformations+ provided.
89
+ # Example:
90
+ #
91
+ # video.preview(resize_to_limit: [100, 100]).processed.url
92
+ #
93
+ # or if you are using pre-defined variants:
94
+ #
95
+ # video.preview(:thumb).processed.url
96
+ #
97
+ # See ActiveStorage::Blob::Representable#preview for more information.
98
+ #
99
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
100
+ # unknown pre-defined variant of the attachment.
101
+ def preview(transformations)
102
+ transformations = transformations_by_name(transformations)
103
+ blob.preview(transformations)
104
+ end
105
+
106
+ # Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the
107
+ # attachment with set of +transformations+ provided.
108
+ # Example:
109
+ #
110
+ # avatar.representation(resize_to_limit: [100, 100]).processed.url
111
+ #
112
+ # or if you are using pre-defined variants:
113
+ #
114
+ # avatar.representation(:thumb).processed.url
115
+ #
116
+ # See ActiveStorage::Blob::Representable#representation for more information.
117
+ #
118
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
119
+ # unknown pre-defined variant of the attachment.
120
+ def representation(transformations)
121
+ transformations = transformations_by_name(transformations)
122
+ blob.representation(transformations)
123
+ end
124
+
69
125
  private
70
126
  def analyze_blob_later
71
127
  blob.analyze_later unless blob.analyzed?
@@ -75,6 +131,22 @@ class ActiveStorage::Attachment < ActiveStorage::Record
75
131
  blob.mirror_later
76
132
  end
77
133
 
134
+ def transform_variants_later
135
+ preprocessed_variations = named_variants.filter_map { |_name, named_variant|
136
+ if named_variant.preprocessed?(record)
137
+ named_variant.transformations
138
+ end
139
+ }
140
+
141
+ if blob.preview_image_needed_before_processing_variants? && preprocessed_variations.any?
142
+ blob.create_preview_image_later(preprocessed_variations)
143
+ else
144
+ preprocessed_variations.each do |transformations|
145
+ blob.preprocessed(transformations)
146
+ end
147
+ end
148
+ end
149
+
78
150
  def purge_dependent_blob_later
79
151
  blob&.purge_later if dependent == :purge_later
80
152
  end
@@ -83,8 +155,21 @@ class ActiveStorage::Attachment < ActiveStorage::Record
83
155
  record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
84
156
  end
85
157
 
86
- def variants
87
- record.attachment_reflections[name]&.variants
158
+ def named_variants
159
+ record.attachment_reflections[name]&.named_variants || {}
160
+ end
161
+
162
+ def transformations_by_name(transformations)
163
+ case transformations
164
+ when Symbol
165
+ variant_name = transformations
166
+ named_variants.fetch(variant_name) do
167
+ record_model_name = record.to_model.model_name.name
168
+ raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
169
+ end.transformations
170
+ else
171
+ transformations
172
+ end
88
173
  end
89
174
  end
90
175
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_storage/analyzer/null_analyzer"
4
4
 
5
+ # = Active Storage \Blob \Analyzable
5
6
  module ActiveStorage::Blob::Analyzable
6
7
  # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
7
8
  # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
@@ -12,7 +13,7 @@ module ActiveStorage::Blob::Analyzable
12
13
  # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
13
14
  # metadata is extracted from it.
14
15
  #
15
- # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
16
+ # In a \Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
16
17
  # in an initializer:
17
18
  #
18
19
  # # Add a custom analyzer for Microsoft Office documents:
@@ -21,9 +22,9 @@ module ActiveStorage::Blob::Analyzable
21
22
  # # Remove the built-in video analyzer:
22
23
  # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
23
24
  #
24
- # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
25
+ # Outside of a \Rails application, manipulate +ActiveStorage.analyzers+ instead.
25
26
  #
26
- # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
27
+ # You won't ordinarily need to call this method from a \Rails application. New blobs are automatically and asynchronously
27
28
  # analyzed via #analyze_later when they're attached for the first time.
28
29
  def analyze
29
30
  update! metadata: metadata.merge(extract_metadata_via_analyzer)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Blob \Identifiable
3
4
  module ActiveStorage::Blob::Identifiable
4
5
  def identify
5
6
  identify_without_saving
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mini_mime"
3
+ require "marcel"
4
4
 
5
5
  module ActiveStorage::Blob::Representable
6
6
  extend ActiveSupport::Concern
@@ -98,6 +98,18 @@ module ActiveStorage::Blob::Representable
98
98
  variable? || previewable?
99
99
  end
100
100
 
101
+ def preview_image_needed_before_processing_variants? # :nodoc:
102
+ previewable? && !preview_image.attached?
103
+ end
104
+
105
+ def create_preview_image_later(variations) # :nodoc:
106
+ ActiveStorage::PreviewImageJob.perform_later(self, variations) if representable?
107
+ end
108
+
109
+ def preprocessed(transformations) # :nodoc:
110
+ ActiveStorage::TransformJob.perform_later(self, transformations) if representable?
111
+ end
112
+
101
113
  private
102
114
  def default_variant_transformations
103
115
  { format: default_variant_format }
@@ -112,10 +124,10 @@ module ActiveStorage::Blob::Representable
112
124
  end
113
125
 
114
126
  def format
115
- if filename.extension.present? && MiniMime.lookup_by_extension(filename.extension)&.content_type == content_type
127
+ if filename.extension.present? && Marcel::MimeType.for(extension: filename.extension) == content_type
116
128
  filename.extension
117
129
  else
118
- MiniMime.lookup_by_content_type(content_type)&.extension
130
+ Marcel::Magic.new(content_type.to_s).extensions.first
119
131
  end
120
132
  end
121
133
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::Blob::Servable # :nodoc:
4
+ def content_type_for_serving
5
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
6
+ end
7
+
8
+ def forced_disposition_for_serving
9
+ if forcibly_serve_as_binary? || !allowed_inline?
10
+ :attachment
11
+ end
12
+ end
13
+
14
+ private
15
+ def forcibly_serve_as_binary?
16
+ ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
17
+ end
18
+
19
+ def allowed_inline?
20
+ ActiveStorage.content_types_allowed_inline.include?(content_type)
21
+ end
22
+ end
@@ -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,27 +17,6 @@
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"
38
-
39
20
  MINIMUM_TOKEN_LENGTH = 28
40
21
 
41
22
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
@@ -44,14 +25,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
44
25
  class_attribute :services, default: {}
45
26
  class_attribute :service, instance_accessor: false
46
27
 
28
+ ##
29
+ # :method:
30
+ #
31
+ # Returns the associated ActiveStorage::Attachment instances.
47
32
  has_many :attachments
48
33
 
34
+ ##
35
+ # :singleton-method:
36
+ #
37
+ # Returns the blobs that aren't attached to any record.
49
38
  scope :unattached, -> { where.missing(:attachments) }
50
39
 
51
40
  after_initialize do
52
41
  self.service_name ||= self.class.service&.name
53
42
  end
54
43
 
44
+ after_update :touch_attachments
45
+
55
46
  after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
56
47
 
57
48
  before_destroy(prepend: true) do
@@ -141,32 +132,56 @@ class ActiveStorage::Blob < ActiveStorage::Record
141
132
 
142
133
  def scope_for_strict_loading # :nodoc:
143
134
  if strict_loading_by_default? && ActiveStorage.track_variants
144
- includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
135
+ includes(
136
+ variant_records: { image_attachment: :blob },
137
+ preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
138
+ )
145
139
  else
146
140
  all
147
141
  end
148
142
  end
149
143
 
150
144
  # Concatenate multiple blobs into a single "composed" blob.
151
- def compose(blobs, filename:, content_type: nil, metadata: nil)
145
+ def compose(blobs, key: nil, filename:, content_type: nil, metadata: nil)
152
146
  raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
153
147
 
154
148
  content_type ||= blobs.pluck(:content_type).compact.first
155
149
 
156
- new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
150
+ new(key: key, filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
157
151
  combined_blob.compose(blobs.pluck(:key))
158
152
  combined_blob.save!
159
153
  end
160
154
  end
155
+
156
+ def validate_service_configuration(service_name, model_class, association_name) # :nodoc:
157
+ if service_name
158
+ services.fetch(service_name) do
159
+ raise ArgumentError, "Cannot configure service #{service_name.inspect} for #{model_class}##{association_name}"
160
+ end
161
+ else
162
+ validate_global_service_configuration
163
+ end
164
+ end
165
+
166
+ def validate_global_service_configuration # :nodoc:
167
+ if connected? && table_exists? && Rails.configuration.active_storage.service.nil?
168
+ raise RuntimeError, "Missing Active Storage service name. Specify Active Storage service name for config.active_storage.service in config/environments/#{Rails.env}.rb"
169
+ end
170
+ end
161
171
  end
162
172
 
173
+ include Analyzable
174
+ include Identifiable
175
+ include Representable
176
+ include Servable
177
+
163
178
  # 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)
179
+ def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
165
180
  super
166
181
  end
167
182
 
168
183
  # 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.
184
+ # secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
170
185
  # This key is not intended to be revealed directly to the user.
171
186
  # Always refer to blobs using the signed_id or a verified form of the key.
172
187
  def key
@@ -229,16 +244,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
229
244
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
230
245
  end
231
246
 
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
247
 
243
248
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
244
249
  # 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 +314,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
309
314
  end
310
315
 
311
316
  def mirror_later # :nodoc:
312
- ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
317
+ service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
313
318
  end
314
319
 
315
320
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
@@ -340,36 +345,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
340
345
  services.fetch(service_name)
341
346
  end
342
347
 
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
348
  private
369
349
  def compute_checksum_in_chunks(io)
350
+ raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
351
+
370
352
  OpenSSL::Digest::MD5.new.tap do |checksum|
371
- while chunk = io.read(5.megabytes)
372
- checksum << chunk
353
+ read_buffer = "".b
354
+ while io.read(5.megabytes, read_buffer)
355
+ checksum << read_buffer
373
356
  end
374
357
 
375
358
  io.rewind
@@ -380,14 +363,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
380
363
  Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
381
364
  end
382
365
 
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
366
  def web_image?
392
367
  ActiveStorage.web_image_content_types.include?(content_type)
393
368
  end
@@ -402,6 +377,18 @@ class ActiveStorage::Blob < ActiveStorage::Record
402
377
  end
403
378
  end
404
379
 
380
+ def touch_attachments
381
+ attachments.then do |relation|
382
+ if ActiveStorage.touch_attachment_records
383
+ relation.includes(:record)
384
+ else
385
+ relation
386
+ end
387
+ end.each do |attachment|
388
+ attachment.touch
389
+ end
390
+ end
391
+
405
392
  def update_service_metadata
406
393
  service.update_metadata key, **service_metadata if service_metadata.any?
407
394
  end