activestorage 5.2.4 → 6.0.2.1
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 +159 -60
- data/MIT-LICENSE +1 -1
- data/README.md +9 -6
- data/app/assets/javascripts/activestorage.js +4 -1
- data/app/controllers/active_storage/base_controller.rb +3 -5
- data/app/controllers/active_storage/blobs_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +5 -2
- data/app/controllers/active_storage/representations_controller.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +6 -1
- data/app/jobs/active_storage/analyze_job.rb +5 -0
- data/app/jobs/active_storage/base_job.rb +0 -1
- data/app/jobs/active_storage/purge_job.rb +3 -0
- data/app/models/active_storage/attachment.rb +20 -9
- data/app/models/active_storage/blob.rb +89 -34
- data/app/models/active_storage/blob/representable.rb +5 -5
- data/app/models/active_storage/filename.rb +0 -6
- data/app/models/active_storage/preview.rb +3 -3
- data/app/models/active_storage/variant.rb +51 -52
- data/app/models/active_storage/variation.rb +24 -33
- data/config/routes.rb +13 -12
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
- data/lib/active_storage.rb +26 -6
- data/lib/active_storage/analyzer.rb +9 -4
- data/lib/active_storage/analyzer/image_analyzer.rb +11 -4
- data/lib/active_storage/analyzer/video_analyzer.rb +3 -5
- data/lib/active_storage/attached.rb +7 -22
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +69 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +27 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/many.rb +16 -10
- data/lib/active_storage/attached/model.rb +147 -0
- data/lib/active_storage/attached/one.rb +16 -19
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/downloading.rb +8 -0
- data/lib/active_storage/engine.rb +43 -6
- data/lib/active_storage/errors.rb +22 -3
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/previewer.rb +21 -11
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
- data/lib/active_storage/previewer/video_previewer.rb +2 -3
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +9 -6
- data/lib/active_storage/service/azure_storage_service.rb +30 -14
- data/lib/active_storage/service/configurator.rb +3 -1
- data/lib/active_storage/service/disk_service.rb +19 -11
- data/lib/active_storage/service/gcs_service.rb +49 -47
- data/lib/active_storage/service/s3_service.rb +10 -6
- data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
- data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
- data/lib/active_storage/transformers/transformer.rb +42 -0
- data/lib/tasks/activestorage.rake +7 -0
- metadata +41 -12
- data/app/models/active_storage/filename/parameters.rb +0 -36
- data/lib/active_storage/attached/macros.rb +0 -110
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
|
4
4
|
class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
|
5
|
+
queue_as { ActiveStorage.queues[:analysis] }
|
6
|
+
|
7
|
+
discard_on ActiveRecord::RecordNotFound
|
8
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
|
9
|
+
|
5
10
|
def perform(blob)
|
6
11
|
blob.analyze
|
7
12
|
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
|
4
4
|
class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
|
5
|
+
queue_as { ActiveStorage.queues[:purge] }
|
6
|
+
|
5
7
|
discard_on ActiveRecord::RecordNotFound
|
8
|
+
retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer
|
6
9
|
|
7
10
|
def perform(blob)
|
8
11
|
blob.purge
|
@@ -3,9 +3,8 @@
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
4
|
|
5
5
|
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
|
6
|
-
# but it is possible to associate many different records with the same blob.
|
7
|
-
#
|
8
|
-
# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though).
|
6
|
+
# but it is possible to associate many different records with the same blob. A foreign-key constraint
|
7
|
+
# on the attachments table prevents blobs from being purged if they’re still attached to any records.
|
9
8
|
class ActiveStorage::Attachment < ActiveRecord::Base
|
10
9
|
self.table_name = "active_storage_attachments"
|
11
10
|
|
@@ -15,17 +14,18 @@ class ActiveStorage::Attachment < ActiveRecord::Base
|
|
15
14
|
delegate_missing_to :blob
|
16
15
|
|
17
16
|
after_create_commit :analyze_blob_later, :identify_blob
|
17
|
+
after_destroy_commit :purge_dependent_blob_later
|
18
18
|
|
19
|
-
# Synchronously
|
19
|
+
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
|
20
20
|
def purge
|
21
|
-
|
22
|
-
blob
|
21
|
+
delete
|
22
|
+
blob&.purge
|
23
23
|
end
|
24
24
|
|
25
|
-
#
|
25
|
+
# Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob.
|
26
26
|
def purge_later
|
27
|
-
|
28
|
-
blob
|
27
|
+
delete
|
28
|
+
blob&.purge_later
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
@@ -36,4 +36,15 @@ class ActiveStorage::Attachment < ActiveRecord::Base
|
|
36
36
|
def analyze_blob_later
|
37
37
|
blob.analyze_later unless blob.analyzed?
|
38
38
|
end
|
39
|
+
|
40
|
+
def purge_dependent_blob_later
|
41
|
+
blob&.purge_later if dependent == :purge_later
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def dependent
|
46
|
+
record.attachment_reflections[name]&.options[:dependent]
|
47
|
+
end
|
39
48
|
end
|
49
|
+
|
50
|
+
ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
|
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_storage/downloader"
|
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
|
#
|
6
|
-
# 1.
|
7
|
-
#
|
8
|
+
# 1. Ahead of the file being uploaded server-side to the service, via <tt>create_and_upload!</tt>. A rewindable
|
9
|
+
# <tt>io</tt> with the file contents must be available at the server for this operation.
|
10
|
+
# 2. Ahead of the file being directly uploaded client-side to the service, via <tt>create_before_direct_upload!</tt>.
|
8
11
|
#
|
9
12
|
# The first option doesn't require any client-side JavaScript integration, and can be used by any other back-end
|
10
13
|
# service that deals with files. The second option is faster, since you're not using your own server as a staging
|
@@ -14,9 +17,11 @@
|
|
14
17
|
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
|
15
18
|
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
|
16
19
|
class ActiveStorage::Blob < ActiveRecord::Base
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
unless Rails.autoloaders.zeitwerk_enabled?
|
21
|
+
require_dependency "active_storage/blob/analyzable"
|
22
|
+
require_dependency "active_storage/blob/identifiable"
|
23
|
+
require_dependency "active_storage/blob/representable"
|
24
|
+
end
|
20
25
|
|
21
26
|
include Analyzable
|
22
27
|
include Identifiable
|
@@ -38,7 +43,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
38
43
|
end
|
39
44
|
|
40
45
|
class << self
|
41
|
-
# You can
|
46
|
+
# You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
|
42
47
|
# This is particularly helpful for direct uploads where the client-side needs to refer to the blob
|
43
48
|
# that was created ahead of the upload itself on form submission.
|
44
49
|
#
|
@@ -48,23 +53,36 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
48
53
|
end
|
49
54
|
|
50
55
|
# Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
blob.
|
55
|
-
|
56
|
+
# When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
|
57
|
+
def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
|
58
|
+
new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
|
59
|
+
blob.upload(io, identify: identify)
|
60
|
+
end
|
61
|
+
end
|
56
62
|
|
57
|
-
|
63
|
+
def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
|
64
|
+
new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
|
65
|
+
blob.unfurl(io, identify: identify)
|
58
66
|
end
|
59
67
|
end
|
60
68
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
69
|
+
def create_after_unfurling!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc:
|
70
|
+
build_after_unfurling(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Creates a new blob instance and then uploads the contents of the given <tt>io</tt> to the
|
74
|
+
# service. The blob instance is saved before the upload begins to avoid clobbering another due
|
75
|
+
# to key collisions.
|
76
|
+
#
|
77
|
+
# When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
|
78
|
+
def create_and_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil)
|
79
|
+
create_after_unfurling!(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap do |blob|
|
80
|
+
blob.upload_without_unfurling(io)
|
81
|
+
end
|
66
82
|
end
|
67
83
|
|
84
|
+
alias_method :create_after_upload!, :create_and_upload!
|
85
|
+
|
68
86
|
# Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
|
69
87
|
# no file yet. It's intended to be used together with a client-side upload, which will first create the blob
|
70
88
|
# in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
|
@@ -73,6 +91,15 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
73
91
|
def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
|
74
92
|
create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
|
75
93
|
end
|
94
|
+
|
95
|
+
# To prevent problems with case-insensitive filesystems, especially in combination
|
96
|
+
# with databases which treat indices as case-sensitive, all blob keys generated are going
|
97
|
+
# to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
|
98
|
+
# the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
|
99
|
+
# the number of bytes used is increased to 28 from the standard 24
|
100
|
+
def generate_unique_secure_token
|
101
|
+
SecureRandom.base36(28)
|
102
|
+
end
|
76
103
|
end
|
77
104
|
|
78
105
|
# Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
|
@@ -81,9 +108,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
81
108
|
ActiveStorage.verifier.generate(id, purpose: :blob_id)
|
82
109
|
end
|
83
110
|
|
84
|
-
# Returns the key pointing to the file on the service that's associated with this blob. The key is
|
85
|
-
#
|
86
|
-
# to be revealed directly to the user.
|
111
|
+
# Returns the key pointing to the file on the service that's associated with this blob. The key is the
|
112
|
+
# secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
|
113
|
+
# This key is not intended to be revealed directly to the user.
|
114
|
+
# Always refer to blobs using the signed_id or a verified form of the key.
|
87
115
|
def key
|
88
116
|
# We can't wait until the record is first saved to have a key for it
|
89
117
|
self[:key] ||= self.class.generate_unique_secure_token
|
@@ -121,7 +149,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
121
149
|
# with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
|
122
150
|
# Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
|
123
151
|
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
|
124
|
-
def service_url(expires_in:
|
152
|
+
def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
|
125
153
|
filename = ActiveStorage::Filename.wrap(filename || self.filename)
|
126
154
|
|
127
155
|
service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
|
@@ -130,7 +158,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
130
158
|
|
131
159
|
# Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
|
132
160
|
# short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
|
133
|
-
def service_url_for_direct_upload(expires_in:
|
161
|
+
def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
|
134
162
|
service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
|
135
163
|
end
|
136
164
|
|
@@ -146,16 +174,25 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
146
174
|
#
|
147
175
|
# Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the
|
148
176
|
# checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+
|
149
|
-
# and store that in +byte_size+ on the blob record.
|
177
|
+
# and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless
|
178
|
+
# you specify a +content_type+ and pass +identify+ as false.
|
150
179
|
#
|
151
|
-
# Normally, you do not have to call this method directly at all. Use the
|
152
|
-
#
|
153
|
-
|
180
|
+
# Normally, you do not have to call this method directly at all. Use the +create_and_upload!+ class method instead.
|
181
|
+
# If you do use this method directly, make sure you are using it on a persisted Blob as otherwise another blob's
|
182
|
+
# data might get overwritten on the service.
|
183
|
+
def upload(io, identify: true)
|
184
|
+
unfurl io, identify: identify
|
185
|
+
upload_without_unfurling io
|
186
|
+
end
|
187
|
+
|
188
|
+
def unfurl(io, identify: true) #:nodoc:
|
154
189
|
self.checksum = compute_checksum_in_chunks(io)
|
155
|
-
self.content_type = extract_content_type(io)
|
190
|
+
self.content_type = extract_content_type(io) if content_type.nil? || identify
|
156
191
|
self.byte_size = io.size
|
157
192
|
self.identified = true
|
193
|
+
end
|
158
194
|
|
195
|
+
def upload_without_unfurling(io) #:nodoc:
|
159
196
|
service.upload key, io, checksum: checksum, **service_metadata
|
160
197
|
end
|
161
198
|
|
@@ -165,9 +202,27 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
165
202
|
service.download key, &block
|
166
203
|
end
|
167
204
|
|
205
|
+
# Downloads the blob to a tempfile on disk. Yields the tempfile.
|
206
|
+
#
|
207
|
+
# The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
|
208
|
+
#
|
209
|
+
# By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tmpdir:+ to create it in a different directory:
|
210
|
+
#
|
211
|
+
# blob.open(tmpdir: "/path/to/tmp") do |file|
|
212
|
+
# # ...
|
213
|
+
# end
|
214
|
+
#
|
215
|
+
# The tempfile is automatically closed and unlinked after the given block is executed.
|
216
|
+
#
|
217
|
+
# Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
|
218
|
+
def open(tmpdir: nil, &block)
|
219
|
+
service.open key, checksum: checksum,
|
220
|
+
name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
|
221
|
+
end
|
222
|
+
|
168
223
|
|
169
|
-
# Deletes the
|
170
|
-
# deleted as well or you will essentially have a dead reference. It's recommended to use
|
224
|
+
# Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
|
225
|
+
# deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
|
171
226
|
# methods in most circumstances.
|
172
227
|
def delete
|
173
228
|
service.delete(key)
|
@@ -176,15 +231,15 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
176
231
|
|
177
232
|
# Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
|
178
233
|
# blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
|
179
|
-
# be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use
|
234
|
+
# be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
|
180
235
|
def purge
|
181
236
|
destroy
|
182
237
|
delete
|
183
238
|
rescue ActiveRecord::InvalidForeignKey
|
184
239
|
end
|
185
240
|
|
186
|
-
# Enqueues an ActiveStorage::PurgeJob
|
187
|
-
#
|
241
|
+
# Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
|
242
|
+
# an Active Record callback, or in any other real-time scenario.
|
188
243
|
def purge_later
|
189
244
|
ActiveStorage::PurgeJob.perform_later(self)
|
190
245
|
end
|
@@ -231,6 +286,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
|
|
231
286
|
{ content_type: content_type }
|
232
287
|
end
|
233
288
|
end
|
234
|
-
|
235
|
-
ActiveSupport.run_load_hooks(:active_storage_blob, self)
|
236
289
|
end
|
290
|
+
|
291
|
+
ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob
|
@@ -10,7 +10,7 @@ module ActiveStorage::Blob::Representable
|
|
10
10
|
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
|
11
11
|
# files, and it allows any image to be transformed for size, colors, and the like. Example:
|
12
12
|
#
|
13
|
-
# avatar.variant(
|
13
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.service_url
|
14
14
|
#
|
15
15
|
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
|
16
16
|
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
@@ -18,7 +18,7 @@ module ActiveStorage::Blob::Representable
|
|
18
18
|
# Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
|
19
19
|
# specific variant that can be created by a controller on-demand. Like so:
|
20
20
|
#
|
21
|
-
# <%= image_tag Current.user.avatar.variant(
|
21
|
+
# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
|
22
22
|
#
|
23
23
|
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
|
24
24
|
# can then produce on-demand.
|
@@ -43,13 +43,13 @@ module ActiveStorage::Blob::Representable
|
|
43
43
|
# from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
|
44
44
|
# extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
|
45
45
|
#
|
46
|
-
# blob.preview(
|
46
|
+
# blob.preview(resize_to_limit: [100, 100]).processed.service_url
|
47
47
|
#
|
48
48
|
# Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
|
49
49
|
# Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
|
50
50
|
# how to use the built-in version:
|
51
51
|
#
|
52
|
-
# <%= image_tag video.preview(
|
52
|
+
# <%= image_tag video.preview(resize_to_limit: [100, 100]) %>
|
53
53
|
#
|
54
54
|
# This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
|
55
55
|
# whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
|
@@ -69,7 +69,7 @@ module ActiveStorage::Blob::Representable
|
|
69
69
|
|
70
70
|
# Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
|
71
71
|
#
|
72
|
-
# blob.representation(
|
72
|
+
# blob.representation(resize_to_limit: [100, 100]).processed.service_url
|
73
73
|
#
|
74
74
|
# Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
|
75
75
|
# ActiveStorage::Blob#representable? to determine whether a blob is representable.
|
@@ -3,8 +3,6 @@
|
|
3
3
|
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
|
4
4
|
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
|
5
5
|
class ActiveStorage::Filename
|
6
|
-
require_dependency "active_storage/filename/parameters"
|
7
|
-
|
8
6
|
include Comparable
|
9
7
|
|
10
8
|
class << self
|
@@ -60,10 +58,6 @@ class ActiveStorage::Filename
|
|
60
58
|
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
|
61
59
|
end
|
62
60
|
|
63
|
-
def parameters #:nodoc:
|
64
|
-
Parameters.new self
|
65
|
-
end
|
66
|
-
|
67
61
|
# Returns the sanitized version of the filename.
|
68
62
|
def to_s
|
69
63
|
sanitized.to_s
|
@@ -22,8 +22,8 @@
|
|
22
22
|
# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
|
23
23
|
#
|
24
24
|
# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
|
25
|
-
# {
|
26
|
-
# and the other requires {
|
25
|
+
# {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
|
26
|
+
# and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
|
27
27
|
#
|
28
28
|
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
|
29
29
|
# install and use third-party software, make sure you understand the licensing implications of doing so.
|
@@ -38,7 +38,7 @@ class ActiveStorage::Preview
|
|
38
38
|
|
39
39
|
# Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
|
40
40
|
#
|
41
|
-
# blob.preview(
|
41
|
+
# blob.preview(resize_to_limit: [100, 100]).processed.service_url
|
42
42
|
#
|
43
43
|
# Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
|
44
44
|
# image is stored with the blob, it is only generated once.
|
@@ -1,24 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "ostruct"
|
4
4
|
|
5
5
|
# Image blobs can have variants that are the result of a set of transformations applied to the original.
|
6
6
|
# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
|
7
7
|
# original.
|
8
8
|
#
|
9
|
-
# Variants rely on {
|
10
|
-
# of the file, so you must add <tt>gem "
|
9
|
+
# Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
|
10
|
+
# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
|
11
|
+
# default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
|
12
|
+
# {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
|
13
|
+
# {libvips}[http://jcupitt.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/jcupitt/ruby-vips]
|
14
|
+
# gem).
|
11
15
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
+
# Rails.application.config.active_storage.variant_processor
|
17
|
+
# # => :mini_magick
|
18
|
+
#
|
19
|
+
# Rails.application.config.active_storage.variant_processor = :vips
|
20
|
+
# # => :vips
|
21
|
+
#
|
22
|
+
# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
|
23
|
+
# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
|
24
|
+
# in a template, for example. Delay the processing to an on-demand controller, like the one provided in
|
16
25
|
# ActiveStorage::RepresentationsController.
|
17
26
|
#
|
18
27
|
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
|
19
28
|
# by Active Storage like so:
|
20
29
|
#
|
21
|
-
# <%= image_tag Current.user.avatar.variant(
|
30
|
+
# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
|
22
31
|
#
|
23
32
|
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
|
24
33
|
# can then produce on-demand.
|
@@ -27,19 +36,24 @@ require "active_storage/downloading"
|
|
27
36
|
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
|
28
37
|
# the transformations, upload the variant to the service, and return itself again. Example:
|
29
38
|
#
|
30
|
-
# avatar.variant(
|
39
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.service_url
|
31
40
|
#
|
32
41
|
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
|
33
42
|
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
34
43
|
#
|
35
|
-
#
|
36
|
-
#
|
44
|
+
# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
|
45
|
+
# ImageProcessing gem (such as +resize_to_limit+):
|
46
|
+
#
|
47
|
+
# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
|
48
|
+
#
|
49
|
+
# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
|
37
50
|
#
|
38
|
-
#
|
51
|
+
# * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
|
52
|
+
# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
|
53
|
+
# * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
|
54
|
+
# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
|
39
55
|
class ActiveStorage::Variant
|
40
|
-
|
41
|
-
|
42
|
-
WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
|
56
|
+
WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ]
|
43
57
|
|
44
58
|
attr_reader :blob, :variation
|
45
59
|
delegate :service, to: :blob
|
@@ -67,7 +81,7 @@ class ActiveStorage::Variant
|
|
67
81
|
# Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
|
68
82
|
# for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
|
69
83
|
# for its redirection.
|
70
|
-
def service_url(expires_in:
|
84
|
+
def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
|
71
85
|
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
|
72
86
|
end
|
73
87
|
|
@@ -82,51 +96,36 @@ class ActiveStorage::Variant
|
|
82
96
|
end
|
83
97
|
|
84
98
|
def process
|
85
|
-
|
86
|
-
transform
|
87
|
-
format image
|
88
|
-
upload image
|
99
|
+
blob.open do |image|
|
100
|
+
transform(image) { |output| upload(output) }
|
89
101
|
end
|
90
102
|
end
|
91
103
|
|
92
|
-
|
93
|
-
|
94
|
-
if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
|
95
|
-
blob.filename
|
96
|
-
else
|
97
|
-
ActiveStorage::Filename.new("#{blob.filename.base}.png")
|
98
|
-
end
|
104
|
+
def transform(image, &block)
|
105
|
+
variation.transform(image, format: format, &block)
|
99
106
|
end
|
100
107
|
|
101
|
-
def
|
102
|
-
|
108
|
+
def upload(file)
|
109
|
+
service.upload(key, file)
|
103
110
|
end
|
104
111
|
|
105
112
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
113
|
+
def specification
|
114
|
+
@specification ||=
|
115
|
+
if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
|
116
|
+
Specification.new \
|
117
|
+
filename: blob.filename,
|
118
|
+
content_type: blob.content_type,
|
119
|
+
format: nil
|
120
|
+
else
|
121
|
+
Specification.new \
|
122
|
+
filename: ActiveStorage::Filename.new("#{blob.filename.base}.png"),
|
123
|
+
content_type: "image/png",
|
124
|
+
format: "png"
|
125
|
+
end
|
119
126
|
end
|
120
127
|
|
121
|
-
|
122
|
-
variation.transform(image)
|
123
|
-
end
|
128
|
+
delegate :filename, :content_type, :format, to: :specification
|
124
129
|
|
125
|
-
|
126
|
-
image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
|
127
|
-
end
|
128
|
-
|
129
|
-
def upload(image)
|
130
|
-
File.open(image.path, "r") { |file| service.upload(key, file) }
|
131
|
-
end
|
130
|
+
class Specification < OpenStruct; end
|
132
131
|
end
|