activestorage 6.0.3.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 +144 -162
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +35 -3
  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 -59
  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 +59 -12
  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 +3 -3
  47. data/lib/active_storage/previewer/video_previewer.rb +2 -2
  48. data/lib/active_storage/service.rb +36 -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 +53 -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage/downloader"
4
-
5
3
  # A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
6
4
  # Blobs can be created in two ways:
7
5
  #
@@ -16,80 +14,109 @@ require "active_storage/downloader"
16
14
  # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
17
15
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
18
16
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
19
- class ActiveStorage::Blob < ActiveRecord::Base
20
- unless Rails.autoloaders.zeitwerk_enabled?
21
- require_dependency "active_storage/blob/analyzable"
22
- require_dependency "active_storage/blob/identifiable"
23
- require_dependency "active_storage/blob/representable"
24
- end
25
-
26
- include Analyzable
27
- include Identifiable
28
- 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
29
36
 
30
37
  self.table_name = "active_storage_blobs"
31
38
 
32
- has_secure_token :key
39
+ MINIMUM_TOKEN_LENGTH = 28
40
+
41
+ has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
33
42
  store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
34
43
 
35
- class_attribute :service
44
+ class_attribute :services, default: {}
45
+ class_attribute :service, instance_accessor: false
36
46
 
37
47
  has_many :attachments
38
48
 
39
- 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?
40
56
 
41
57
  before_destroy(prepend: true) do
42
58
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
43
59
  end
44
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
+
45
71
  class << self
46
72
  # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
47
73
  # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
48
74
  # that was created ahead of the upload itself on form submission.
49
75
  #
50
76
  # The signed ID is also used to create stable URLs for the blob through the BlobsController.
51
- def find_signed(id)
52
- find ActiveStorage.verifier.verify(id, purpose: :blob_id)
77
+ def find_signed!(id, record: nil)
78
+ super(id, purpose: :blob_id)
53
79
  end
54
80
 
55
- # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
56
- # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
57
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
58
- 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|
59
83
  blob.upload(io, identify: identify)
60
84
  end
61
85
  end
62
86
 
63
- def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
64
- 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|
65
91
  blob.unfurl(io, identify: identify)
66
92
  end
67
93
  end
68
94
 
69
- def create_after_unfurling!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc:
70
- 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!)
71
97
  end
72
98
 
73
- # Creates a new blob instance and then uploads the contents of the given <tt>io</tt> to the
74
- # service. The blob instance is saved before the upload begins to avoid clobbering another due
75
- # to key collisions.
76
- #
77
- # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
78
- def create_and_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil)
79
- 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|
80
106
  blob.upload_without_unfurling(io)
81
107
  end
82
108
  end
83
109
 
84
110
  alias_method :create_after_upload!, :create_and_upload!
111
+ deprecate create_after_upload!: :create_and_upload!
85
112
 
86
113
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
87
114
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
88
115
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
89
116
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
90
117
  # the signed ID.
91
- def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
92
- 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
93
120
  end
94
121
 
95
122
  # To prevent problems with case-insensitive filesystems, especially in combination
@@ -97,15 +124,27 @@ class ActiveStorage::Blob < ActiveRecord::Base
97
124
  # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
98
125
  # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
99
126
  # the number of bytes used is increased to 28 from the standard 24
100
- def generate_unique_secure_token
101
- 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
102
142
  end
103
143
  end
104
144
 
105
145
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
106
- # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
107
146
  def signed_id
108
- ActiveStorage.verifier.generate(id, purpose: :blob_id)
147
+ super(purpose: :blob_id)
109
148
  end
110
149
 
111
150
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
@@ -114,7 +153,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
114
153
  # Always refer to blobs using the signed_id or a verified form of the key.
115
154
  def key
116
155
  # We can't wait until the record is first saved to have a key for it
117
- self[:key] ||= self.class.generate_unique_secure_token
156
+ self[:key] ||= self.class.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
118
157
  end
119
158
 
120
159
  # Returns an ActiveStorage::Filename instance of the filename that can be
@@ -144,18 +183,18 @@ class ActiveStorage::Blob < ActiveRecord::Base
144
183
  content_type.start_with?("text")
145
184
  end
146
185
 
147
-
148
- # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
149
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
150
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
151
- # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
152
- def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
153
- filename = ActiveStorage::Filename.wrap(filename || self.filename)
154
-
155
- service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
156
- 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
157
193
  end
158
194
 
195
+ alias_method :service_url, :url
196
+ deprecate service_url: :url
197
+
159
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
160
199
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
161
200
  def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
@@ -167,6 +206,16 @@ class ActiveStorage::Blob < ActiveRecord::Base
167
206
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
168
207
  end
169
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
+
170
219
 
171
220
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
172
221
  # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
@@ -220,6 +269,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
220
269
  name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
221
270
  end
222
271
 
272
+ def mirror_later #:nodoc:
273
+ ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
274
+ end
223
275
 
224
276
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
225
277
  # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
@@ -229,8 +281,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
229
281
  service.delete_prefixed("variants/#{key}/") if image?
230
282
  end
231
283
 
232
- # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
233
- # 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
234
286
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
235
287
  def purge
236
288
  destroy
@@ -244,6 +296,11 @@ class ActiveStorage::Blob < ActiveRecord::Base
244
296
  ActiveStorage::PurgeJob.perform_later(self)
245
297
  end
246
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
+
247
304
  private
248
305
  def compute_checksum_in_chunks(io)
249
306
  Digest::MD5.new.tap do |checksum|
@@ -267,14 +324,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
267
324
  ActiveStorage.content_types_allowed_inline.include?(content_type)
268
325
  end
269
326
 
270
- def content_type_for_service_url
271
- forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
272
- end
273
-
274
- def forced_disposition_for_service_url
275
- if forcibly_serve_as_binary? || !allowed_inline?
276
- :attachment
277
- end
327
+ def web_image?
328
+ ActiveStorage.web_image_content_types.include?(content_type)
278
329
  end
279
330
 
280
331
  def service_metadata
@@ -286,6 +337,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
286
337
  { content_type: content_type }
287
338
  end
288
339
  end
340
+
341
+ def update_service_metadata
342
+ service.update_metadata key, **service_metadata if service_metadata.any?
343
+ end
289
344
  end
290
345
 
291
346
  ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob