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.

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +159 -60
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +9 -6
  5. data/app/assets/javascripts/activestorage.js +4 -1
  6. data/app/controllers/active_storage/base_controller.rb +3 -5
  7. data/app/controllers/active_storage/blobs_controller.rb +1 -1
  8. data/app/controllers/active_storage/disk_controller.rb +5 -2
  9. data/app/controllers/active_storage/representations_controller.rb +1 -1
  10. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  11. data/app/javascript/activestorage/blob_record.js +6 -1
  12. data/app/jobs/active_storage/analyze_job.rb +5 -0
  13. data/app/jobs/active_storage/base_job.rb +0 -1
  14. data/app/jobs/active_storage/purge_job.rb +3 -0
  15. data/app/models/active_storage/attachment.rb +20 -9
  16. data/app/models/active_storage/blob.rb +89 -34
  17. data/app/models/active_storage/blob/representable.rb +5 -5
  18. data/app/models/active_storage/filename.rb +0 -6
  19. data/app/models/active_storage/preview.rb +3 -3
  20. data/app/models/active_storage/variant.rb +51 -52
  21. data/app/models/active_storage/variation.rb +24 -33
  22. data/config/routes.rb +13 -12
  23. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
  24. data/lib/active_storage.rb +26 -6
  25. data/lib/active_storage/analyzer.rb +9 -4
  26. data/lib/active_storage/analyzer/image_analyzer.rb +11 -4
  27. data/lib/active_storage/analyzer/video_analyzer.rb +3 -5
  28. data/lib/active_storage/attached.rb +7 -22
  29. data/lib/active_storage/attached/changes.rb +16 -0
  30. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  31. data/lib/active_storage/attached/changes/create_one.rb +69 -0
  32. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  33. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  34. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  35. data/lib/active_storage/attached/many.rb +16 -10
  36. data/lib/active_storage/attached/model.rb +147 -0
  37. data/lib/active_storage/attached/one.rb +16 -19
  38. data/lib/active_storage/downloader.rb +43 -0
  39. data/lib/active_storage/downloading.rb +8 -0
  40. data/lib/active_storage/engine.rb +43 -6
  41. data/lib/active_storage/errors.rb +22 -3
  42. data/lib/active_storage/gem_version.rb +4 -4
  43. data/lib/active_storage/previewer.rb +21 -11
  44. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  45. data/lib/active_storage/previewer/video_previewer.rb +2 -3
  46. data/lib/active_storage/reflection.rb +64 -0
  47. data/lib/active_storage/service.rb +9 -6
  48. data/lib/active_storage/service/azure_storage_service.rb +30 -14
  49. data/lib/active_storage/service/configurator.rb +3 -1
  50. data/lib/active_storage/service/disk_service.rb +19 -11
  51. data/lib/active_storage/service/gcs_service.rb +49 -47
  52. data/lib/active_storage/service/s3_service.rb +10 -6
  53. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  54. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  55. data/lib/active_storage/transformers/transformer.rb +42 -0
  56. data/lib/tasks/activestorage.rake +7 -0
  57. metadata +41 -12
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. 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
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ActiveStorage::BaseJob < ActiveJob::Base
4
- queue_as { ActiveStorage.queue }
5
4
  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. If you're doing that,
7
- # you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying
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 purges the blob (deletes it from the configured service) and destroys the attachment.
19
+ # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
20
20
  def purge
21
- destroy
22
- blob.purge
21
+ delete
22
+ blob&.purge
23
23
  end
24
24
 
25
- # Destroys the attachment and asynchronously purges the blob (deletes it from the configured service).
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
- destroy
28
- blob.purge_later
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. Subsequent to the file being uploaded server-side to the service via <tt>create_after_upload!</tt>.
7
- # 2. Ahead of the file being directly uploaded client-side to the service via <tt>create_before_direct_upload!</tt>.
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
- require_dependency "active_storage/blob/analyzable"
18
- require_dependency "active_storage/blob/identifiable"
19
- require_dependency "active_storage/blob/representable"
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 used the signed ID of a blob to refer to it on the client side without fear of tampering.
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
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil)
52
- new.tap do |blob|
53
- blob.filename = filename
54
- blob.content_type = content_type
55
- blob.metadata = metadata
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
- blob.upload io
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
- # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
62
- # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
63
- # time), while having an open database transaction.
64
- def create_after_upload!(io:, filename:, content_type: nil, metadata: nil)
65
- build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata).tap(&:save!)
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 in the
85
- # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended
86
- # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key.
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: service.url_expires_in, disposition: :inline, filename: nil, **options)
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: service.url_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 factory class methods of +build_after_upload+
152
- # and +create_after_upload!+.
153
- def upload(io)
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 file on the service that's associated with this blob. This should only be done if the blob is going to be
170
- # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+
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 +#purge_later+ instead.
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 job that'll call +purge+. This is the recommended way to purge blobs when the call
187
- # needs to be made from a transaction, a callback, or any other real-time scenario.
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(resize: "100x100").processed.service_url
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(resize: "100x100") %>
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(resize: "100x100").processed.service_url
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(resize: "100x100") %>
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(resize: "100x100").processed.service_url
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
- # {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.
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(resize: "100x100").processed.service_url
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 "active_storage/downloading"
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 {MiniMagick}[https://github.com/minimagick/minimagick] for the actual transformations
10
- # of the file, so you must add <tt>gem "mini_magick"</tt> to your Gemfile if you wish to use variants.
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
- # Note that to create a variant it's necessary to download the entire blob file from the service and load it
13
- # into memory. The larger the image, the more memory is used. Because of this process, you also want to be
14
- # considerate about when the variant is actually processed. You shouldn't be processing variants inline in a
15
- # template, for example. Delay the processing to an on-demand controller, like the one provided in
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(resize: "100x100") %>
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(resize: "100x100").processed.service_url
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
- # A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php. You can
36
- # combine as many as you like freely:
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
- # avatar.variant(resize: "100x100", monochrome: true, rotate: "-90")
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
- include ActiveStorage::Downloading
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: service.url_expires_in, disposition: :inline)
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
- open_image do |image|
86
- transform image
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
- def filename
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 content_type
102
- blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
108
+ def upload(file)
109
+ service.upload(key, file)
103
110
  end
104
111
 
105
112
 
106
- def open_image(&block)
107
- image = download_image
108
-
109
- begin
110
- yield image
111
- ensure
112
- image.destroy!
113
- end
114
- end
115
-
116
- def download_image
117
- require "mini_magick"
118
- MiniMagick::Image.create(blob.filename.extension_with_delimiter) { |file| download_blob_to(file) }
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
- def transform(image)
122
- variation.transform(image)
123
- end
128
+ delegate :filename, :content_type, :format, to: :specification
124
129
 
125
- def format(image)
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