activestorage 6.0.4 → 6.1.0.rc1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -187
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +36 -4
  5. data/app/controllers/active_storage/base_controller.rb +11 -0
  6. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  7. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
  8. data/app/controllers/active_storage/disk_controller.rb +8 -20
  9. data/app/controllers/active_storage/representations/proxy_controller.rb +19 -0
  10. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -2
  11. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  12. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  13. data/app/controllers/concerns/active_storage/set_current.rb +2 -2
  14. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  15. data/app/jobs/active_storage/mirror_job.rb +15 -0
  16. data/app/models/active_storage/attachment.rb +18 -10
  17. data/app/models/active_storage/blob.rb +114 -57
  18. data/app/models/active_storage/blob/analyzable.rb +6 -2
  19. data/app/models/active_storage/blob/identifiable.rb +7 -6
  20. data/app/models/active_storage/blob/representable.rb +34 -4
  21. data/app/models/active_storage/preview.rb +31 -10
  22. data/app/models/active_storage/record.rb +7 -0
  23. data/app/models/active_storage/variant.rb +28 -41
  24. data/app/models/active_storage/variant_record.rb +8 -0
  25. data/app/models/active_storage/variant_with_record.rb +54 -0
  26. data/app/models/active_storage/variation.rb +25 -20
  27. data/config/routes.rb +58 -8
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  31. data/lib/active_storage.rb +5 -2
  32. data/lib/active_storage/analyzer.rb +6 -0
  33. data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
  34. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  35. data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
  36. data/lib/active_storage/attached/changes/create_many.rb +1 -0
  37. data/lib/active_storage/attached/changes/create_one.rb +17 -4
  38. data/lib/active_storage/attached/many.rb +4 -3
  39. data/lib/active_storage/attached/model.rb +49 -10
  40. data/lib/active_storage/attached/one.rb +4 -3
  41. data/lib/active_storage/engine.rb +25 -27
  42. data/lib/active_storage/gem_version.rb +3 -3
  43. data/lib/active_storage/log_subscriber.rb +6 -0
  44. data/lib/active_storage/previewer.rb +3 -2
  45. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  46. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  47. data/lib/active_storage/previewer/video_previewer.rb +2 -2
  48. data/lib/active_storage/service.rb +35 -7
  49. data/lib/active_storage/service/azure_storage_service.rb +40 -35
  50. data/lib/active_storage/service/configurator.rb +3 -1
  51. data/lib/active_storage/service/disk_service.rb +36 -31
  52. data/lib/active_storage/service/gcs_service.rb +18 -16
  53. data/lib/active_storage/service/mirror_service.rb +31 -7
  54. data/lib/active_storage/service/registry.rb +32 -0
  55. data/lib/active_storage/service/s3_service.rb +51 -23
  56. data/lib/active_storage/transformers/image_processing_transformer.rb +13 -7
  57. data/lib/active_storage/transformers/transformer.rb +0 -3
  58. metadata +57 -21
  59. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
  60. data/lib/active_storage/downloading.rb +0 -47
  61. data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
@@ -5,4 +5,15 @@ class ActiveStorage::BaseController < ActionController::Base
5
5
  include ActiveStorage::SetCurrent
6
6
 
7
7
  protect_from_forgery with: :exception
8
+
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
8
19
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Proxy files through application. This avoids having a redirect and makes files easier to cache.
4
+ class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
5
+ include ActiveStorage::SetBlob
6
+ include ActiveStorage::SetHeaders
7
+
8
+ def show
9
+ http_cache_forever public: true do
10
+ set_content_headers_from @blob
11
+ stream @blob
12
+ end
13
+ end
14
+ end
@@ -4,11 +4,11 @@
4
4
  # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
5
  # security-through-obscurity factor of the signed blob references, you'll need to implement your own
6
6
  # authenticated redirection controller.
7
- class ActiveStorage::BlobsController < ActiveStorage::BaseController
7
+ class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
8
8
  include ActiveStorage::SetBlob
9
9
 
10
10
  def show
11
11
  expires_in ActiveStorage.service_urls_expire_in
12
- redirect_to @blob.service_url(disposition: params[:disposition])
12
+ redirect_to @blob.url(disposition: params[:disposition])
13
13
  end
14
14
  end
@@ -5,11 +5,13 @@
5
5
  # Always go through the BlobsController, or your own authenticated controller, rather than directly
6
6
  # to the service URL.
7
7
  class ActiveStorage::DiskController < ActiveStorage::BaseController
8
+ include ActiveStorage::FileServer
9
+
8
10
  skip_forgery_protection
9
11
 
10
12
  def show
11
13
  if key = decode_verified_key
12
- serve_file disk_service.path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition]
14
+ serve_file named_disk_service(key[:service_name]).path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition]
13
15
  else
14
16
  head :not_found
15
17
  end
@@ -20,7 +22,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
20
22
  def update
21
23
  if token = decode_verified_token
22
24
  if acceptable_content?(token)
23
- disk_service.upload token[:key], request.body, checksum: token[:checksum]
25
+ named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
24
26
  else
25
27
  head :unprocessable_entity
26
28
  end
@@ -32,30 +34,16 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
32
34
  end
33
35
 
34
36
  private
35
- def disk_service
36
- ActiveStorage::Blob.service
37
+ def named_disk_service(name)
38
+ ActiveStorage::Blob.services.fetch(name) do
39
+ ActiveStorage::Blob.service
40
+ end
37
41
  end
38
42
 
39
-
40
43
  def decode_verified_key
41
44
  ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
42
45
  end
43
46
 
44
- def serve_file(path, content_type:, disposition:)
45
- Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
46
- self.status = status
47
- self.response_body = body
48
-
49
- headers.each do |name, value|
50
- response.headers[name] = value
51
- end
52
-
53
- response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
54
- response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
55
- end
56
- end
57
-
58
-
59
47
  def decode_verified_token
60
48
  ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
61
49
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Proxy files through application. This avoids having a redirect and makes files easier to cache.
4
+ class ActiveStorage::Representations::ProxyController < ActiveStorage::BaseController
5
+ include ActiveStorage::SetBlob
6
+ include ActiveStorage::SetHeaders
7
+
8
+ def show
9
+ http_cache_forever public: true do
10
+ set_content_headers_from representation.image
11
+ stream representation
12
+ end
13
+ end
14
+
15
+ private
16
+ def representation
17
+ @representation ||= @blob.representation(params[:variation_key]).processed
18
+ end
19
+ end
@@ -4,11 +4,11 @@
4
4
  # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
5
  # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
6
6
  # authenticated redirection controller.
7
- class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
7
+ class ActiveStorage::Representations::RedirectController < ActiveStorage::BaseController
8
8
  include ActiveStorage::SetBlob
9
9
 
10
10
  def show
11
11
  expires_in ActiveStorage.service_urls_expire_in
12
- redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
12
+ redirect_to @blob.representation(params[:variation_key]).processed.url(disposition: params[:disposition])
13
13
  end
14
14
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::FileServer # :nodoc:
4
+ private
5
+ def serve_file(path, content_type:, disposition:)
6
+ Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
7
+ self.status = status
8
+ self.response_body = body
9
+
10
+ headers.each do |name, value|
11
+ response.headers[name] = value
12
+ end
13
+
14
+ response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
15
+ response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
16
+ end
17
+ end
18
+ end
@@ -9,7 +9,7 @@ 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 = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
13
13
  rescue ActiveSupport::MessageVerifier::InvalidSignature
14
14
  head :not_found
15
15
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs.
4
- # Include this concern in custom controllers that call ActiveStorage::Blob#service_url,
5
- # ActiveStorage::Variant#service_url, or ActiveStorage::Preview#service_url so the disk service can
4
+ # Include this concern in custom controllers that call ActiveStorage::Blob#url,
5
+ # ActiveStorage::Variant#url, or ActiveStorage::Preview#url so the disk service can
6
6
  # generate URLs using the same host, protocol, and base path as the current request.
7
7
  module ActiveStorage::SetCurrent
8
8
  extend ActiveSupport::Concern
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::SetHeaders #:nodoc:
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+ def set_content_headers_from(blob)
8
+ response.headers["Content-Type"] = blob.content_type_for_serving
9
+ response.headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format \
10
+ disposition: blob.forced_disposition_for_serving || params[:disposition] || "inline", filename: blob.filename.sanitized
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/try"
4
+
5
+ # Provides asynchronous mirroring of directly-uploaded blobs.
6
+ class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
7
+ queue_as { ActiveStorage.queues[:mirror] }
8
+
9
+ discard_on ActiveStorage::FileNotFoundError
10
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
11
+
12
+ def perform(key, checksum:)
13
+ ActiveStorage::Blob.service.try(:mirror, key, checksum: checksum)
14
+ end
15
+ end
@@ -5,43 +5,51 @@ require "active_support/core_ext/module/delegation"
5
5
  # Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
6
6
  # but it is possible to associate many different records with the same blob. A foreign-key constraint
7
7
  # on the attachments table prevents blobs from being purged if they’re still attached to any records.
8
- class ActiveStorage::Attachment < ActiveRecord::Base
8
+ #
9
+ # Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob].
10
+ class ActiveStorage::Attachment < ActiveStorage::Record
9
11
  self.table_name = "active_storage_attachments"
10
12
 
11
13
  belongs_to :record, polymorphic: true, touch: true
12
- belongs_to :blob, class_name: "ActiveStorage::Blob"
14
+ belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
13
15
 
14
16
  delegate_missing_to :blob
17
+ delegate :signed_id, to: :blob
15
18
 
16
- after_create_commit :analyze_blob_later, :identify_blob
19
+ after_create_commit :mirror_blob_later, :analyze_blob_later
17
20
  after_destroy_commit :purge_dependent_blob_later
18
21
 
19
22
  # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
20
23
  def purge
21
- delete
24
+ transaction do
25
+ delete
26
+ record&.touch
27
+ end
22
28
  blob&.purge
23
29
  end
24
30
 
25
31
  # Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob.
26
32
  def purge_later
27
- delete
33
+ transaction do
34
+ delete
35
+ record&.touch
36
+ end
28
37
  blob&.purge_later
29
38
  end
30
39
 
31
40
  private
32
- def identify_blob
33
- blob.identify
34
- end
35
-
36
41
  def analyze_blob_later
37
42
  blob.analyze_later unless blob.analyzed?
38
43
  end
39
44
 
45
+ def mirror_blob_later
46
+ blob.mirror_later
47
+ end
48
+
40
49
  def purge_dependent_blob_later
41
50
  blob&.purge_later if dependent == :purge_later
42
51
  end
43
52
 
44
-
45
53
  def dependent
46
54
  record.attachment_reflections[name]&.options[:dependent]
47
55
  end
@@ -14,80 +14,109 @@
14
14
  # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
15
15
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
16
16
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
17
- class ActiveStorage::Blob < ActiveRecord::Base
18
- unless Rails.autoloaders.zeitwerk_enabled?
19
- require_dependency "active_storage/blob/analyzable"
20
- require_dependency "active_storage/blob/identifiable"
21
- require_dependency "active_storage/blob/representable"
22
- end
23
-
24
- include Analyzable
25
- include Identifiable
26
- include Representable
17
+ 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
27
36
 
28
37
  self.table_name = "active_storage_blobs"
29
38
 
30
- has_secure_token :key
39
+ MINIMUM_TOKEN_LENGTH = 28
40
+
41
+ has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
31
42
  store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
32
43
 
33
- class_attribute :service
44
+ class_attribute :services, default: {}
45
+ class_attribute :service, instance_accessor: false
34
46
 
35
47
  has_many :attachments
36
48
 
37
- scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
49
+ scope :unattached, -> { where.missing(:attachments) }
50
+
51
+ after_initialize do
52
+ self.service_name ||= self.class.service.name
53
+ end
54
+
55
+ after_update_commit :update_service_metadata, if: :content_type_previously_changed?
38
56
 
39
57
  before_destroy(prepend: true) do
40
58
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
41
59
  end
42
60
 
61
+ validates :service_name, presence: true
62
+
63
+ validate do
64
+ if service_name_changed? && service_name.present?
65
+ services.fetch(service_name) do
66
+ errors.add(:service_name, :invalid)
67
+ end
68
+ end
69
+ end
70
+
43
71
  class << self
44
72
  # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
45
73
  # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
46
74
  # that was created ahead of the upload itself on form submission.
47
75
  #
48
76
  # The signed ID is also used to create stable URLs for the blob through the BlobsController.
49
- def find_signed(id)
50
- find ActiveStorage.verifier.verify(id, purpose: :blob_id)
77
+ def find_signed!(id, record: nil)
78
+ super(id, purpose: :blob_id)
51
79
  end
52
80
 
53
- # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
54
- # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
55
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
56
- new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
81
+ def build_after_upload(io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
82
+ new(filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
57
83
  blob.upload(io, identify: identify)
58
84
  end
59
85
  end
60
86
 
61
- def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
62
- new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
87
+ deprecate :build_after_upload
88
+
89
+ def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
90
+ new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
63
91
  blob.unfurl(io, identify: identify)
64
92
  end
65
93
  end
66
94
 
67
- def create_after_unfurling!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc:
68
- build_after_unfurling(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!)
95
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
96
+ build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
69
97
  end
70
98
 
71
- # Creates a new blob instance and then uploads the contents of the given <tt>io</tt> to the
72
- # service. The blob instance is saved before the upload begins to avoid clobbering another due
73
- # to key collisions.
74
- #
75
- # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
76
- def create_and_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil)
77
- create_after_unfurling!(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap do |blob|
99
+ # Creates a new blob instance and then uploads the contents of
100
+ # the given <tt>io</tt> to the service. The blob instance is going to
101
+ # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
102
+ # When providing a content type, pass <tt>identify: false</tt> to bypass
103
+ # automatic content type inference.
104
+ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
105
+ create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
78
106
  blob.upload_without_unfurling(io)
79
107
  end
80
108
  end
81
109
 
82
110
  alias_method :create_after_upload!, :create_and_upload!
111
+ deprecate create_after_upload!: :create_and_upload!
83
112
 
84
113
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
85
114
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
86
115
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
87
116
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
88
117
  # the signed ID.
89
- def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
90
- create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
118
+ def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
119
+ create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
91
120
  end
92
121
 
93
122
  # To prevent problems with case-insensitive filesystems, especially in combination
@@ -95,15 +124,27 @@ class ActiveStorage::Blob < ActiveRecord::Base
95
124
  # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
96
125
  # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
97
126
  # the number of bytes used is increased to 28 from the standard 24
98
- def generate_unique_secure_token
99
- SecureRandom.base36(28)
127
+ def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
128
+ SecureRandom.base36(length)
129
+ end
130
+
131
+ # Customize signed ID purposes for backwards compatibility.
132
+ def combine_signed_id_purposes(purpose) #:nodoc:
133
+ purpose.to_s
134
+ end
135
+
136
+ # Customize the default signed ID verifier for backwards compatibility.
137
+ #
138
+ # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
139
+ # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
140
+ def signed_id_verifier #:nodoc:
141
+ @signed_id_verifier ||= ActiveStorage.verifier
100
142
  end
101
143
  end
102
144
 
103
145
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
104
- # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
105
146
  def signed_id
106
- ActiveStorage.verifier.generate(id, purpose: :blob_id)
147
+ super(purpose: :blob_id)
107
148
  end
108
149
 
109
150
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
@@ -112,7 +153,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
112
153
  # Always refer to blobs using the signed_id or a verified form of the key.
113
154
  def key
114
155
  # We can't wait until the record is first saved to have a key for it
115
- self[:key] ||= self.class.generate_unique_secure_token
156
+ self[:key] ||= self.class.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
116
157
  end
117
158
 
118
159
  # Returns an ActiveStorage::Filename instance of the filename that can be
@@ -142,18 +183,18 @@ class ActiveStorage::Blob < ActiveRecord::Base
142
183
  content_type.start_with?("text")
143
184
  end
144
185
 
145
-
146
- # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
147
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
148
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
149
- # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
150
- def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
151
- filename = ActiveStorage::Filename.wrap(filename || self.filename)
152
-
153
- service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
154
- disposition: forced_disposition_for_service_url || disposition, **options
186
+ # Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a
187
+ # short-lived URL for private files. Private files are signed, and not for public use. Instead,
188
+ # the URL should only be exposed as a redirect from a stable, possibly authenticated URL. Hiding the
189
+ # URL behind a redirect also allows you to change services without updating all URLs.
190
+ def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
191
+ service.url key, expires_in: expires_in, filename: ActiveStorage::Filename.wrap(filename || self.filename),
192
+ content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
155
193
  end
156
194
 
195
+ alias_method :service_url, :url
196
+ deprecate service_url: :url
197
+
157
198
  # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
158
199
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
159
200
  def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
@@ -165,6 +206,16 @@ class ActiveStorage::Blob < ActiveRecord::Base
165
206
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
166
207
  end
167
208
 
209
+ def content_type_for_serving #:nodoc:
210
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
211
+ end
212
+
213
+ def forced_disposition_for_serving #:nodoc:
214
+ if forcibly_serve_as_binary? || !allowed_inline?
215
+ :attachment
216
+ end
217
+ end
218
+
168
219
 
169
220
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
170
221
  # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
@@ -218,6 +269,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
218
269
  name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
219
270
  end
220
271
 
272
+ def mirror_later #:nodoc:
273
+ ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
274
+ end
221
275
 
222
276
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
223
277
  # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
@@ -227,8 +281,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
227
281
  service.delete_prefixed("variants/#{key}/") if image?
228
282
  end
229
283
 
230
- # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
231
- # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
284
+ # Destroys the blob record and then deletes the file on the service. This is the recommended way to dispose of unwanted
285
+ # blobs. Note, though, that deleting the file off the service will initiate an HTTP connection to the service, which may
232
286
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
233
287
  def purge
234
288
  destroy
@@ -242,6 +296,11 @@ class ActiveStorage::Blob < ActiveRecord::Base
242
296
  ActiveStorage::PurgeJob.perform_later(self)
243
297
  end
244
298
 
299
+ # Returns an instance of service, which can be configured globally or per attachment
300
+ def service
301
+ services.fetch(service_name)
302
+ end
303
+
245
304
  private
246
305
  def compute_checksum_in_chunks(io)
247
306
  Digest::MD5.new.tap do |checksum|
@@ -265,14 +324,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
265
324
  ActiveStorage.content_types_allowed_inline.include?(content_type)
266
325
  end
267
326
 
268
- def content_type_for_service_url
269
- forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
270
- end
271
-
272
- def forced_disposition_for_service_url
273
- if forcibly_serve_as_binary? || !allowed_inline?
274
- :attachment
275
- end
327
+ def web_image?
328
+ ActiveStorage.web_image_content_types.include?(content_type)
276
329
  end
277
330
 
278
331
  def service_metadata
@@ -284,6 +337,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
284
337
  { content_type: content_type }
285
338
  end
286
339
  end
340
+
341
+ def update_service_metadata
342
+ service.update_metadata key, **service_metadata if service_metadata.any?
343
+ end
287
344
  end
288
345
 
289
346
  ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob