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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +144 -204
- data/MIT-LICENSE +1 -1
- data/README.md +25 -11
- data/app/assets/javascripts/activestorage.esm.js +856 -0
- data/app/assets/javascripts/activestorage.js +270 -377
- data/app/controllers/active_storage/base_controller.rb +1 -10
- data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
- data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
- data/app/controllers/active_storage/direct_uploads_controller.rb +7 -1
- data/app/controllers/active_storage/disk_controller.rb +1 -0
- data/app/controllers/active_storage/representations/base_controller.rb +5 -1
- data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
- data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
- data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
- data/app/controllers/concerns/active_storage/set_current.rb +3 -3
- data/app/controllers/concerns/active_storage/streaming.rb +65 -0
- data/app/javascript/activestorage/blob_record.js +10 -3
- data/app/javascript/activestorage/direct_upload.js +4 -2
- data/app/javascript/activestorage/direct_upload_controller.js +9 -1
- data/app/javascript/activestorage/ujs.js +1 -1
- data/app/models/active_storage/attachment.rb +36 -3
- data/app/models/active_storage/blob/representable.rb +7 -5
- data/app/models/active_storage/blob.rb +92 -36
- data/app/models/active_storage/current.rb +12 -2
- data/app/models/active_storage/preview.rb +6 -4
- data/app/models/active_storage/record.rb +1 -1
- data/app/models/active_storage/variant.rb +3 -6
- data/app/models/active_storage/variant_record.rb +2 -0
- data/app/models/active_storage/variant_with_record.rb +9 -5
- data/app/models/active_storage/variation.rb +2 -2
- data/config/routes.rb +10 -10
- data/db/migrate/20170806125915_create_active_storage_tables.rb +32 -11
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +15 -2
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
- data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
- data/lib/active_storage/analyzer.rb +8 -4
- data/lib/active_storage/attached/changes/create_many.rb +7 -3
- data/lib/active_storage/attached/changes/create_one.rb +1 -1
- data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_one.rb +1 -1
- data/lib/active_storage/attached/changes/detach_many.rb +18 -0
- data/lib/active_storage/attached/changes/detach_one.rb +24 -0
- data/lib/active_storage/attached/changes/purge_many.rb +27 -0
- data/lib/active_storage/attached/changes/purge_one.rb +27 -0
- data/lib/active_storage/attached/changes.rb +7 -1
- data/lib/active_storage/attached/many.rb +27 -15
- data/lib/active_storage/attached/model.rb +31 -5
- data/lib/active_storage/attached/one.rb +32 -27
- data/lib/active_storage/direct_upload_token.rb +59 -0
- data/lib/active_storage/downloader.rb +4 -4
- data/lib/active_storage/engine.rb +30 -1
- data/lib/active_storage/errors.rb +3 -0
- data/lib/active_storage/fixture_set.rb +76 -0
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/previewer.rb +4 -4
- data/lib/active_storage/reflection.rb +12 -2
- data/lib/active_storage/service/azure_storage_service.rb +28 -6
- data/lib/active_storage/service/configurator.rb +1 -1
- data/lib/active_storage/service/disk_service.rb +24 -19
- data/lib/active_storage/service/gcs_service.rb +109 -11
- data/lib/active_storage/service/mirror_service.rb +2 -2
- data/lib/active_storage/service/registry.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +37 -15
- data/lib/active_storage/service.rb +13 -5
- data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
- data/lib/active_storage/transformers/transformer.rb +1 -1
- data/lib/active_storage.rb +6 -1
- metadata +31 -19
- 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:
|
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
|
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)
|
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)
|
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
|
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
|
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
|
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
|
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)
|
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)
|
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
|
277
|
-
|
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
|
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
|
4
|
-
attribute :
|
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
|
98
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord.writing_role) do
|
97
99
|
image.attach(attachable)
|
98
100
|
end
|
99
101
|
end
|
@@ -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],
|
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
|
94
|
+
def forced_disposition_for_serving # :nodoc:
|
98
95
|
nil
|
99
96
|
end
|
100
97
|
|
@@ -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
|
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.
|
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],
|
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
|
-
|
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(
|
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
|
-
|
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
|
11
|
-
|
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
|
-
|
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:
|
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
|
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
|
-
|
4
|
-
|
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,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
|