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