activestorage 6.1.7 → 7.0.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.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -257
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +856 -0
  6. data/app/assets/javascripts/activestorage.js +270 -377
  7. data/app/controllers/active_storage/base_controller.rb +1 -10
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/direct_uploads_controller.rb +7 -1
  11. data/app/controllers/active_storage/disk_controller.rb +1 -0
  12. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  13. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
  14. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  15. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  16. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  17. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  18. data/app/javascript/activestorage/blob_record.js +10 -3
  19. data/app/javascript/activestorage/direct_upload.js +4 -2
  20. data/app/javascript/activestorage/direct_upload_controller.js +9 -1
  21. data/app/javascript/activestorage/ujs.js +1 -1
  22. data/app/models/active_storage/attachment.rb +35 -2
  23. data/app/models/active_storage/blob/representable.rb +7 -5
  24. data/app/models/active_storage/blob.rb +92 -36
  25. data/app/models/active_storage/current.rb +12 -2
  26. data/app/models/active_storage/preview.rb +6 -4
  27. data/app/models/active_storage/record.rb +1 -1
  28. data/app/models/active_storage/variant.rb +6 -9
  29. data/app/models/active_storage/variant_record.rb +2 -0
  30. data/app/models/active_storage/variant_with_record.rb +9 -5
  31. data/app/models/active_storage/variation.rb +3 -3
  32. data/config/routes.rb +10 -10
  33. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  34. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +0 -4
  35. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +0 -2
  36. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
  37. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  38. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  39. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  40. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  41. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  42. data/lib/active_storage/analyzer.rb +8 -4
  43. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  44. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  45. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  46. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  47. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  48. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  49. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  50. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  51. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  52. data/lib/active_storage/attached/changes.rb +7 -1
  53. data/lib/active_storage/attached/many.rb +27 -15
  54. data/lib/active_storage/attached/model.rb +35 -7
  55. data/lib/active_storage/attached/one.rb +32 -27
  56. data/lib/active_storage/direct_upload_token.rb +59 -0
  57. data/lib/active_storage/downloader.rb +4 -4
  58. data/lib/active_storage/engine.rb +42 -16
  59. data/lib/active_storage/errors.rb +3 -0
  60. data/lib/active_storage/fixture_set.rb +76 -0
  61. data/lib/active_storage/gem_version.rb +3 -3
  62. data/lib/active_storage/previewer/video_previewer.rb +0 -2
  63. data/lib/active_storage/previewer.rb +4 -4
  64. data/lib/active_storage/reflection.rb +12 -2
  65. data/lib/active_storage/service/azure_storage_service.rb +28 -6
  66. data/lib/active_storage/service/configurator.rb +1 -1
  67. data/lib/active_storage/service/disk_service.rb +24 -19
  68. data/lib/active_storage/service/gcs_service.rb +109 -11
  69. data/lib/active_storage/service/mirror_service.rb +2 -2
  70. data/lib/active_storage/service/registry.rb +1 -1
  71. data/lib/active_storage/service/s3_service.rb +37 -15
  72. data/lib/active_storage/service.rb +13 -5
  73. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -66
  74. data/lib/active_storage/transformers/transformer.rb +1 -1
  75. data/lib/active_storage.rb +6 -292
  76. metadata +30 -19
  77. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -39,7 +39,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
39
39
  MINIMUM_TOKEN_LENGTH = 28
40
40
 
41
41
  has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
42
- store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
42
+ store :metadata, accessors: [ :analyzed, :identified, :composed ], coder: ActiveRecord::Coders::JSON
43
43
 
44
44
  class_attribute :services, default: {}
45
45
  class_attribute :service, instance_accessor: false
@@ -52,13 +52,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
52
52
  self.service_name ||= self.class.service&.name
53
53
  end
54
54
 
55
- after_update_commit :update_service_metadata, if: :content_type_previously_changed?
55
+ after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
56
56
 
57
57
  before_destroy(prepend: true) do
58
58
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
59
59
  end
60
60
 
61
61
  validates :service_name, presence: true
62
+ validates :checksum, presence: true, unless: :composed
62
63
 
63
64
  validate do
64
65
  if service_name_changed? && service_name.present?
@@ -86,21 +87,13 @@ class ActiveStorage::Blob < ActiveStorage::Record
86
87
  super(id, purpose: purpose)
87
88
  end
88
89
 
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:
90
+ def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
98
91
  new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
99
92
  blob.unfurl(io, identify: identify)
100
93
  end
101
94
  end
102
95
 
103
- def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
96
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) # :nodoc:
104
97
  build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
105
98
  end
106
99
 
@@ -115,9 +108,6 @@ class ActiveStorage::Blob < ActiveStorage::Record
115
108
  end
116
109
  end
117
110
 
118
- alias_method :create_after_upload!, :create_and_upload!
119
- deprecate create_after_upload!: :create_and_upload!
120
-
121
111
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
122
112
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
123
113
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
@@ -137,7 +127,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
137
127
  end
138
128
 
139
129
  # Customize signed ID purposes for backwards compatibility.
140
- def combine_signed_id_purposes(purpose) #:nodoc:
130
+ def combine_signed_id_purposes(purpose) # :nodoc:
141
131
  purpose.to_s
142
132
  end
143
133
 
@@ -145,14 +135,34 @@ class ActiveStorage::Blob < ActiveStorage::Record
145
135
  #
146
136
  # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
147
137
  # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
148
- def signed_id_verifier #:nodoc:
138
+ def signed_id_verifier # :nodoc:
149
139
  @signed_id_verifier ||= ActiveStorage.verifier
150
140
  end
141
+
142
+ def scope_for_strict_loading # :nodoc:
143
+ if strict_loading_by_default? && ActiveStorage.track_variants
144
+ includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
145
+ else
146
+ all
147
+ end
148
+ end
149
+
150
+ # Concatenate multiple blobs into a single "composed" blob.
151
+ def compose(blobs, filename:, content_type: nil, metadata: nil)
152
+ raise ActiveRecord::RecordNotSaved, "All blobs must be persisted." if blobs.any?(&:new_record?)
153
+
154
+ content_type ||= blobs.pluck(:content_type).compact.first
155
+
156
+ new(filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size)).tap do |combined_blob|
157
+ combined_blob.compose(blobs.pluck(:key))
158
+ combined_blob.save!
159
+ end
160
+ end
151
161
  end
152
162
 
153
163
  # 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)
164
+ def signed_id(purpose: :blob_id, expires_in: nil)
165
+ super
156
166
  end
157
167
 
158
168
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
@@ -171,6 +181,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
171
181
  ActiveStorage::Filename.new(self[:filename])
172
182
  end
173
183
 
184
+ def custom_metadata
185
+ self[:metadata][:custom] || {}
186
+ end
187
+
188
+ def custom_metadata=(metadata)
189
+ self[:metadata] = self[:metadata].merge(custom: metadata)
190
+ end
191
+
174
192
  # Returns true if the content_type of this blob is in the image range, like image/png.
175
193
  def image?
176
194
  content_type.start_with?("image")
@@ -200,25 +218,22 @@ class ActiveStorage::Blob < ActiveStorage::Record
200
218
  content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
201
219
  end
202
220
 
203
- alias_method :service_url, :url
204
- deprecate service_url: :url
205
-
206
221
  # 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
222
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
208
223
  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
224
+ 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
225
  end
211
226
 
212
227
  # Returns a Hash of headers for +service_url_for_direct_upload+ requests.
213
228
  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
229
+ service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
215
230
  end
216
231
 
217
- def content_type_for_serving #:nodoc:
232
+ def content_type_for_serving # :nodoc:
218
233
  forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
219
234
  end
220
235
 
221
- def forced_disposition_for_serving #:nodoc:
236
+ def forced_disposition_for_serving # :nodoc:
222
237
  if forcibly_serve_as_binary? || !allowed_inline?
223
238
  :attachment
224
239
  end
@@ -242,23 +257,33 @@ class ActiveStorage::Blob < ActiveStorage::Record
242
257
  upload_without_unfurling io
243
258
  end
244
259
 
245
- def unfurl(io, identify: true) #:nodoc:
260
+ def unfurl(io, identify: true) # :nodoc:
246
261
  self.checksum = compute_checksum_in_chunks(io)
247
262
  self.content_type = extract_content_type(io) if content_type.nil? || identify
248
263
  self.byte_size = io.size
249
264
  self.identified = true
250
265
  end
251
266
 
252
- def upload_without_unfurling(io) #:nodoc:
267
+ def upload_without_unfurling(io) # :nodoc:
253
268
  service.upload key, io, checksum: checksum, **service_metadata
254
269
  end
255
270
 
271
+ def compose(keys) # :nodoc:
272
+ self.composed = true
273
+ service.compose(keys, key, **service_metadata)
274
+ end
275
+
256
276
  # Downloads the file associated with this blob. If no block is given, the entire file is read into memory and returned.
257
277
  # 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
278
  def download(&block)
259
279
  service.download key, &block
260
280
  end
261
281
 
282
+ # Downloads a part of the file associated with this blob.
283
+ def download_chunk(range)
284
+ service.download_chunk key, range
285
+ end
286
+
262
287
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
263
288
  #
264
289
  # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
@@ -273,11 +298,17 @@ class ActiveStorage::Blob < ActiveStorage::Record
273
298
  #
274
299
  # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
275
300
  def open(tmpdir: nil, &block)
276
- service.open key, checksum: checksum,
277
- name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
301
+ service.open(
302
+ key,
303
+ checksum: checksum,
304
+ verify: !composed,
305
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ],
306
+ tmpdir: tmpdir,
307
+ &block
308
+ )
278
309
  end
279
310
 
280
- def mirror_later #:nodoc:
311
+ def mirror_later # :nodoc:
281
312
  ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
282
313
  end
283
314
 
@@ -294,7 +325,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
294
325
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
295
326
  def purge
296
327
  destroy
297
- delete
328
+ delete if previously_persisted?
298
329
  rescue ActiveRecord::InvalidForeignKey
299
330
  end
300
331
 
@@ -309,9 +340,34 @@ class ActiveStorage::Blob < ActiveStorage::Record
309
340
  services.fetch(service_name)
310
341
  end
311
342
 
343
+ def content_type=(value)
344
+ unless ActiveStorage.silence_invalid_content_types_warning
345
+ if INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7.include?(value)
346
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
347
+ #{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
348
+ If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.variable_content_types`.
349
+ Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
350
+ MSG
351
+ end
352
+
353
+ if INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7.include?(value)
354
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
355
+ #{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
356
+ If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.content_types_to_serve_as_binary`.
357
+ Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
358
+ MSG
359
+ end
360
+ end
361
+
362
+ super
363
+ end
364
+
365
+ INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7 = ["image/jpg", "image/pjpeg", "image/bmp"]
366
+ INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7 = ["text/javascript"]
367
+
312
368
  private
313
369
  def compute_checksum_in_chunks(io)
314
- Digest::MD5.new.tap do |checksum|
370
+ OpenSSL::Digest::MD5.new.tap do |checksum|
315
371
  while chunk = io.read(5.megabytes)
316
372
  checksum << chunk
317
373
  end
@@ -338,11 +394,11 @@ class ActiveStorage::Blob < ActiveStorage::Record
338
394
 
339
395
  def service_metadata
340
396
  if forcibly_serve_as_binary?
341
- { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
397
+ { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
342
398
  elsif !allowed_inline?
343
- { content_type: content_type, disposition: :attachment, filename: filename }
399
+ { content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
344
400
  else
345
- { content_type: content_type }
401
+ { content_type: content_type, custom_metadata: custom_metadata }
346
402
  end
347
403
  end
348
404
 
@@ -1,5 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
4
- attribute :host
3
+ class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
4
+ attribute :url_options
5
+
6
+ def host=(host)
7
+ ActiveSupport::Deprecation.warn("ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options=")
8
+ self.url_options = { host: host }
9
+ end
10
+
11
+ def host
12
+ ActiveSupport::Deprecation.warn("ActiveStorage::Current.host is deprecated, instead use ActiveStorage::Current.url_options")
13
+ self.url_options&.dig(:host)
14
+ end
5
15
  end
@@ -66,9 +66,6 @@ class ActiveStorage::Preview
66
66
  end
67
67
  end
68
68
 
69
- alias_method :service_url, :url
70
- deprecate service_url: :url
71
-
72
69
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
70
  def key
74
71
  if processed?
@@ -78,6 +75,11 @@ class ActiveStorage::Preview
78
75
  end
79
76
  end
80
77
 
78
+ # Downloads the file associated with this preview's variant. If no block is
79
+ # given, the entire file is read into memory and returned. That'll use a lot
80
+ # of RAM for very large files. If a block is given, then the download is
81
+ # streamed and yielded in chunks. Raises ActiveStorage::Preview::UnprocessedError
82
+ # if the preview has not been processed yet.
81
83
  def download(&block)
82
84
  if processed?
83
85
  variant.download(&block)
@@ -93,7 +95,7 @@ class ActiveStorage::Preview
93
95
 
94
96
  def process
95
97
  previewer.preview(service_name: blob.service_name) do |attachable|
96
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
98
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
97
99
  image.attach(attachable)
98
100
  end
99
101
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
3
+ class ActiveStorage::Record < ActiveRecord::Base # :nodoc:
4
4
  self.abstract_class = true
5
5
  end
6
6
 
@@ -4,7 +4,7 @@
4
4
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
5
5
  # original.
6
6
  #
7
- # Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
7
+ # Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
8
8
  # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
9
9
  # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
10
10
  # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
@@ -42,13 +42,13 @@
42
42
  # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
43
43
  # ImageProcessing gem (such as +resize_to_limit+):
44
44
  #
45
- # avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
45
+ # avatar.variant(resize_to_limit: [800, 800], colourspace: "b-w", rotate: "-90")
46
46
  #
47
47
  # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
48
48
  #
49
- # * {ImageProcessing::MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md#methods]
49
+ # * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
50
50
  # * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
51
- # * {ImageProcessing::Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md#methods]
51
+ # * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
52
52
  # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
53
53
  class ActiveStorage::Variant
54
54
  attr_reader :blob, :variation
@@ -67,7 +67,7 @@ class ActiveStorage::Variant
67
67
 
68
68
  # Returns a combination key of the blob and the variation that together identifies a specific variant.
69
69
  def key
70
- "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
70
+ "variants/#{blob.key}/#{OpenSSL::Digest::SHA256.hexdigest(variation.key)}"
71
71
  end
72
72
 
73
73
  # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
@@ -79,9 +79,6 @@ class ActiveStorage::Variant
79
79
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
80
80
  end
81
81
 
82
- alias_method :service_url, :url
83
- deprecate service_url: :url
84
-
85
82
  # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
86
83
  # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
87
84
  def download(&block)
@@ -94,7 +91,7 @@ class ActiveStorage::Variant
94
91
 
95
92
  alias_method :content_type_for_serving, :content_type
96
93
 
97
- def forced_disposition_for_serving #:nodoc:
94
+ def forced_disposition_for_serving # :nodoc:
98
95
  nil
99
96
  end
100
97
 
@@ -6,3 +6,5 @@ class ActiveStorage::VariantRecord < ActiveStorage::Record
6
6
  belongs_to :blob
7
7
  has_one_attached :image
8
8
  end
9
+
10
+ ActiveSupport.run_load_hooks :active_storage_variant_record, ActiveStorage::VariantRecord
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Like an ActiveStorage::Variant, but keeps detail about the variant in the database as an
4
+ # ActiveStorage::VariantRecord. This is only used if `ActiveStorage.track_variants` is enabled.
3
5
  class ActiveStorage::VariantWithRecord
4
6
  attr_reader :blob, :variation
7
+ delegate :service, to: :blob
5
8
 
6
9
  def initialize(blob, variation)
7
10
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
@@ -26,9 +29,6 @@ class ActiveStorage::VariantWithRecord
26
29
 
27
30
  delegate :key, :url, :download, to: :image, allow_nil: true
28
31
 
29
- alias_method :service_url, :url
30
- deprecate service_url: :url
31
-
32
32
  private
33
33
  def transform_blob
34
34
  blob.open do |input|
@@ -41,7 +41,7 @@ class ActiveStorage::VariantWithRecord
41
41
 
42
42
  def create_or_find_record(image:)
43
43
  @record =
44
- ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
44
+ ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
45
45
  blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
46
46
  record.image.attach(image)
47
47
  end
@@ -49,6 +49,10 @@ class ActiveStorage::VariantWithRecord
49
49
  end
50
50
 
51
51
  def record
52
- @record ||= blob.variant_records.find_by(variation_digest: variation.digest)
52
+ @record ||= if blob.variant_records.loaded?
53
+ blob.variant_records.find { |v| v.variation_digest == variation.digest }
54
+ else
55
+ blob.variant_records.find_by(variation_digest: variation.digest)
56
+ end
53
57
  end
54
58
  end
@@ -8,9 +8,9 @@ require "mini_mime"
8
8
  # In case you do need to use this directly, it's instantiated using a hash of transformations where
9
9
  # the key is the command and the value is the arguments. Example:
10
10
  #
11
- # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
11
+ # ActiveStorage::Variation.new(resize_to_limit: [100, 100], colourspace: "b-w", rotate: "-90", saver: { trim: true })
12
12
  #
13
- # The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands.
13
+ # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
14
14
  class ActiveStorage::Variation
15
15
  attr_reader :transformations
16
16
 
@@ -75,7 +75,7 @@ class ActiveStorage::Variation
75
75
  end
76
76
 
77
77
  def digest
78
- Digest::SHA1.base64digest Marshal.dump(transformations)
78
+ OpenSSL::Digest::SHA1.base64digest Marshal.dump(transformations)
79
79
  end
80
80
 
81
81
  private
data/config/routes.rb CHANGED
@@ -16,11 +16,7 @@ Rails.application.routes.draw do
16
16
  end
17
17
 
18
18
  direct :rails_representation do |representation, options|
19
- signed_blob_id = representation.blob.signed_id
20
- variation_key = representation.variation.key
21
- filename = representation.blob.filename
22
-
23
- route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
19
+ route_for(ActiveStorage.resolve_model_to_route, representation, options)
24
20
  end
25
21
 
26
22
  resolve("ActiveStorage::Variant") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
@@ -28,22 +24,24 @@ Rails.application.routes.draw do
28
24
  resolve("ActiveStorage::Preview") { |preview, options| route_for(ActiveStorage.resolve_model_to_route, preview, options) }
29
25
 
30
26
  direct :rails_blob do |blob, options|
31
- route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
27
+ route_for(ActiveStorage.resolve_model_to_route, blob, options)
32
28
  end
33
29
 
34
30
  resolve("ActiveStorage::Blob") { |blob, options| route_for(ActiveStorage.resolve_model_to_route, blob, options) }
35
31
  resolve("ActiveStorage::Attachment") { |attachment, options| route_for(ActiveStorage.resolve_model_to_route, attachment.blob, options) }
36
32
 
37
33
  direct :rails_storage_proxy do |model, options|
34
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
35
+
38
36
  if model.respond_to?(:signed_id)
39
37
  route_for(
40
38
  :rails_service_blob_proxy,
41
- model.signed_id,
39
+ model.signed_id(expires_in: expires_in),
42
40
  model.filename,
43
41
  options
44
42
  )
45
43
  else
46
- signed_blob_id = model.blob.signed_id
44
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in)
47
45
  variation_key = model.variation.key
48
46
  filename = model.blob.filename
49
47
 
@@ -58,15 +56,17 @@ Rails.application.routes.draw do
58
56
  end
59
57
 
60
58
  direct :rails_storage_redirect do |model, options|
59
+ expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
60
+
61
61
  if model.respond_to?(:signed_id)
62
62
  route_for(
63
63
  :rails_service_blob,
64
- model.signed_id,
64
+ model.signed_id(expires_in: expires_in),
65
65
  model.filename,
66
66
  options
67
67
  )
68
68
  else
69
- signed_blob_id = model.blob.signed_id
69
+ signed_blob_id = model.blob.signed_id(expires_in: expires_in)
70
70
  variation_key = model.variation.key
71
71
  filename = model.blob.filename
72
72
 
@@ -10,8 +10,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
10
10
  t.text :metadata
11
11
  t.string :service_name, null: false
12
12
  t.bigint :byte_size, null: false
13
- t.string :checksum, null: false
14
- t.datetime :created_at, null: false
13
+ t.string :checksum
14
+
15
+ if connection.supports_datetime_with_precision?
16
+ t.datetime :created_at, precision: 6, null: false
17
+ else
18
+ t.datetime :created_at, null: false
19
+ end
15
20
 
16
21
  t.index [ :key ], unique: true
17
22
  end
@@ -21,9 +26,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
21
26
  t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
22
27
  t.references :blob, null: false, type: foreign_key_type
23
28
 
24
- t.datetime :created_at, null: false
29
+ if connection.supports_datetime_with_precision?
30
+ t.datetime :created_at, precision: 6, null: false
31
+ else
32
+ t.datetime :created_at, null: false
33
+ end
25
34
 
26
- t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
35
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
27
36
  t.foreign_key :active_storage_blobs, column: :blob_id
28
37
  end
29
38
 
@@ -31,7 +40,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
31
40
  t.belongs_to :blob, null: false, index: false, type: foreign_key_type
32
41
  t.string :variation_digest, null: false
33
42
 
34
- t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
43
+ t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
35
44
  t.foreign_key :active_storage_blobs, column: :blob_id
36
45
  end
37
46
  end
@@ -1,7 +1,5 @@
1
1
  class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
2
2
  def up
3
- return unless table_exists?(:active_storage_blobs)
4
-
5
3
  unless column_exists?(:active_storage_blobs, :service_name)
6
4
  add_column :active_storage_blobs, :service_name, :string
7
5
 
@@ -14,8 +12,6 @@ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
14
12
  end
15
13
 
16
14
  def down
17
- return unless table_exists?(:active_storage_blobs)
18
-
19
15
  remove_column :active_storage_blobs, :service_name
20
16
  end
21
17
  end
@@ -1,7 +1,5 @@
1
1
  class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
2
  def change
3
- return unless table_exists?(:active_storage_blobs)
4
-
5
3
  # Use Active Record's configured type for primary key
6
4
  create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
7
5
  t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
@@ -0,0 +1,5 @@
1
+ class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
2
+ def change
3
+ change_column_null(:active_storage_blobs, :checksum, true)
4
+ end
5
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ # Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
5
+ #
6
+ # Example:
7
+ #
8
+ # ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
9
+ # # => { duration: 5.0, bit_rate: 320340 }
10
+ #
11
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
12
+ class Analyzer::AudioAnalyzer < Analyzer
13
+ def self.accept?(blob)
14
+ blob.audio?
15
+ end
16
+
17
+ def metadata
18
+ { duration: duration, bit_rate: bit_rate }.compact
19
+ end
20
+
21
+ private
22
+ def duration
23
+ duration = audio_stream["duration"]
24
+ Float(duration) if duration
25
+ end
26
+
27
+ def bit_rate
28
+ bit_rate = audio_stream["bit_rate"]
29
+ Integer(bit_rate) if bit_rate
30
+ end
31
+
32
+ def audio_stream
33
+ @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
34
+ end
35
+
36
+ def streams
37
+ probe["streams"] || []
38
+ end
39
+
40
+ def probe
41
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
42
+ end
43
+
44
+ def probe_from(file)
45
+ instrument(File.basename(ffprobe_path)) do
46
+ IO.popen([ ffprobe_path,
47
+ "-print_format", "json",
48
+ "-show_streams",
49
+ "-show_format",
50
+ "-v", "error",
51
+ file.path
52
+ ]) do |output|
53
+ JSON.parse(output.read)
54
+ end
55
+ end
56
+ rescue Errno::ENOENT
57
+ logger.info "Skipping audio analysis because FFmpeg isn't installed"
58
+ {}
59
+ end
60
+
61
+ def ffprobe_path
62
+ ActiveStorage.paths[:ffprobe] || "ffprobe"
63
+ end
64
+ end
65
+ end