activestorage 6.1.7.7 → 7.0.0.alpha1

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 +126 -321
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +844 -0
  6. data/app/assets/javascripts/activestorage.js +257 -376
  7. data/app/controllers/active_storage/base_controller.rb +1 -10
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -5
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -5
  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 +26 -27
  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 +6 -9
  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 +3 -3
  27. data/config/routes.rb +10 -10
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +11 -2
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +0 -4
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +1 -3
  31. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  32. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  33. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  34. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  35. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  36. data/lib/active_storage/analyzer.rb +8 -4
  37. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  38. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  39. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  40. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  41. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  42. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  43. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  44. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  45. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  46. data/lib/active_storage/attached/changes.rb +7 -1
  47. data/lib/active_storage/attached/many.rb +27 -15
  48. data/lib/active_storage/attached/model.rb +31 -5
  49. data/lib/active_storage/attached/one.rb +32 -27
  50. data/lib/active_storage/downloader.rb +2 -2
  51. data/lib/active_storage/engine.rb +28 -16
  52. data/lib/active_storage/fixture_set.rb +76 -0
  53. data/lib/active_storage/gem_version.rb +4 -4
  54. data/lib/active_storage/previewer/video_previewer.rb +0 -2
  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 +1 -1
  58. data/lib/active_storage/service/configurator.rb +1 -1
  59. data/lib/active_storage/service/disk_service.rb +13 -18
  60. data/lib/active_storage/service/gcs_service.rb +91 -7
  61. data/lib/active_storage/service/mirror_service.rb +1 -1
  62. data/lib/active_storage/service/registry.rb +1 -1
  63. data/lib/active_storage/service/s3_service.rb +4 -4
  64. data/lib/active_storage/service.rb +3 -3
  65. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -66
  66. data/lib/active_storage/transformers/transformer.rb +1 -1
  67. data/lib/active_storage.rb +3 -292
  68. metadata +32 -25
  69. data/app/controllers/concerns/active_storage/disable_session.rb +0 -12
  70. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -2,18 +2,9 @@
2
2
 
3
3
  # The base class for all Active Storage controllers.
4
4
  class ActiveStorage::BaseController < ActionController::Base
5
- include ActiveStorage::SetCurrent
5
+ include ActiveStorage::SetCurrent, ActiveStorage::Streaming
6
6
 
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,15 +1,24 @@
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://edgeguides.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
7
- include ActiveStorage::DisableSession
8
11
 
9
12
  def show
10
- http_cache_forever public: true do
11
- set_content_headers_from @blob
12
- stream @blob
13
+ if request.headers["Range"].present?
14
+ send_blob_byte_range_data @blob, request.headers["Range"]
15
+ else
16
+ http_cache_forever public: true do
17
+ response.headers["Accept-Ranges"] = "bytes"
18
+ response.headers["Content-Length"] = @blob.byte_size.to_s
19
+
20
+ send_blob_stream @blob
21
+ end
13
22
  end
14
23
  end
15
24
  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://edgeguides.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
@@ -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,14 +1,15 @@
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://edgeguides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
4
9
  class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
5
- include ActiveStorage::SetHeaders
6
- include ActiveStorage::DisableSession
7
-
8
10
  def show
9
11
  http_cache_forever public: true do
10
- set_content_headers_from @representation.image
11
- stream @representation
12
+ send_blob_stream @representation.image
12
13
  end
13
14
  end
14
15
  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://edgeguides.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
@@ -86,21 +86,13 @@ class ActiveStorage::Blob < ActiveStorage::Record
86
86
  super(id, purpose: purpose)
87
87
  end
88
88
 
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:
89
+ def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
98
90
  new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
99
91
  blob.unfurl(io, identify: identify)
100
92
  end
101
93
  end
102
94
 
103
- def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
95
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
104
96
  build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
105
97
  end
106
98
 
@@ -115,9 +107,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
115
107
  end
116
108
  end
117
109
 
118
- alias_method :create_after_upload!, :create_and_upload!
119
- deprecate create_after_upload!: :create_and_upload!
120
-
121
110
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
122
111
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
123
112
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
@@ -137,7 +126,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
137
126
  end
138
127
 
139
128
  # Customize signed ID purposes for backwards compatibility.
140
- def combine_signed_id_purposes(purpose) #:nodoc:
129
+ def combine_signed_id_purposes(purpose) # :nodoc:
141
130
  purpose.to_s
142
131
  end
143
132
 
@@ -145,14 +134,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
145
134
  #
146
135
  # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
147
136
  # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
148
- def signed_id_verifier #:nodoc:
137
+ def signed_id_verifier # :nodoc:
149
138
  @signed_id_verifier ||= ActiveStorage.verifier
150
139
  end
140
+
141
+ def scope_for_strict_loading # :nodoc:
142
+ if strict_loading_by_default? && ActiveStorage.track_variants
143
+ includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
144
+ else
145
+ all
146
+ end
147
+ end
151
148
  end
152
149
 
153
150
  # 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)
151
+ def signed_id(purpose: :blob_id, expires_in: nil)
152
+ super
156
153
  end
157
154
 
158
155
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
@@ -200,9 +197,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
200
197
  content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
201
198
  end
202
199
 
203
- alias_method :service_url, :url
204
- deprecate service_url: :url
205
-
206
200
  # 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
201
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
208
202
  def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
@@ -214,11 +208,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
214
208
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
215
209
  end
216
210
 
217
- def content_type_for_serving #:nodoc:
211
+ def content_type_for_serving # :nodoc:
218
212
  forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
219
213
  end
220
214
 
221
- def forced_disposition_for_serving #:nodoc:
215
+ def forced_disposition_for_serving # :nodoc:
222
216
  if forcibly_serve_as_binary? || !allowed_inline?
223
217
  :attachment
224
218
  end
@@ -242,14 +236,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
242
236
  upload_without_unfurling io
243
237
  end
244
238
 
245
- def unfurl(io, identify: true) #:nodoc:
239
+ def unfurl(io, identify: true) # :nodoc:
246
240
  self.checksum = compute_checksum_in_chunks(io)
247
241
  self.content_type = extract_content_type(io) if content_type.nil? || identify
248
242
  self.byte_size = io.size
249
243
  self.identified = true
250
244
  end
251
245
 
252
- def upload_without_unfurling(io) #:nodoc:
246
+ def upload_without_unfurling(io) # :nodoc:
253
247
  service.upload key, io, checksum: checksum, **service_metadata
254
248
  end
255
249
 
@@ -259,6 +253,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
259
253
  service.download key, &block
260
254
  end
261
255
 
256
+ # Downloads a part of the file associated with this blob.
257
+ def download_chunk(range)
258
+ service.download_chunk key, range
259
+ end
260
+
262
261
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
263
262
  #
264
263
  # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
@@ -277,7 +276,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
277
276
  name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
278
277
  end
279
278
 
280
- def mirror_later #:nodoc:
279
+ def mirror_later # :nodoc:
281
280
  ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
282
281
  end
283
282
 
@@ -294,7 +293,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
294
293
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
295
294
  def purge
296
295
  destroy
297
- delete
296
+ delete if previously_persisted?
298
297
  rescue ActiveRecord::InvalidForeignKey
299
298
  end
300
299
 
@@ -311,7 +310,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
311
310
 
312
311
  private
313
312
  def compute_checksum_in_chunks(io)
314
- Digest::MD5.new.tap do |checksum|
313
+ OpenSSL::Digest::MD5.new.tap do |checksum|
315
314
  while chunk = io.read(5.megabytes)
316
315
  checksum << chunk
317
316
  end
@@ -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
@@ -66,9 +66,6 @@ class ActiveStorage::Preview
66
66
  end
67
67
  end
68
68
 
69
- alias_method :service_url, :url
70
- deprecate service_url: :url
71
-
72
69
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
70
  def key
74
71
  if processed?
@@ -78,6 +75,11 @@ class ActiveStorage::Preview
78
75
  end
79
76
  end
80
77
 
78
+ # Downloads the file associated with this preview's variant. If no block is
79
+ # given, the entire file is read into memory and returned. That'll use a lot
80
+ # of RAM for very large files. If a block is given, then the download is
81
+ # streamed and yielded in chunks. Raises ActiveStorage::Preview::UnprocessedError
82
+ # if the preview has not been processed yet.
81
83
  def download(&block)
82
84
  if processed?
83
85
  variant.download(&block)
@@ -93,7 +95,7 @@ class ActiveStorage::Preview
93
95
 
94
96
  def process
95
97
  previewer.preview(service_name: blob.service_name) do |attachable|
96
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
98
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
97
99
  image.attach(attachable)
98
100
  end
99
101
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
3
+ class ActiveStorage::Record < ActiveRecord::Base # :nodoc:
4
4
  self.abstract_class = true
5
5
  end
6
6
 
@@ -4,7 +4,7 @@
4
4
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
5
5
  # original.
6
6
  #
7
- # Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
7
+ # Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
8
8
  # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
9
9
  # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
10
10
  # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
@@ -42,13 +42,13 @@
42
42
  # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
43
43
  # ImageProcessing gem (such as +resize_to_limit+):
44
44
  #
45
- # avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
45
+ # avatar.variant(resize_to_limit: [800, 800], colourspace: "b-w", rotate: "-90")
46
46
  #
47
47
  # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
48
48
  #
49
- # * {ImageProcessing::MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md#methods]
49
+ # * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
50
50
  # * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
51
- # * {ImageProcessing::Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md#methods]
51
+ # * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
52
52
  # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
53
53
  class ActiveStorage::Variant
54
54
  attr_reader :blob, :variation
@@ -67,7 +67,7 @@ class ActiveStorage::Variant
67
67
 
68
68
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
69
69
  def key
70
- "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
70
+ "variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
71
71
  end
72
72
 
73
73
  # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
@@ -79,9 +79,6 @@ class ActiveStorage::Variant
79
79
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
80
80
  end
81
81
 
82
- alias_method :service_url, :url
83
- deprecate service_url: :url
84
-
85
82
  # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
86
83
  # 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.
87
84
  def download(&block)
@@ -94,7 +91,7 @@ class ActiveStorage::Variant
94
91
 
95
92
  alias_method :content_type_for_serving, :content_type
96
93
 
97
- def forced_disposition_for_serving #:nodoc:
94
+ def forced_disposition_for_serving # :nodoc:
98
95
  nil
99
96
  end
100
97
 
@@ -6,3 +6,5 @@ class ActiveStorage::VariantRecord < ActiveStorage::Record
6
6
  belongs_to :blob
7
7
  has_one_attached :image
8
8
  end
9
+
10
+ ActiveSupport.run_load_hooks :active_storage_variant_record, ActiveStorage::VariantRecord
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an
4
+ # ActiveStorage::VariantRecord. This is only used if `ActiveStorage.track_variants` is enabled.
3
5
  class ActiveStorage::VariantWithRecord
4
6
  attr_reader :blob, :variation
7
+ delegate :service, to: :blob
5
8
 
6
9
  def initialize(blob, variation)
7
10
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
@@ -26,9 +29,6 @@ class ActiveStorage::VariantWithRecord
26
29
 
27
30
  delegate :key, :url, :download, to: :image, allow_nil: true
28
31
 
29
- alias_method :service_url, :url
30
- deprecate service_url: :url
31
-
32
32
  private
33
33
  def transform_blob
34
34
  blob.open do |input|
@@ -41,7 +41,7 @@ class ActiveStorage::VariantWithRecord
41
41
 
42
42
  def create_or_find_record(image:)
43
43
  @record =
44
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
44
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
45
45
  blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
46
46
  record.image.attach(image)
47
47
  end
@@ -49,6 +49,10 @@ class ActiveStorage::VariantWithRecord
49
49
  end
50
50
 
51
51
  def record
52
- @record ||= blob.variant_records.find_by(variation_digest: variation.digest)
52
+ @record ||= if blob.variant_records.loaded?
53
+ blob.variant_records.find { |v| v.variation_digest == variation.digest }
54
+ else
55
+ blob.variant_records.find_by(variation_digest: variation.digest)
56
+ end
53
57
  end
54
58
  end