activestorage 5.2.4.rc1 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +131 -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 +4 -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 +66 -24
  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 +23 -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 +20 -16
  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 +39 -13
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. data/lib/active_storage/attached/macros.rb +0 -110
@@ -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,5 +1,7 @@
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
  #
@@ -38,7 +40,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
38
40
  end
39
41
 
40
42
  class << self
41
- # You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
43
+ # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
42
44
  # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
43
45
  # that was created ahead of the upload itself on form submission.
44
46
  #
@@ -48,21 +50,25 @@ class ActiveStorage::Blob < ActiveRecord::Base
48
50
  end
49
51
 
50
52
  # 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
53
+ # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
54
+ def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
55
+ new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
56
+ blob.upload(io, identify: identify)
57
+ end
58
+ end
56
59
 
57
- blob.upload io
60
+ def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
61
+ new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
62
+ blob.unfurl(io, identify: identify)
58
63
  end
59
64
  end
60
65
 
61
66
  # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
62
67
  # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
63
68
  # 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
+ # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
70
+ def create_after_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true)
71
+ build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!)
66
72
  end
67
73
 
68
74
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
@@ -73,6 +79,15 @@ class ActiveStorage::Blob < ActiveRecord::Base
73
79
  def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
74
80
  create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
75
81
  end
82
+
83
+ # To prevent problems with case-insensitive filesystems, especially in combination
84
+ # with databases which treat indices as case-sensitive, all blob keys generated are going
85
+ # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
86
+ # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
87
+ # the number of bytes used is increased to 28 from the standard 24
88
+ def generate_unique_secure_token
89
+ SecureRandom.base36(28)
90
+ end
76
91
  end
77
92
 
78
93
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
@@ -81,9 +96,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
81
96
  ActiveStorage.verifier.generate(id, purpose: :blob_id)
82
97
  end
83
98
 
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.
99
+ # Returns the key pointing to the file on the service that's associated with this blob. The key is the
100
+ # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
101
+ # This key is not intended to be revealed directly to the user.
102
+ # Always refer to blobs using the signed_id or a verified form of the key.
87
103
  def key
88
104
  # We can't wait until the record is first saved to have a key for it
89
105
  self[:key] ||= self.class.generate_unique_secure_token
@@ -121,7 +137,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
121
137
  # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
122
138
  # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
123
139
  # 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)
140
+ def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
125
141
  filename = ActiveStorage::Filename.wrap(filename || self.filename)
126
142
 
127
143
  service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
@@ -130,7 +146,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
130
146
 
131
147
  # 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
148
  # 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)
149
+ def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
134
150
  service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
135
151
  end
136
152
 
@@ -146,16 +162,24 @@ class ActiveStorage::Blob < ActiveRecord::Base
146
162
  #
147
163
  # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the
148
164
  # 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.
165
+ # and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless
166
+ # you specify a +content_type+ and pass +identify+ as false.
150
167
  #
151
168
  # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
152
169
  # and +create_after_upload!+.
153
- def upload(io)
170
+ def upload(io, identify: true)
171
+ unfurl io, identify: identify
172
+ upload_without_unfurling io
173
+ end
174
+
175
+ def unfurl(io, identify: true) #:nodoc:
154
176
  self.checksum = compute_checksum_in_chunks(io)
155
- self.content_type = extract_content_type(io)
177
+ self.content_type = extract_content_type(io) if content_type.nil? || identify
156
178
  self.byte_size = io.size
157
179
  self.identified = true
180
+ end
158
181
 
182
+ def upload_without_unfurling(io) #:nodoc:
159
183
  service.upload key, io, checksum: checksum, **service_metadata
160
184
  end
161
185
 
@@ -165,9 +189,27 @@ class ActiveStorage::Blob < ActiveRecord::Base
165
189
  service.download key, &block
166
190
  end
167
191
 
192
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
193
+ #
194
+ # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
195
+ #
196
+ # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tmpdir:+ to create it in a different directory:
197
+ #
198
+ # blob.open(tmpdir: "/path/to/tmp") do |file|
199
+ # # ...
200
+ # end
201
+ #
202
+ # The tempfile is automatically closed and unlinked after the given block is executed.
203
+ #
204
+ # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
205
+ def open(tmpdir: nil, &block)
206
+ service.open key, checksum: checksum,
207
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
208
+ end
209
+
168
210
 
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+
211
+ # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
212
+ # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
171
213
  # methods in most circumstances.
172
214
  def delete
173
215
  service.delete(key)
@@ -176,15 +218,15 @@ class ActiveStorage::Blob < ActiveRecord::Base
176
218
 
177
219
  # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
178
220
  # 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.
221
+ # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
180
222
  def purge
181
223
  destroy
182
224
  delete
183
225
  rescue ActiveRecord::InvalidForeignKey
184
226
  end
185
227
 
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.
228
+ # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
229
+ # an Active Record callback, or in any other real-time scenario.
188
230
  def purge_later
189
231
  ActiveStorage::PurgeJob.perform_later(self)
190
232
  end
@@ -231,6 +273,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
231
273
  { content_type: content_type }
232
274
  end
233
275
  end
234
-
235
- ActiveSupport.run_load_hooks(:active_storage_blob, self)
236
276
  end
277
+
278
+ 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