activestorage 6.1.7 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -276
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +29 -15
  5. data/app/assets/javascripts/activestorage.esm.js +848 -0
  6. data/app/assets/javascripts/activestorage.js +263 -376
  7. data/app/controllers/active_storage/base_controller.rb +0 -9
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +16 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/disk_controller.rb +5 -2
  11. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +8 -3
  13. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  14. data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
  15. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  16. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  17. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  18. data/app/controllers/concerns/active_storage/streaming.rb +66 -0
  19. data/app/javascript/activestorage/blob_record.js +4 -1
  20. data/app/javascript/activestorage/direct_upload.js +3 -2
  21. data/app/javascript/activestorage/index.js +3 -1
  22. data/app/javascript/activestorage/ujs.js +1 -1
  23. data/app/jobs/active_storage/analyze_job.rb +1 -1
  24. data/app/jobs/active_storage/mirror_job.rb +1 -1
  25. data/app/jobs/active_storage/purge_job.rb +1 -1
  26. data/app/jobs/active_storage/transform_job.rb +12 -0
  27. data/app/models/active_storage/attachment.rb +111 -4
  28. data/app/models/active_storage/blob/analyzable.rb +4 -3
  29. data/app/models/active_storage/blob/identifiable.rb +1 -0
  30. data/app/models/active_storage/blob/representable.rb +14 -8
  31. data/app/models/active_storage/blob.rb +93 -57
  32. data/app/models/active_storage/current.rb +2 -2
  33. data/app/models/active_storage/filename.rb +2 -0
  34. data/app/models/active_storage/named_variant.rb +21 -0
  35. data/app/models/active_storage/preview.rb +11 -7
  36. data/app/models/active_storage/record.rb +1 -1
  37. data/app/models/active_storage/variant.rb +10 -12
  38. data/app/models/active_storage/variant_record.rb +2 -0
  39. data/app/models/active_storage/variant_with_record.rb +28 -12
  40. data/app/models/active_storage/variation.rb +7 -5
  41. data/config/routes.rb +12 -10
  42. data/db/migrate/20170806125915_create_active_storage_tables.rb +15 -6
  43. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
  44. data/lib/active_storage/analyzer/audio_analyzer.rb +77 -0
  45. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +41 -0
  46. data/lib/active_storage/analyzer/image_analyzer/vips.rb +51 -0
  47. data/lib/active_storage/analyzer/image_analyzer.rb +4 -30
  48. data/lib/active_storage/analyzer/video_analyzer.rb +41 -17
  49. data/lib/active_storage/analyzer.rb +10 -4
  50. data/lib/active_storage/attached/changes/create_many.rb +14 -5
  51. data/lib/active_storage/attached/changes/create_one.rb +46 -4
  52. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  53. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  54. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  55. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  56. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  57. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  58. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  59. data/lib/active_storage/attached/changes.rb +7 -1
  60. data/lib/active_storage/attached/many.rb +32 -19
  61. data/lib/active_storage/attached/model.rb +80 -29
  62. data/lib/active_storage/attached/one.rb +37 -31
  63. data/lib/active_storage/attached.rb +2 -0
  64. data/lib/active_storage/deprecator.rb +7 -0
  65. data/lib/active_storage/downloader.rb +4 -4
  66. data/lib/active_storage/engine.rb +55 -7
  67. data/lib/active_storage/fixture_set.rb +75 -0
  68. data/lib/active_storage/gem_version.rb +3 -3
  69. data/lib/active_storage/log_subscriber.rb +12 -0
  70. data/lib/active_storage/previewer.rb +12 -5
  71. data/lib/active_storage/reflection.rb +12 -2
  72. data/lib/active_storage/service/azure_storage_service.rb +30 -6
  73. data/lib/active_storage/service/configurator.rb +1 -1
  74. data/lib/active_storage/service/disk_service.rb +26 -19
  75. data/lib/active_storage/service/gcs_service.rb +100 -11
  76. data/lib/active_storage/service/mirror_service.rb +12 -7
  77. data/lib/active_storage/service/registry.rb +1 -1
  78. data/lib/active_storage/service/s3_service.rb +39 -15
  79. data/lib/active_storage/service.rb +17 -7
  80. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  81. data/lib/active_storage/transformers/transformer.rb +3 -1
  82. data/lib/active_storage/version.rb +1 -1
  83. data/lib/active_storage.rb +22 -2
  84. metadata +30 -30
  85. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mini_mime"
3
+ require "marcel"
4
4
 
5
5
  module ActiveStorage::Blob::Representable
6
6
  extend ActiveSupport::Concern
@@ -12,8 +12,8 @@ module ActiveStorage::Blob::Representable
12
12
  has_one_attached :preview_image
13
13
  end
14
14
 
15
- # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
16
- # files, and it allows any image to be transformed for size, colors, and the like. Example:
15
+ # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord instance with the set of +transformations+ provided.
16
+ # This is only relevant for image files, and it allows any image to be transformed for size, colors, and the like. Example:
17
17
  #
18
18
  # avatar.variant(resize_to_limit: [100, 100]).processed.url
19
19
  #
@@ -28,8 +28,9 @@ module ActiveStorage::Blob::Representable
28
28
  # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
29
29
  # can then produce on-demand.
30
30
  #
31
- # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
32
- # variable, call ActiveStorage::Blob#variable?.
31
+ # Raises ActiveStorage::InvariableError if the variant processor cannot
32
+ # transform the blob. To determine whether a blob is variable, call
33
+ # ActiveStorage::Blob#variable?.
33
34
  def variant(transformations)
34
35
  if variable?
35
36
  variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
@@ -38,7 +39,8 @@ module ActiveStorage::Blob::Representable
38
39
  end
39
40
  end
40
41
 
41
- # Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
42
+ # Returns true if the variant processor can transform the blob (its content
43
+ # type is in +ActiveStorage.variable_content_types+).
42
44
  def variable?
43
45
  ActiveStorage.variable_content_types.include?(content_type)
44
46
  end
@@ -96,6 +98,10 @@ module ActiveStorage::Blob::Representable
96
98
  variable? || previewable?
97
99
  end
98
100
 
101
+ def preprocessed(transformations) # :nodoc:
102
+ ActiveStorage::TransformJob.perform_later(self, transformations)
103
+ end
104
+
99
105
  private
100
106
  def default_variant_transformations
101
107
  { format: default_variant_format }
@@ -110,10 +116,10 @@ module ActiveStorage::Blob::Representable
110
116
  end
111
117
 
112
118
  def format
113
- if filename.extension.present? && MiniMime.lookup_by_extension(filename.extension)&.content_type == content_type
119
+ if filename.extension.present? && Marcel::MimeType.for(extension: filename.extension) == content_type
114
120
  filename.extension
115
121
  else
116
- MiniMime.lookup_by_content_type(content_type)&.extension
122
+ Marcel::Magic.new(content_type.to_s).extensions.first
117
123
  end
118
124
  end
119
125
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Blob
4
+ #
3
5
  # A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
4
6
  # Blobs can be created in two ways:
5
7
  #
@@ -15,50 +17,46 @@
15
17
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
16
18
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
17
19
  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
20
+ include Analyzable
21
+ include Identifiable
22
+ include Representable
36
23
 
37
24
  self.table_name = "active_storage_blobs"
38
25
 
39
26
  MINIMUM_TOKEN_LENGTH = 28
40
27
 
41
28
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
42
- store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
29
+ store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
43
30
 
44
31
  class_attribute :services, default: {}
45
32
  class_attribute :service, instance_accessor: false
46
33
 
34
+ ##
35
+ # :method:
36
+ #
37
+ # Returns the associated +ActiveStorage::Attachment+s.
47
38
  has_many :attachments
48
39
 
40
+ ##
41
+ # :singleton-method:
42
+ #
43
+ # Returns the blobs that aren't attached to any record.
49
44
  scope :unattached, -> { where.missing(:attachments) }
50
45
 
51
46
  after_initialize do
52
47
  self.service_name ||= self.class.service&.name
53
48
  end
54
49
 
55
- after_update_commit :update_service_metadata, if: :content_type_previously_changed?
50
+ after_update :touch_attachment_records
51
+
52
+ after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
56
53
 
57
54
  before_destroy(prepend: true) do
58
55
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
59
56
  end
60
57
 
61
58
  validates :service_name, presence: true
59
+ validates :checksum, presence: true, unless: :composed
62
60
 
63
61
  validate do
64
62
  if service_name_changed? && service_name.present?
@@ -86,21 +84,13 @@ class ActiveStorage::Blob < ActiveStorage::Record
86
84
  super(id, purpose: purpose)
87
85
  end
88
86
 
89
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
90
- new(filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
91
- blob.upload(io, identify: identify)
92
- end
93
- end
94
-
95
- deprecate :build_after_upload
96
-
97
- def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
87
+ def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
98
88
  new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
99
89
  blob.unfurl(io, identify: identify)
100
90
  end
101
91
  end
102
92
 
103
- def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
93
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
104
94
  build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
105
95
  end
106
96
 
@@ -115,9 +105,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
115
105
  end
116
106
  end
117
107
 
118
- alias_method :create_after_upload!, :create_and_upload!
119
- deprecate create_after_upload!: :create_and_upload!
120
-
121
108
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
122
109
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
123
110
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
@@ -130,14 +117,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
130
117
  # To prevent problems with case-insensitive filesystems, especially in combination
131
118
  # with databases which treat indices as case-sensitive, all blob keys generated are going
132
119
  # 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`
120
+ # the same or higher amount of entropy as in the base-58 encoding used by +has_secure_token+
134
121
  # the number of bytes used is increased to 28 from the standard 24
135
122
  def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
136
123
  SecureRandom.base36(length)
137
124
  end
138
125
 
139
126
  # Customize signed ID purposes for backwards compatibility.
140
- def combine_signed_id_purposes(purpose) #:nodoc:
127
+ def combine_signed_id_purposes(purpose) # :nodoc:
141
128
  purpose.to_s
142
129
  end
143
130
 
@@ -145,18 +132,38 @@ class ActiveStorage::Blob < ActiveStorage::Record
145
132
  #
146
133
  # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
147
134
  # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
148
- def signed_id_verifier #:nodoc:
135
+ def signed_id_verifier # :nodoc:
149
136
  @signed_id_verifier ||= ActiveStorage.verifier
150
137
  end
138
+
139
+ def scope_for_strict_loading # :nodoc:
140
+ if strict_loading_by_default? && ActiveStorage.track_variants
141
+ includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
142
+ else
143
+ all
144
+ end
145
+ end
146
+
147
+ # Concatenate multiple blobs into a single "composed" blob.
148
+ def compose(blobs, filename:, content_type: nil, metadata: nil)
149
+ raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
150
+
151
+ content_type ||= blobs.pluck(:content_type).compact.first
152
+
153
+ new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
154
+ combined_blob.compose(blobs.pluck(:key))
155
+ combined_blob.save!
156
+ end
157
+ end
151
158
  end
152
159
 
153
160
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
154
- def signed_id
155
- super(purpose: :blob_id)
161
+ def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
162
+ super
156
163
  end
157
164
 
158
165
  # 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.
166
+ # secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
160
167
  # This key is not intended to be revealed directly to the user.
161
168
  # Always refer to blobs using the signed_id or a verified form of the key.
162
169
  def key
@@ -171,6 +178,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
171
178
  ActiveStorage::Filename.new(self[:filename])
172
179
  end
173
180
 
181
+ def custom_metadata
182
+ self[:metadata][:custom] || {}
183
+ end
184
+
185
+ def custom_metadata=(metadata)
186
+ self[:metadata] = self[:metadata].merge(custom: metadata)
187
+ end
188
+
174
189
  # Returns true if the content_type of this blob is in the image range, like image/png.
175
190
  def image?
176
191
  content_type.start_with?("image")
@@ -200,25 +215,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
200
215
  content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
201
216
  end
202
217
 
203
- alias_method :service_url, :url
204
- deprecate service_url: :url
205
-
206
218
  # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
207
219
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
208
220
  def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
209
- service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
221
+ service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
210
222
  end
211
223
 
212
224
  # Returns a Hash of headers for +service_url_for_direct_upload+ requests.
213
225
  def service_headers_for_direct_upload
214
- service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
226
+ service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
215
227
  end
216
228
 
217
- def content_type_for_serving #:nodoc:
229
+ def content_type_for_serving # :nodoc:
218
230
  forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
219
231
  end
220
232
 
221
- def forced_disposition_for_serving #:nodoc:
233
+ def forced_disposition_for_serving # :nodoc:
222
234
  if forcibly_serve_as_binary? || !allowed_inline?
223
235
  :attachment
224
236
  end
@@ -242,23 +254,33 @@ class ActiveStorage::Blob < ActiveStorage::Record
242
254
  upload_without_unfurling io
243
255
  end
244
256
 
245
- def unfurl(io, identify: true) #:nodoc:
257
+ def unfurl(io, identify: true) # :nodoc:
246
258
  self.checksum = compute_checksum_in_chunks(io)
247
259
  self.content_type = extract_content_type(io) if content_type.nil? || identify
248
260
  self.byte_size = io.size
249
261
  self.identified = true
250
262
  end
251
263
 
252
- def upload_without_unfurling(io) #:nodoc:
264
+ def upload_without_unfurling(io) # :nodoc:
253
265
  service.upload key, io, checksum: checksum, **service_metadata
254
266
  end
255
267
 
268
+ def compose(keys) # :nodoc:
269
+ self.composed = true
270
+ service.compose(keys, key, **service_metadata)
271
+ end
272
+
256
273
  # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
257
274
  # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
258
275
  def download(&block)
259
276
  service.download key, &block
260
277
  end
261
278
 
279
+ # Downloads a part of the file associated with this blob.
280
+ def download_chunk(range)
281
+ service.download_chunk key, range
282
+ end
283
+
262
284
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
263
285
  #
264
286
  # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
@@ -273,12 +295,18 @@ class ActiveStorage::Blob < ActiveStorage::Record
273
295
  #
274
296
  # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
275
297
  def open(tmpdir: nil, &block)
276
- service.open key, checksum: checksum,
277
- name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
298
+ service.open(
299
+ key,
300
+ checksum: checksum,
301
+ verify: !composed,
302
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ],
303
+ tmpdir: tmpdir,
304
+ &block
305
+ )
278
306
  end
279
307
 
280
- def mirror_later #:nodoc:
281
- ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
308
+ def mirror_later # :nodoc:
309
+ service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
282
310
  end
283
311
 
284
312
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
@@ -294,7 +322,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
294
322
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
295
323
  def purge
296
324
  destroy
297
- delete
325
+ delete if previously_persisted?
298
326
  rescue ActiveRecord::InvalidForeignKey
299
327
  end
300
328
 
@@ -311,7 +339,9 @@ class ActiveStorage::Blob < ActiveStorage::Record
311
339
 
312
340
  private
313
341
  def compute_checksum_in_chunks(io)
314
- Digest::MD5.new.tap do |checksum|
342
+ raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
343
+
344
+ OpenSSL::Digest::MD5.new.tap do |checksum|
315
345
  while chunk = io.read(5.megabytes)
316
346
  checksum << chunk
317
347
  end
@@ -338,11 +368,17 @@ class ActiveStorage::Blob < ActiveStorage::Record
338
368
 
339
369
  def service_metadata
340
370
  if forcibly_serve_as_binary?
341
- { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
371
+ { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
342
372
  elsif !allowed_inline?
343
- { content_type: content_type, disposition: :attachment, filename: filename }
373
+ { content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
344
374
  else
345
- { content_type: content_type }
375
+ { content_type: content_type, custom_metadata: custom_metadata }
376
+ end
377
+ end
378
+
379
+ def touch_attachment_records
380
+ attachments.includes(:record).each do |attachment|
381
+ attachment.touch
346
382
  end
347
383
  end
348
384
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
4
- attribute :host
3
+ class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
4
+ attribute :url_options
5
5
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Filename
4
+ #
3
5
  # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
4
6
  # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
5
7
  class ActiveStorage::Filename
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::NamedVariant # :nodoc:
4
+ attr_reader :transformations, :preprocessed
5
+
6
+ def initialize(transformations)
7
+ @preprocessed = transformations[:preprocessed]
8
+ @transformations = transformations.except(:preprocessed)
9
+ end
10
+
11
+ def preprocessed?(record)
12
+ case preprocessed
13
+ when Symbol
14
+ record.send(preprocessed)
15
+ when Proc
16
+ preprocessed.call(record)
17
+ else
18
+ preprocessed
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Preview
4
+ #
3
5
  # Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
4
6
  # extracting its first frame, and a PDF blob can be previewed by extracting its first page.
5
7
  #
@@ -10,7 +12,7 @@
10
12
  # documentation for more details on what's required of previewers.
11
13
  #
12
14
  # To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
13
- # first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
15
+ # first previewer for which +accept?+ returns true when given the blob. In a \Rails application, add or remove previewers
14
16
  # by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
15
17
  #
16
18
  # Rails.application.config.active_storage.previewers
@@ -20,13 +22,13 @@
20
22
  # Rails.application.config.active_storage.previewers << DOCXPreviewer
21
23
  # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
22
24
  #
23
- # Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
25
+ # Outside of a \Rails application, modify +ActiveStorage.previewers+ instead.
24
26
  #
25
27
  # The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
26
28
  # {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
27
29
  # and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
28
30
  #
29
- # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
31
+ # These libraries are not provided by \Rails. You must install them yourself to use the built-in previewers. Before you
30
32
  # install and use third-party software, make sure you understand the licensing implications of doing so.
31
33
  class ActiveStorage::Preview
32
34
  class UnprocessedError < StandardError; end
@@ -66,9 +68,6 @@ class ActiveStorage::Preview
66
68
  end
67
69
  end
68
70
 
69
- alias_method :service_url, :url
70
- deprecate service_url: :url
71
-
72
71
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
72
  def key
74
73
  if processed?
@@ -78,6 +77,11 @@ class ActiveStorage::Preview
78
77
  end
79
78
  end
80
79
 
80
+ # Downloads the file associated with this preview's variant. If no block is
81
+ # given, the entire file is read into memory and returned. That'll use a lot
82
+ # of RAM for very large files. If a block is given, then the download is
83
+ # streamed and yielded in chunks. Raises ActiveStorage::Preview::UnprocessedError
84
+ # if the preview has not been processed yet.
81
85
  def download(&block)
82
86
  if processed?
83
87
  variant.download(&block)
@@ -93,7 +97,7 @@ class ActiveStorage::Preview
93
97
 
94
98
  def process
95
99
  previewer.preview(service_name: blob.service_name) do |attachable|
96
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
100
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
97
101
  image.attach(attachable)
98
102
  end
99
103
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
3
+ class ActiveStorage::Record < ActiveRecord::Base # :nodoc:
4
4
  self.abstract_class = true
5
5
  end
6
6
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Variant
4
+ #
3
5
  # Image blobs can have variants that are the result of a set of transformations applied to the original.
4
6
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
5
7
  # original.
@@ -42,7 +44,7 @@
42
44
  # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
43
45
  # ImageProcessing gem (such as +resize_to_limit+):
44
46
  #
45
- # avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
47
+ # avatar.variant(resize_to_limit: [800, 800], colourspace: "b-w", rotate: "-90")
46
48
  #
47
49
  # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
48
50
  #
@@ -67,21 +69,18 @@ class ActiveStorage::Variant
67
69
 
68
70
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
69
71
  def key
70
- "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
72
+ "variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
71
73
  end
72
74
 
73
75
  # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
74
76
  #
75
- # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
77
+ # Use <tt>url_for(variant)</tt> (or the implied form, like <tt>link_to variant</tt> or <tt>redirect_to variant</tt>) to get the stable URL
76
78
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
77
79
  # for its redirection.
78
80
  def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
79
81
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
80
82
  end
81
83
 
82
- alias_method :service_url, :url
83
- deprecate service_url: :url
84
-
85
84
  # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
86
85
  # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
87
86
  def download(&block)
@@ -92,17 +91,16 @@ class ActiveStorage::Variant
92
91
  ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
93
92
  end
94
93
 
95
- alias_method :content_type_for_serving, :content_type
96
-
97
- def forced_disposition_for_serving #:nodoc:
98
- nil
99
- end
100
-
101
94
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
102
95
  def image
103
96
  self
104
97
  end
105
98
 
99
+ # Deletes variant file from service.
100
+ def destroy
101
+ service.delete(key)
102
+ end
103
+
106
104
  private
107
105
  def processed?
108
106
  service.exist?(key)
@@ -6,3 +6,5 @@ class ActiveStorage::VariantRecord < ActiveStorage::Record
6
6
  belongs_to :blob
7
7
  has_one_attached :image
8
8
  end
9
+
10
+ ActiveSupport.run_load_hooks :active_storage_variant_record, ActiveStorage::VariantRecord
@@ -1,35 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Variant With Record
4
+ #
5
+ # Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an
6
+ # ActiveStorage::VariantRecord. This is only used if +ActiveStorage.track_variants+ is enabled.
3
7
  class ActiveStorage::VariantWithRecord
4
8
  attr_reader :blob, :variation
9
+ delegate :service, to: :blob
10
+ delegate :content_type, to: :variation
5
11
 
6
12
  def initialize(blob, variation)
7
13
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
8
14
  end
9
15
 
10
16
  def processed
11
- process
17
+ process unless processed?
12
18
  self
13
19
  end
14
20
 
15
- def process
16
- transform_blob { |image| create_or_find_record(image: image) } unless processed?
21
+ def image
22
+ record&.image
17
23
  end
18
24
 
19
- def processed?
20
- record.present?
25
+ def filename
26
+ ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
21
27
  end
22
28
 
23
- def image
24
- record&.image
29
+ # Destroys record and deletes file from service.
30
+ def destroy
31
+ record&.destroy
25
32
  end
26
33
 
27
34
  delegate :key, :url, :download, to: :image, allow_nil: true
28
35
 
29
- alias_method :service_url, :url
30
- deprecate service_url: :url
31
-
32
36
  private
37
+ def processed?
38
+ record.present?
39
+ end
40
+
41
+ def process
42
+ transform_blob { |image| create_or_find_record(image: image) }
43
+ end
44
+
33
45
  def transform_blob
34
46
  blob.open do |input|
35
47
  variation.transform(input) do |output|
@@ -41,7 +53,7 @@ class ActiveStorage::VariantWithRecord
41
53
 
42
54
  def create_or_find_record(image:)
43
55
  @record =
44
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
56
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
45
57
  blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
46
58
  record.image.attach(image)
47
59
  end
@@ -49,6 +61,10 @@ class ActiveStorage::VariantWithRecord
49
61
  end
50
62
 
51
63
  def record
52
- @record ||= blob.variant_records.find_by(variation_digest: variation.digest)
64
+ @record ||= if blob.variant_records.loaded?
65
+ blob.variant_records.find { |v| v.variation_digest == variation.digest }
66
+ else
67
+ blob.variant_records.find_by(variation_digest: variation.digest)
68
+ end
53
69
  end
54
70
  end