activestorage 5.2.7.1 → 6.1.4.6

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +225 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +43 -8
  5. data/app/assets/javascripts/activestorage.js +5 -2
  6. data/app/controllers/active_storage/base_controller.rb +13 -4
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  8. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
  10. data/app/controllers/active_storage/disk_controller.rb +13 -22
  11. data/app/controllers/active_storage/representations/base_controller.rb +14 -0
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
  13. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -5
  14. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  15. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  16. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  17. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  18. data/app/javascript/activestorage/blob_record.js +7 -2
  19. data/app/jobs/active_storage/analyze_job.rb +5 -0
  20. data/app/jobs/active_storage/base_job.rb +0 -1
  21. data/app/jobs/active_storage/mirror_job.rb +15 -0
  22. data/app/jobs/active_storage/purge_job.rb +3 -0
  23. data/app/models/active_storage/attachment.rb +35 -16
  24. data/app/models/active_storage/blob/analyzable.rb +6 -2
  25. data/app/models/active_storage/blob/identifiable.rb +7 -6
  26. data/app/models/active_storage/blob/representable.rb +36 -6
  27. data/app/models/active_storage/blob.rb +186 -68
  28. data/app/models/active_storage/filename.rb +0 -6
  29. data/app/models/active_storage/preview.rb +37 -12
  30. data/app/models/active_storage/record.rb +7 -0
  31. data/app/models/active_storage/variant.rb +53 -67
  32. data/app/models/active_storage/variant_record.rb +8 -0
  33. data/app/models/active_storage/variant_with_record.rb +54 -0
  34. data/app/models/active_storage/variation.rb +30 -94
  35. data/config/routes.rb +66 -15
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  37. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  38. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  39. data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
  40. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  41. data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
  42. data/lib/active_storage/analyzer.rb +15 -4
  43. data/lib/active_storage/attached/changes/create_many.rb +47 -0
  44. data/lib/active_storage/attached/changes/create_one.rb +82 -0
  45. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  46. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  47. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  48. data/lib/active_storage/attached/changes.rb +16 -0
  49. data/lib/active_storage/attached/many.rb +19 -12
  50. data/lib/active_storage/attached/model.rb +212 -0
  51. data/lib/active_storage/attached/one.rb +19 -21
  52. data/lib/active_storage/attached.rb +7 -22
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/engine.rb +60 -38
  55. data/lib/active_storage/errors.rb +25 -3
  56. data/lib/active_storage/gem_version.rb +4 -4
  57. data/lib/active_storage/log_subscriber.rb +6 -0
  58. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  59. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
  60. data/lib/active_storage/previewer/video_previewer.rb +17 -10
  61. data/lib/active_storage/previewer.rb +34 -14
  62. data/lib/active_storage/reflection.rb +64 -0
  63. data/lib/active_storage/service/azure_storage_service.rb +65 -44
  64. data/lib/active_storage/service/configurator.rb +6 -2
  65. data/lib/active_storage/service/disk_service.rb +57 -44
  66. data/lib/active_storage/service/gcs_service.rb +68 -64
  67. data/lib/active_storage/service/mirror_service.rb +31 -7
  68. data/lib/active_storage/service/registry.rb +32 -0
  69. data/lib/active_storage/service/s3_service.rb +56 -24
  70. data/lib/active_storage/service.rb +44 -12
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
  72. data/lib/active_storage/transformers/transformer.rb +39 -0
  73. data/lib/active_storage.rb +31 -296
  74. data/lib/tasks/activestorage.rake +11 -0
  75. metadata +82 -16
  76. data/app/models/active_storage/filename/parameters.rb +0 -36
  77. data/lib/active_storage/attached/macros.rb +0 -110
  78. data/lib/active_storage/downloading.rb +0 -39
@@ -3,8 +3,9 @@
3
3
  # A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
4
4
  # Blobs can be created in two ways:
5
5
  #
6
- # 1. Subsequent to the file being uploaded server-side to the service via <tt>create_after_upload!</tt>.
7
- # 2. Ahead of the file being directly uploaded client-side to the service via <tt>create_before_direct_upload!</tt>.
6
+ # 1. Ahead of the file being uploaded server-side to the service, via <tt>create_and_upload!</tt>. A rewindable
7
+ # <tt>io</tt> with the file contents must be available at the server for this operation.
8
+ # 2. Ahead of the file being directly uploaded client-side to the service, via <tt>create_before_direct_upload!</tt>.
8
9
  #
9
10
  # The first option doesn't require any client-side JavaScript integration, and can be used by any other back-end
10
11
  # service that deals with files. The second option is faster, since you're not using your own server as a staging
@@ -13,80 +14,154 @@
13
14
  # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
14
15
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
15
16
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
16
- class ActiveStorage::Blob < ActiveRecord::Base
17
- require_dependency "active_storage/blob/analyzable"
18
- require_dependency "active_storage/blob/identifiable"
19
- require_dependency "active_storage/blob/representable"
20
-
21
- include Analyzable
22
- include Identifiable
23
- 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
24
36
 
25
37
  self.table_name = "active_storage_blobs"
26
38
 
27
- has_secure_token :key
39
+ MINIMUM_TOKEN_LENGTH = 28
40
+
41
+ has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
28
42
  store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
29
43
 
30
- class_attribute :service
44
+ class_attribute :services, default: {}
45
+ class_attribute :service, instance_accessor: false
31
46
 
32
47
  has_many :attachments
33
48
 
34
- 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?
35
56
 
36
57
  before_destroy(prepend: true) do
37
58
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
38
59
  end
39
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
+
40
71
  class << self
41
- # You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
72
+ # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
42
73
  # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
43
74
  # that was created ahead of the upload itself on form submission.
44
75
  #
45
76
  # The signed ID is also used to create stable URLs for the blob through the BlobsController.
46
- def find_signed(id)
47
- find ActiveStorage.verifier.verify(id, purpose: :blob_id)
77
+ def find_signed(id, record: nil, purpose: :blob_id)
78
+ super(id, purpose: purpose)
48
79
  end
49
80
 
50
- # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
51
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil)
52
- new.tap do |blob|
53
- blob.filename = filename
54
- blob.content_type = content_type
55
- blob.metadata = metadata
81
+ # Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
82
+ # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
83
+ # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
84
+ # the valid signed id can't find a record.
85
+ def find_signed!(id, record: nil, purpose: :blob_id)
86
+ super(id, purpose: purpose)
87
+ end
56
88
 
57
- blob.upload io
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)
58
92
  end
59
93
  end
60
94
 
61
- # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
62
- # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
63
- # time), while having an open database transaction.
64
- def create_after_upload!(io:, filename:, content_type: nil, metadata: nil)
65
- build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata).tap(&:save!)
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:
98
+ new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
99
+ blob.unfurl(io, identify: identify)
100
+ end
101
+ end
102
+
103
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
104
+ build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
105
+ end
106
+
107
+ # Creates a new blob instance and then uploads the contents of
108
+ # the given <tt>io</tt> to the service. The blob instance is going to
109
+ # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
110
+ # When providing a content type, pass <tt>identify: false</tt> to bypass
111
+ # automatic content type inference.
112
+ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
113
+ create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
114
+ blob.upload_without_unfurling(io)
115
+ end
66
116
  end
67
117
 
118
+ alias_method :create_after_upload!, :create_and_upload!
119
+ deprecate create_after_upload!: :create_and_upload!
120
+
68
121
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
69
122
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
70
123
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
71
124
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
72
125
  # the signed ID.
73
- def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
74
- create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
126
+ def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
127
+ create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
128
+ end
129
+
130
+ # To prevent problems with case-insensitive filesystems, especially in combination
131
+ # with databases which treat indices as case-sensitive, all blob keys generated are going
132
+ # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
133
+ # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
134
+ # the number of bytes used is increased to 28 from the standard 24
135
+ def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
136
+ SecureRandom.base36(length)
137
+ end
138
+
139
+ # Customize signed ID purposes for backwards compatibility.
140
+ def combine_signed_id_purposes(purpose) #:nodoc:
141
+ purpose.to_s
142
+ end
143
+
144
+ # Customize the default signed ID verifier for backwards compatibility.
145
+ #
146
+ # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
147
+ # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
148
+ def signed_id_verifier #:nodoc:
149
+ @signed_id_verifier ||= ActiveStorage.verifier
75
150
  end
76
151
  end
77
152
 
78
153
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
79
- # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
80
154
  def signed_id
81
- ActiveStorage.verifier.generate(id, purpose: :blob_id)
155
+ super(purpose: :blob_id)
82
156
  end
83
157
 
84
- # Returns the key pointing to the file on the service that's associated with this blob. The key is in the
85
- # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended
86
- # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key.
158
+ # Returns the key pointing to the file on the service that's associated with this blob. The key is the
159
+ # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
160
+ # This key is not intended to be revealed directly to the user.
161
+ # Always refer to blobs using the signed_id or a verified form of the key.
87
162
  def key
88
163
  # We can't wait until the record is first saved to have a key for it
89
- self[:key] ||= self.class.generate_unique_secure_token
164
+ self[:key] ||= self.class.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
90
165
  end
91
166
 
92
167
  # Returns an ActiveStorage::Filename instance of the filename that can be
@@ -116,21 +191,21 @@ class ActiveStorage::Blob < ActiveRecord::Base
116
191
  content_type.start_with?("text")
117
192
  end
118
193
 
119
-
120
- # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
121
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
122
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
123
- # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
124
- def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
125
- filename = ActiveStorage::Filename.wrap(filename || self.filename)
126
-
127
- service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
128
- disposition: forced_disposition_for_service_url || disposition, **options
194
+ # Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a
195
+ # short-lived URL for private files. Private files are signed, and not for public use. Instead,
196
+ # the URL should only be exposed as a redirect from a stable, possibly authenticated URL. Hiding the
197
+ # URL behind a redirect also allows you to change services without updating all URLs.
198
+ def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
199
+ service.url key, expires_in: expires_in, filename: ActiveStorage::Filename.wrap(filename || self.filename),
200
+ content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
129
201
  end
130
202
 
203
+ alias_method :service_url, :url
204
+ deprecate service_url: :url
205
+
131
206
  # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
132
207
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
133
- def service_url_for_direct_upload(expires_in: service.url_expires_in)
208
+ def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
134
209
  service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
135
210
  end
136
211
 
@@ -139,6 +214,16 @@ class ActiveStorage::Blob < ActiveRecord::Base
139
214
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
140
215
  end
141
216
 
217
+ def content_type_for_serving #:nodoc:
218
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
219
+ end
220
+
221
+ def forced_disposition_for_serving #:nodoc:
222
+ if forcibly_serve_as_binary? || !allowed_inline?
223
+ :attachment
224
+ end
225
+ end
226
+
142
227
 
143
228
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
144
229
  # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
@@ -146,16 +231,25 @@ class ActiveStorage::Blob < ActiveRecord::Base
146
231
  #
147
232
  # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the
148
233
  # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+
149
- # and store that in +byte_size+ on the blob record.
234
+ # and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless
235
+ # you specify a +content_type+ and pass +identify+ as false.
150
236
  #
151
- # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
152
- # and +create_after_upload!+.
153
- def upload(io)
237
+ # Normally, you do not have to call this method directly at all. Use the +create_and_upload!+ class method instead.
238
+ # If you do use this method directly, make sure you are using it on a persisted Blob as otherwise another blob's
239
+ # data might get overwritten on the service.
240
+ def upload(io, identify: true)
241
+ unfurl io, identify: identify
242
+ upload_without_unfurling io
243
+ end
244
+
245
+ def unfurl(io, identify: true) #:nodoc:
154
246
  self.checksum = compute_checksum_in_chunks(io)
155
- self.content_type = extract_content_type(io)
247
+ self.content_type = extract_content_type(io) if content_type.nil? || identify
156
248
  self.byte_size = io.size
157
249
  self.identified = true
250
+ end
158
251
 
252
+ def upload_without_unfurling(io) #:nodoc:
159
253
  service.upload key, io, checksum: checksum, **service_metadata
160
254
  end
161
255
 
@@ -165,30 +259,56 @@ class ActiveStorage::Blob < ActiveRecord::Base
165
259
  service.download key, &block
166
260
  end
167
261
 
262
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
263
+ #
264
+ # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
265
+ #
266
+ # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tmpdir:+ to create it in a different directory:
267
+ #
268
+ # blob.open(tmpdir: "/path/to/tmp") do |file|
269
+ # # ...
270
+ # end
271
+ #
272
+ # The tempfile is automatically closed and unlinked after the given block is executed.
273
+ #
274
+ # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
275
+ def open(tmpdir: nil, &block)
276
+ service.open key, checksum: checksum,
277
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
278
+ end
279
+
280
+ def mirror_later #:nodoc:
281
+ ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
282
+ end
168
283
 
169
- # Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be
170
- # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+
284
+ # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
285
+ # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
171
286
  # methods in most circumstances.
172
287
  def delete
173
288
  service.delete(key)
174
289
  service.delete_prefixed("variants/#{key}/") if image?
175
290
  end
176
291
 
177
- # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
178
- # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
179
- # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use +#purge_later+ instead.
292
+ # Destroys the blob record and then deletes the file on the service. This is the recommended way to dispose of unwanted
293
+ # blobs. Note, though, that deleting the file off the service will initiate an HTTP connection to the service, which may
294
+ # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
180
295
  def purge
181
296
  destroy
182
297
  delete
183
298
  rescue ActiveRecord::InvalidForeignKey
184
299
  end
185
300
 
186
- # Enqueues an ActiveStorage::PurgeJob job that'll call +purge+. This is the recommended way to purge blobs when the call
187
- # needs to be made from a transaction, a callback, or any other real-time scenario.
301
+ # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
302
+ # an Active Record callback, or in any other real-time scenario.
188
303
  def purge_later
189
304
  ActiveStorage::PurgeJob.perform_later(self)
190
305
  end
191
306
 
307
+ # Returns an instance of service, which can be configured globally or per attachment
308
+ def service
309
+ services.fetch(service_name)
310
+ end
311
+
192
312
  private
193
313
  def compute_checksum_in_chunks(io)
194
314
  Digest::MD5.new.tap do |checksum|
@@ -212,14 +332,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
212
332
  ActiveStorage.content_types_allowed_inline.include?(content_type)
213
333
  end
214
334
 
215
- def content_type_for_service_url
216
- forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
217
- end
218
-
219
- def forced_disposition_for_service_url
220
- if forcibly_serve_as_binary? || !allowed_inline?
221
- :attachment
222
- end
335
+ def web_image?
336
+ ActiveStorage.web_image_content_types.include?(content_type)
223
337
  end
224
338
 
225
339
  def service_metadata
@@ -232,5 +346,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
232
346
  end
233
347
  end
234
348
 
235
- ActiveSupport.run_load_hooks(:active_storage_blob, self)
349
+ def update_service_metadata
350
+ service.update_metadata key, **service_metadata if service_metadata.any?
351
+ end
236
352
  end
353
+
354
+ ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob
@@ -3,8 +3,6 @@
3
3
  # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
4
4
  # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
5
5
  class ActiveStorage::Filename
6
- require_dependency "active_storage/filename/parameters"
7
-
8
6
  include Comparable
9
7
 
10
8
  class << self
@@ -60,10 +58,6 @@ class ActiveStorage::Filename
60
58
  @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
61
59
  end
62
60
 
63
- def parameters #:nodoc:
64
- Parameters.new self
65
- end
66
-
67
61
  # Returns the sanitized version of the filename.
68
62
  def to_s
69
63
  sanitized.to_s
@@ -3,8 +3,9 @@
3
3
  # Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
4
4
  # extracting its first frame, and a PDF blob can be previewed by extracting its first page.
5
5
  #
6
- # A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
7
- # ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
6
+ # A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs.
7
+ # ActiveStorage::Previewer::VideoPreviewer is used for videos whereas ActiveStorage::Previewer::PopplerPDFPreviewer
8
+ # and ActiveStorage::Previewer::MuPDFPreviewer are used for PDFs. Build custom previewers by
8
9
  # subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
9
10
  # documentation for more details on what's required of previewers.
10
11
  #
@@ -13,17 +14,17 @@
13
14
  # by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
14
15
  #
15
16
  # Rails.application.config.active_storage.previewers
16
- # # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
17
+ # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
17
18
  #
18
19
  # # Add a custom previewer for Microsoft Office documents:
19
20
  # Rails.application.config.active_storage.previewers << DOCXPreviewer
20
- # # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
21
+ # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
21
22
  #
22
23
  # Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
23
24
  #
24
25
  # The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
25
- # {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
26
- # and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf.
26
+ # {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
27
+ # and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
27
28
  #
28
29
  # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
29
30
  # install and use third-party software, make sure you understand the licensing implications of doing so.
@@ -38,7 +39,7 @@ class ActiveStorage::Preview
38
39
 
39
40
  # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
40
41
  #
41
- # blob.preview(resize: "100x100").processed.service_url
42
+ # blob.preview(resize_to_limit: [100, 100]).processed.url
42
43
  #
43
44
  # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
44
45
  # image is stored with the blob, it is only generated once.
@@ -56,10 +57,30 @@ class ActiveStorage::Preview
56
57
  # preview has not been processed yet.
57
58
  #
58
59
  # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
59
- # a stable URL that redirects to the short-lived URL returned by this method.
60
- def service_url(**options)
60
+ # a stable URL that redirects to the URL returned by this method.
61
+ def url(**options)
61
62
  if processed?
62
- variant.service_url(options)
63
+ variant.url(**options)
64
+ else
65
+ raise UnprocessedError
66
+ end
67
+ end
68
+
69
+ alias_method :service_url, :url
70
+ deprecate service_url: :url
71
+
72
+ # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
+ def key
74
+ if processed?
75
+ variant.key
76
+ else
77
+ raise UnprocessedError
78
+ end
79
+ end
80
+
81
+ def download(&block)
82
+ if processed?
83
+ variant.download(&block)
63
84
  else
64
85
  raise UnprocessedError
65
86
  end
@@ -71,11 +92,15 @@ class ActiveStorage::Preview
71
92
  end
72
93
 
73
94
  def process
74
- previewer.preview { |attachable| image.attach(attachable) }
95
+ previewer.preview(service_name: blob.service_name) do |attachable|
96
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
97
+ image.attach(attachable)
98
+ end
99
+ end
75
100
  end
76
101
 
77
102
  def variant
78
- ActiveStorage::Variant.new(image, variation).processed
103
+ image.variant(variation).processed
79
104
  end
80
105
 
81
106
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
4
+ self.abstract_class = true
5
+ end
6
+
7
+ ActiveSupport.run_load_hooks :active_storage_record, ActiveStorage::Record