activestorage 6.1.6.1 → 7.0.3.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -212
  3. data/README.md +25 -11
  4. data/app/assets/javascripts/activestorage.esm.js +844 -0
  5. data/app/assets/javascripts/activestorage.js +257 -376
  6. data/app/controllers/active_storage/base_controller.rb +0 -9
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +15 -4
  8. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  9. data/app/controllers/active_storage/disk_controller.rb +1 -0
  10. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +7 -3
  12. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  13. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  14. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  15. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  16. data/app/javascript/activestorage/ujs.js +1 -1
  17. data/app/models/active_storage/attachment.rb +35 -2
  18. data/app/models/active_storage/blob/representable.rb +7 -5
  19. data/app/models/active_storage/blob.rb +92 -36
  20. data/app/models/active_storage/current.rb +12 -2
  21. data/app/models/active_storage/preview.rb +6 -4
  22. data/app/models/active_storage/record.rb +1 -1
  23. data/app/models/active_storage/variant.rb +3 -6
  24. data/app/models/active_storage/variant_record.rb +2 -0
  25. data/app/models/active_storage/variant_with_record.rb +9 -5
  26. data/app/models/active_storage/variation.rb +2 -2
  27. data/config/routes.rb +10 -10
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +32 -11
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +4 -0
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +17 -2
  31. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
  32. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  33. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  34. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  35. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  36. data/lib/active_storage/analyzer/video_analyzer.rb +27 -12
  37. data/lib/active_storage/analyzer.rb +8 -4
  38. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  39. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  40. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  41. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  42. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  43. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  44. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  45. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  46. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  47. data/lib/active_storage/attached/changes.rb +7 -1
  48. data/lib/active_storage/attached/many.rb +27 -15
  49. data/lib/active_storage/attached/model.rb +35 -7
  50. data/lib/active_storage/attached/one.rb +32 -27
  51. data/lib/active_storage/downloader.rb +4 -4
  52. data/lib/active_storage/engine.rb +45 -1
  53. data/lib/active_storage/fixture_set.rb +76 -0
  54. data/lib/active_storage/gem_version.rb +4 -4
  55. data/lib/active_storage/previewer.rb +4 -4
  56. data/lib/active_storage/reflection.rb +12 -2
  57. data/lib/active_storage/service/azure_storage_service.rb +28 -6
  58. data/lib/active_storage/service/configurator.rb +1 -1
  59. data/lib/active_storage/service/disk_service.rb +24 -19
  60. data/lib/active_storage/service/gcs_service.rb +109 -11
  61. data/lib/active_storage/service/mirror_service.rb +2 -2
  62. data/lib/active_storage/service/registry.rb +1 -1
  63. data/lib/active_storage/service/s3_service.rb +37 -15
  64. data/lib/active_storage/service.rb +13 -5
  65. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  66. data/lib/active_storage/transformers/transformer.rb +1 -1
  67. data/lib/active_storage/version.rb +1 -1
  68. data/lib/active_storage.rb +4 -0
  69. metadata +24 -14
  70. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -7,13 +7,4 @@ class ActiveStorage::BaseController < ActionController::Base
7
7
  protect_from_forgery with: :exception
8
8
 
9
9
  self.etag_with_template_digest = false
10
-
11
- private
12
- def stream(blob)
13
- blob.download do |chunk|
14
- response.stream.write chunk
15
- end
16
- ensure
17
- response.stream.close
18
- end
19
10
  end
@@ -1,14 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Proxy files through application. This avoids having a redirect and makes files easier to cache.
4
+ #
5
+ # WARNING: All Active Storage controllers are publicly accessible by default. The
6
+ # generated URLs are hard to guess, but permanent by design. If your files
7
+ # require a higher level of protection consider implementing
8
+ # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
4
9
  class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
5
10
  include ActiveStorage::SetBlob
6
- include ActiveStorage::SetHeaders
11
+ include ActiveStorage::Streaming
7
12
 
8
13
  def show
9
- http_cache_forever public: true do
10
- set_content_headers_from @blob
11
- stream @blob
14
+ if request.headers["Range"].present?
15
+ send_blob_byte_range_data @blob, request.headers["Range"]
16
+ else
17
+ http_cache_forever public: true do
18
+ response.headers["Accept-Ranges"] = "bytes"
19
+ response.headers["Content-Length"] = @blob.byte_size.to_s
20
+
21
+ send_blob_stream @blob, disposition: params[:disposition]
22
+ end
12
23
  end
13
24
  end
14
25
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
4
- # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
- # security-through-obscurity factor of the signed blob references, you'll need to implement your own
6
- # authenticated redirection controller.
4
+ #
5
+ # WARNING: All Active Storage controllers are publicly accessible by default. The
6
+ # generated URLs are hard to guess, but permanent by design. If your files
7
+ # require a higher level of protection consider implementing
8
+ # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
7
9
  class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
8
10
  include ActiveStorage::SetBlob
9
11
 
10
12
  def show
11
13
  expires_in ActiveStorage.service_urls_expire_in
12
- redirect_to @blob.url(disposition: params[:disposition])
14
+ redirect_to @blob.url(disposition: params[:disposition]), allow_other_host: true
13
15
  end
14
16
  end
@@ -23,6 +23,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
23
23
  if token = decode_verified_token
24
24
  if acceptable_content?(token)
25
25
  named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
26
+ head :no_content
26
27
  else
27
28
  head :unprocessable_entity
28
29
  end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController #:nodoc:
3
+ class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController # :nodoc:
4
4
  include ActiveStorage::SetBlob
5
5
 
6
6
  before_action :set_representation
7
7
 
8
8
  private
9
+ def blob_scope
10
+ ActiveStorage::Blob.scope_for_strict_loading
11
+ end
12
+
9
13
  def set_representation
10
14
  @representation = @blob.representation(params[:variation_key]).processed
11
15
  rescue ActiveSupport::MessageVerifier::InvalidSignature
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Proxy files through application. This avoids having a redirect and makes files easier to cache.
4
+ #
5
+ # WARNING: All Active Storage controllers are publicly accessible by default. The
6
+ # generated URLs are hard to guess, but permanent by design. If your files
7
+ # require a higher level of protection consider implementing
8
+ # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
4
9
  class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
5
- include ActiveStorage::SetHeaders
10
+ include ActiveStorage::Streaming
6
11
 
7
12
  def show
8
13
  http_cache_forever public: true do
9
- set_content_headers_from @representation.image
10
- stream @representation
14
+ send_blob_stream @representation.image, disposition: params[:disposition]
11
15
  end
12
16
  end
13
17
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
4
- # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
- # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
6
- # authenticated redirection controller.
4
+ #
5
+ # WARNING: All Active Storage controllers are publicly accessible by default. The
6
+ # generated URLs are hard to guess, but permanent by design. If your files
7
+ # require a higher level of protection consider implementing
8
+ # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
7
9
  class ActiveStorage::Representations::RedirectController < ActiveStorage::Representations::BaseController
8
10
  def show
9
11
  expires_in ActiveStorage.service_urls_expire_in
10
- redirect_to @representation.url(disposition: params[:disposition])
12
+ redirect_to @representation.url(disposition: params[:disposition]), allow_other_host: true
11
13
  end
12
14
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveStorage::SetBlob #:nodoc:
3
+ module ActiveStorage::SetBlob # :nodoc:
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
@@ -9,8 +9,12 @@ module ActiveStorage::SetBlob #:nodoc:
9
9
 
10
10
  private
11
11
  def set_blob
12
- @blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
12
+ @blob = blob_scope.find_signed!(params[:signed_blob_id] || params[:signed_id])
13
13
  rescue ActiveSupport::MessageVerifier::InvalidSignature
14
14
  head :not_found
15
15
  end
16
+
17
+ def blob_scope
18
+ ActiveStorage::Blob
19
+ end
16
20
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs.
3
+ # Sets the <tt>ActiveStorage::Current.url_options</tt> attribute, which the disk service uses to generate URLs.
4
4
  # Include this concern in custom controllers that call ActiveStorage::Blob#url,
5
5
  # ActiveStorage::Variant#url, or ActiveStorage::Preview#url so the disk service can
6
- # generate URLs using the same host, protocol, and base path as the current request.
6
+ # generate URLs using the same host, protocol, and port as the current request.
7
7
  module ActiveStorage::SetCurrent
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
11
  before_action do
12
- ActiveStorage::Current.host = request.base_url
12
+ ActiveStorage::Current.url_options = { protocol: request.protocol, host: request.host, port: request.port }
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module ActiveStorage::Streaming
6
+ DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
7
+
8
+ include ActionController::DataStreaming
9
+ include ActionController::Live
10
+
11
+ private
12
+ # Stream the blob in byte ranges specified through the header
13
+ def send_blob_byte_range_data(blob, range_header, disposition: nil)
14
+ ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
15
+
16
+ return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
17
+
18
+ if ranges.length == 1
19
+ range = ranges.first
20
+ content_type = blob.content_type_for_serving
21
+ data = blob.download_chunk(range)
22
+
23
+ response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{blob.byte_size}"
24
+ else
25
+ boundary = SecureRandom.hex
26
+ content_type = "multipart/byteranges; boundary=#{boundary}"
27
+ data = +""
28
+
29
+ ranges.compact.each do |range|
30
+ chunk = blob.download_chunk(range)
31
+
32
+ data << "\r\n--#{boundary}\r\n"
33
+ data << "Content-Type: #{blob.content_type_for_serving}\r\n"
34
+ data << "Content-Range: bytes #{range.begin}-#{range.end}/#{blob.byte_size}\r\n\r\n"
35
+ data << chunk
36
+ end
37
+
38
+ data << "\r\n--#{boundary}--\r\n"
39
+ end
40
+
41
+ response.headers["Accept-Ranges"] = "bytes"
42
+ response.headers["Content-Length"] = data.length.to_s
43
+
44
+ send_data(
45
+ data,
46
+ disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
47
+ filename: blob.filename.sanitized,
48
+ status: :partial_content,
49
+ type: content_type
50
+ )
51
+ end
52
+
53
+ # Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
54
+ # The content type and filename is set directly from the +blob+.
55
+ def send_blob_stream(blob, disposition: nil) # :doc:
56
+ send_stream(
57
+ filename: blob.filename.sanitized,
58
+ disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
59
+ type: blob.content_type_for_serving) do |stream|
60
+ blob.download do |chunk|
61
+ stream.write chunk
62
+ end
63
+ end
64
+ end
65
+ end
@@ -9,7 +9,7 @@ export function start() {
9
9
  if (!started) {
10
10
  started = true
11
11
  document.addEventListener("click", didClick, true)
12
- document.addEventListener("submit", didSubmitForm)
12
+ document.addEventListener("submit", didSubmitForm, true)
13
13
  document.addEventListener("ajax:before", didSubmitRemoteElement)
14
14
  }
15
15
  }
@@ -7,6 +7,14 @@ require "active_support/core_ext/module/delegation"
7
7
  # on the attachments table prevents blobs from being purged if they’re still attached to any records.
8
8
  #
9
9
  # Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob].
10
+ #
11
+ # If you wish to preload attachments or blobs, you can use these scopes:
12
+ #
13
+ # # preloads attachments, their corresponding blobs, and variant records (if using `ActiveStorage.track_variants`)
14
+ # User.all.with_attached_avatars
15
+ #
16
+ # # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
17
+ # User.first.avatars.with_all_variant_records
10
18
  class ActiveStorage::Attachment < ActiveStorage::Record
11
19
  self.table_name = "active_storage_attachments"
12
20
 
@@ -19,11 +27,13 @@ class ActiveStorage::Attachment < ActiveStorage::Record
19
27
  after_create_commit :mirror_blob_later, :analyze_blob_later
20
28
  after_destroy_commit :purge_dependent_blob_later
21
29
 
30
+ scope :with_all_variant_records, -> { includes(blob: :variant_records) }
31
+
22
32
  # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
23
33
  def purge
24
34
  transaction do
25
35
  delete
26
- record&.touch
36
+ record.touch if record&.persisted?
27
37
  end
28
38
  blob&.purge
29
39
  end
@@ -32,11 +42,30 @@ class ActiveStorage::Attachment < ActiveStorage::Record
32
42
  def purge_later
33
43
  transaction do
34
44
  delete
35
- record&.touch
45
+ record.touch if record&.persisted?
36
46
  end
37
47
  blob&.purge_later
38
48
  end
39
49
 
50
+ # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
51
+ # instance for the attachment with the set of +transformations+ provided.
52
+ # See ActiveStorage::Blob::Representable#variant for more information.
53
+ #
54
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
55
+ # unknown pre-defined variant of the attachment.
56
+ 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
+
66
+ blob.variant(transformations)
67
+ end
68
+
40
69
  private
41
70
  def analyze_blob_later
42
71
  blob.analyze_later unless blob.analyzed?
@@ -53,6 +82,10 @@ class ActiveStorage::Attachment < ActiveStorage::Record
53
82
  def dependent
54
83
  record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
55
84
  end
85
+
86
+ def variants
87
+ record.attachment_reflections[name]&.variants
88
+ end
56
89
  end
57
90
 
58
91
  ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
@@ -12,8 +12,8 @@ module ActiveStorage::Blob::Representable
12
12
  has_one_attached :preview_image
13
13
  end
14
14
 
15
- # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
16
- # files, and it allows any image to be transformed for size, colors, and the like. Example:
15
+ # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord instance with the set of +transformations+ provided.
16
+ # This is only relevant for image files, and it allows any image to be transformed for size, colors, and the like. Example:
17
17
  #
18
18
  # avatar.variant(resize_to_limit: [100, 100]).processed.url
19
19
  #
@@ -28,8 +28,9 @@ module ActiveStorage::Blob::Representable
28
28
  # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
29
29
  # can then produce on-demand.
30
30
  #
31
- # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
32
- # variable, call ActiveStorage::Blob#variable?.
31
+ # Raises ActiveStorage::InvariableError if the variant processor cannot
32
+ # transform the blob. To determine whether a blob is variable, call
33
+ # ActiveStorage::Blob#variable?.
33
34
  def variant(transformations)
34
35
  if variable?
35
36
  variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
@@ -38,7 +39,8 @@ module ActiveStorage::Blob::Representable
38
39
  end
39
40
  end
40
41
 
41
- # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
42
+ # Returns true if the variant processor can transform the blob (its content
43
+ # type is in +ActiveStorage.variable_content_types+).
42
44
  def variable?
43
45
  ActiveStorage.variable_content_types.include?(content_type)
44
46
  end
@@ -39,7 +39,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
39
39
  MINIMUM_TOKEN_LENGTH = 28
40
40
 
41
41
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
42
- store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
42
+ store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
43
43
 
44
44
  class_attribute :services, default: {}
45
45
  class_attribute :service, instance_accessor: false
@@ -52,13 +52,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
52
52
  self.service_name ||= self.class.service&.name
53
53
  end
54
54
 
55
- after_update_commit :update_service_metadata, if: :content_type_previously_changed?
55
+ after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
56
56
 
57
57
  before_destroy(prepend: true) do
58
58
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
59
59
  end
60
60
 
61
61
  validates :service_name, presence: true
62
+ validates :checksum, presence: true, unless: :composed
62
63
 
63
64
  validate do
64
65
  if service_name_changed? && service_name.present?
@@ -86,21 +87,13 @@ class ActiveStorage::Blob < ActiveStorage::Record
86
87
  super(id, purpose: purpose)
87
88
  end
88
89
 
89
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
90
- new(filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
91
- blob.upload(io, identify: identify)
92
- end
93
- end
94
-
95
- deprecate :build_after_upload
96
-
97
- def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
90
+ def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
98
91
  new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
99
92
  blob.unfurl(io, identify: identify)
100
93
  end
101
94
  end
102
95
 
103
- def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
96
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
104
97
  build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
105
98
  end
106
99
 
@@ -115,9 +108,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
115
108
  end
116
109
  end
117
110
 
118
- alias_method :create_after_upload!, :create_and_upload!
119
- deprecate create_after_upload!: :create_and_upload!
120
-
121
111
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
122
112
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
123
113
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
@@ -137,7 +127,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
137
127
  end
138
128
 
139
129
  # Customize signed ID purposes for backwards compatibility.
140
- def combine_signed_id_purposes(purpose) #:nodoc:
130
+ def combine_signed_id_purposes(purpose) # :nodoc:
141
131
  purpose.to_s
142
132
  end
143
133
 
@@ -145,14 +135,34 @@ class ActiveStorage::Blob < ActiveStorage::Record
145
135
  #
146
136
  # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
147
137
  # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
148
- def signed_id_verifier #:nodoc:
138
+ def signed_id_verifier # :nodoc:
149
139
  @signed_id_verifier ||= ActiveStorage.verifier
150
140
  end
141
+
142
+ def scope_for_strict_loading # :nodoc:
143
+ if strict_loading_by_default? && ActiveStorage.track_variants
144
+ includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
145
+ else
146
+ all
147
+ end
148
+ end
149
+
150
+ # Concatenate multiple blobs into a single "composed" blob.
151
+ def compose(blobs, filename:, content_type: nil, metadata: nil)
152
+ raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
153
+
154
+ content_type ||= blobs.pluck(:content_type).compact.first
155
+
156
+ new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
157
+ combined_blob.compose(blobs.pluck(:key))
158
+ combined_blob.save!
159
+ end
160
+ end
151
161
  end
152
162
 
153
163
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
154
- def signed_id
155
- super(purpose: :blob_id)
164
+ def signed_id(purpose: :blob_id, expires_in: nil)
165
+ super
156
166
  end
157
167
 
158
168
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
@@ -171,6 +181,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
171
181
  ActiveStorage::Filename.new(self[:filename])
172
182
  end
173
183
 
184
+ def custom_metadata
185
+ self[:metadata][:custom] || {}
186
+ end
187
+
188
+ def custom_metadata=(metadata)
189
+ self[:metadata] = self[:metadata].merge(custom: metadata)
190
+ end
191
+
174
192
  # Returns true if the content_type of this blob is in the image range, like image/png.
175
193
  def image?
176
194
  content_type.start_with?("image")
@@ -200,25 +218,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
200
218
  content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
201
219
  end
202
220
 
203
- alias_method :service_url, :url
204
- deprecate service_url: :url
205
-
206
221
  # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
207
222
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
208
223
  def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
209
- service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
224
+ service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
210
225
  end
211
226
 
212
227
  # Returns a Hash of headers for +service_url_for_direct_upload+ requests.
213
228
  def service_headers_for_direct_upload
214
- service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
229
+ service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
215
230
  end
216
231
 
217
- def content_type_for_serving #:nodoc:
232
+ def content_type_for_serving # :nodoc:
218
233
  forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
219
234
  end
220
235
 
221
- def forced_disposition_for_serving #:nodoc:
236
+ def forced_disposition_for_serving # :nodoc:
222
237
  if forcibly_serve_as_binary? || !allowed_inline?
223
238
  :attachment
224
239
  end
@@ -242,23 +257,33 @@ class ActiveStorage::Blob < ActiveStorage::Record
242
257
  upload_without_unfurling io
243
258
  end
244
259
 
245
- def unfurl(io, identify: true) #:nodoc:
260
+ def unfurl(io, identify: true) # :nodoc:
246
261
  self.checksum = compute_checksum_in_chunks(io)
247
262
  self.content_type = extract_content_type(io) if content_type.nil? || identify
248
263
  self.byte_size = io.size
249
264
  self.identified = true
250
265
  end
251
266
 
252
- def upload_without_unfurling(io) #:nodoc:
267
+ def upload_without_unfurling(io) # :nodoc:
253
268
  service.upload key, io, checksum: checksum, **service_metadata
254
269
  end
255
270
 
271
+ def compose(keys) # :nodoc:
272
+ self.composed = true
273
+ service.compose(keys, key, **service_metadata)
274
+ end
275
+
256
276
  # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
257
277
  # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
258
278
  def download(&block)
259
279
  service.download key, &block
260
280
  end
261
281
 
282
+ # Downloads a part of the file associated with this blob.
283
+ def download_chunk(range)
284
+ service.download_chunk key, range
285
+ end
286
+
262
287
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
263
288
  #
264
289
  # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
@@ -273,11 +298,17 @@ class ActiveStorage::Blob < ActiveStorage::Record
273
298
  #
274
299
  # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
275
300
  def open(tmpdir: nil, &block)
276
- service.open key, checksum: checksum,
277
- name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
301
+ service.open(
302
+ key,
303
+ checksum: checksum,
304
+ verify: !composed,
305
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ],
306
+ tmpdir: tmpdir,
307
+ &block
308
+ )
278
309
  end
279
310
 
280
- def mirror_later #:nodoc:
311
+ def mirror_later # :nodoc:
281
312
  ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
282
313
  end
283
314
 
@@ -294,7 +325,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
294
325
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
295
326
  def purge
296
327
  destroy
297
- delete
328
+ delete if previously_persisted?
298
329
  rescue ActiveRecord::InvalidForeignKey
299
330
  end
300
331
 
@@ -309,9 +340,34 @@ class ActiveStorage::Blob < ActiveStorage::Record
309
340
  services.fetch(service_name)
310
341
  end
311
342
 
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
+
312
368
  private
313
369
  def compute_checksum_in_chunks(io)
314
- Digest::MD5.new.tap do |checksum|
370
+ OpenSSL::Digest::MD5.new.tap do |checksum|
315
371
  while chunk = io.read(5.megabytes)
316
372
  checksum << chunk
317
373
  end
@@ -338,11 +394,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
338
394
 
339
395
  def service_metadata
340
396
  if forcibly_serve_as_binary?
341
- { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
397
+ { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
342
398
  elsif !allowed_inline?
343
- { content_type: content_type, disposition: :attachment, filename: filename }
399
+ { content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
344
400
  else
345
- { content_type: content_type }
401
+ { content_type: content_type, custom_metadata: custom_metadata }
346
402
  end
347
403
  end
348
404
 
@@ -1,5 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
4
- attribute :host
3
+ class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
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
5
15
  end