activestorage 6.1.4.1 → 7.0.0.rc2

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -204
  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 +36 -3
  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 +3 -6
  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 +2 -2
  32. data/config/routes.rb +10 -10
  33. data/db/migrate/20170806125915_create_active_storage_tables.rb +32 -11
  34. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +15 -2
  35. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
  36. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  37. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  38. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  39. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  40. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  41. data/lib/active_storage/analyzer.rb +8 -4
  42. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  43. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  44. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  45. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  46. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  47. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  48. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  49. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  50. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  51. data/lib/active_storage/attached/changes.rb +7 -1
  52. data/lib/active_storage/attached/many.rb +27 -15
  53. data/lib/active_storage/attached/model.rb +31 -5
  54. data/lib/active_storage/attached/one.rb +32 -27
  55. data/lib/active_storage/direct_upload_token.rb +59 -0
  56. data/lib/active_storage/downloader.rb +4 -4
  57. data/lib/active_storage/engine.rb +30 -1
  58. data/lib/active_storage/errors.rb +3 -0
  59. data/lib/active_storage/fixture_set.rb +76 -0
  60. data/lib/active_storage/gem_version.rb +4 -4
  61. data/lib/active_storage/previewer.rb +4 -4
  62. data/lib/active_storage/reflection.rb +12 -2
  63. data/lib/active_storage/service/azure_storage_service.rb +28 -6
  64. data/lib/active_storage/service/configurator.rb +1 -1
  65. data/lib/active_storage/service/disk_service.rb +24 -19
  66. data/lib/active_storage/service/gcs_service.rb +109 -11
  67. data/lib/active_storage/service/mirror_service.rb +2 -2
  68. data/lib/active_storage/service/registry.rb +1 -1
  69. data/lib/active_storage/service/s3_service.rb +37 -15
  70. data/lib/active_storage/service.rb +13 -5
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  72. data/lib/active_storage/transformers/transformer.rb +1 -1
  73. data/lib/active_storage.rb +6 -1
  74. metadata +31 -19
  75. 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
 
@@ -42,7 +42,7 @@
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
  #
@@ -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,7 +8,7 @@ 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
13
  # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
14
14
  class ActiveStorage::Variation
@@ -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
 
@@ -1,35 +1,56 @@
1
1
  class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
2
2
  def change
3
- create_table :active_storage_blobs do |t|
3
+ # Use Active Record's configured type for primary and foreign keys
4
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
5
+
6
+ create_table :active_storage_blobs, id: primary_key_type do |t|
4
7
  t.string :key, null: false
5
8
  t.string :filename, null: false
6
9
  t.string :content_type
7
10
  t.text :metadata
8
11
  t.string :service_name, null: false
9
12
  t.bigint :byte_size, null: false
10
- t.string :checksum, null: false
11
- 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
12
20
 
13
21
  t.index [ :key ], unique: true
14
22
  end
15
23
 
16
- create_table :active_storage_attachments do |t|
24
+ create_table :active_storage_attachments, id: primary_key_type do |t|
17
25
  t.string :name, null: false
18
- t.references :record, null: false, polymorphic: true, index: false
19
- t.references :blob, null: false
26
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
27
+ t.references :blob, null: false, type: foreign_key_type
20
28
 
21
- 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
22
34
 
23
- 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
24
36
  t.foreign_key :active_storage_blobs, column: :blob_id
25
37
  end
26
38
 
27
- create_table :active_storage_variant_records do |t|
28
- t.belongs_to :blob, null: false, index: false
39
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
40
+ t.belongs_to :blob, null: false, index: false, type: foreign_key_type
29
41
  t.string :variation_digest, null: false
30
42
 
31
- 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
32
44
  t.foreign_key :active_storage_blobs, column: :blob_id
33
45
  end
34
46
  end
47
+
48
+ private
49
+ def primary_and_foreign_key_types
50
+ config = Rails.configuration.generators
51
+ setting = config.options[config.orm][:primary_key_type]
52
+ primary_key_type = setting || :primary_key
53
+ foreign_key_type = setting || :bigint
54
+ [primary_key_type, foreign_key_type]
55
+ end
35
56
  end
@@ -1,11 +1,24 @@
1
1
  class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
2
  def change
3
- create_table :active_storage_variant_records do |t|
4
- t.belongs_to :blob, null: false, index: false
3
+ # Use Active Record's configured type for primary key
4
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
5
+ t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
5
6
  t.string :variation_digest, null: false
6
7
 
7
8
  t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
8
9
  t.foreign_key :active_storage_blobs, column: :blob_id
9
10
  end
10
11
  end
12
+
13
+ private
14
+ def primary_key_type
15
+ config = Rails.configuration.generators
16
+ config.options[config.orm][:primary_key_type] || :primary_key
17
+ end
18
+
19
+ def blobs_primary_key_type
20
+ pkey_name = connection.primary_key(:active_storage_blobs)
21
+ pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
22
+ pkey_column.bigint? ? :bigint : pkey_column.type
23
+ end
11
24
  end
@@ -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