activestorage 5.2.7 → 6.0.0.beta1

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 +102 -142
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -5
  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 +4 -1
  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 +18 -9
  16. data/app/models/active_storage/blob/representable.rb +5 -5
  17. data/app/models/active_storage/blob.rb +63 -22
  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 +23 -92
  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 +7 -0
  24. data/lib/active_storage/analyzer/video_analyzer.rb +2 -4
  25. data/lib/active_storage/analyzer.rb +9 -4
  26. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  27. data/lib/active_storage/attached/changes/create_one.rb +68 -0
  28. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  29. data/lib/active_storage/attached/changes/delete_many.rb +23 -0
  30. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  31. data/lib/active_storage/attached/changes.rb +16 -0
  32. data/lib/active_storage/attached/many.rb +16 -10
  33. data/lib/active_storage/attached/model.rb +140 -0
  34. data/lib/active_storage/attached/one.rb +16 -19
  35. data/lib/active_storage/attached.rb +7 -22
  36. data/lib/active_storage/downloader.rb +44 -0
  37. data/lib/active_storage/downloading.rb +8 -0
  38. data/lib/active_storage/engine.rb +36 -21
  39. data/lib/active_storage/errors.rb +22 -3
  40. data/lib/active_storage/gem_version.rb +4 -4
  41. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
  42. data/lib/active_storage/previewer/video_previewer.rb +2 -3
  43. data/lib/active_storage/previewer.rb +21 -11
  44. data/lib/active_storage/reflection.rb +64 -0
  45. data/lib/active_storage/service/azure_storage_service.rb +30 -14
  46. data/lib/active_storage/service/configurator.rb +3 -1
  47. data/lib/active_storage/service/disk_service.rb +20 -16
  48. data/lib/active_storage/service/gcs_service.rb +48 -46
  49. data/lib/active_storage/service/mirror_service.rb +1 -1
  50. data/lib/active_storage/service/s3_service.rb +10 -9
  51. data/lib/active_storage/service.rb +5 -6
  52. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  53. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  54. data/lib/active_storage/transformers/transformer.rb +42 -0
  55. data/lib/active_storage.rb +13 -292
  56. data/lib/tasks/activestorage.rake +7 -0
  57. metadata +28 -16
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. data/lib/active_storage/attached/macros.rb +0 -110
@@ -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,26 @@ 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 +tempdir:+ to create it in a different directory:
197
+ #
198
+ # blob.open(tempdir: "/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(tempdir: nil, &block)
206
+ ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block)
207
+ end
208
+
168
209
 
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+
210
+ # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
211
+ # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
171
212
  # methods in most circumstances.
172
213
  def delete
173
214
  service.delete(key)
@@ -176,15 +217,15 @@ class ActiveStorage::Blob < ActiveRecord::Base
176
217
 
177
218
  # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
178
219
  # 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.
220
+ # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
180
221
  def purge
181
222
  destroy
182
223
  delete
183
224
  rescue ActiveRecord::InvalidForeignKey
184
225
  end
185
226
 
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.
227
+ # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
228
+ # an Active Record callback, or in any other real-time scenario.
188
229
  def purge_later
189
230
  ActiveStorage::PurgeJob.perform_later(self)
190
231
  end
@@ -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_fit: [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_fit: [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_fit: [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_fit+):
46
+ #
47
+ # avatar.variant(resize_to_fit: [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
@@ -6,23 +6,12 @@
6
6
  # In case you do need to use this directly, it's instantiated using a hash of transformations where
7
7
  # the key is the command and the value is the arguments. Example:
8
8
  #
9
- # ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
9
+ # ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90")
10
10
  #
11
- # You can also combine multiple transformations in one step, e.g. for center-weighted cropping:
12
- #
13
- # ActiveStorage::Variation.new(combine_options: {
14
- # resize: "100x100^",
15
- # gravity: "center",
16
- # crop: "100x100+0+0",
17
- # })
18
- #
19
- # A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
11
+ # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
20
12
  class ActiveStorage::Variation
21
13
  attr_reader :transformations
22
14
 
23
- class UnsupportedImageProcessingMethod < StandardError; end
24
- class UnsupportedImageProcessingArgument < StandardError; end
25
-
26
15
  class << self
27
16
  # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
28
17
  # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
@@ -54,24 +43,13 @@ class ActiveStorage::Variation
54
43
  @transformations = transformations
55
44
  end
56
45
 
57
- # Accepts an open MiniMagick image instance, like what's returned by <tt>MiniMagick::Image.read(io)</tt>,
58
- # and performs the +transformations+ against it. The transformed image instance is then returned.
59
- def transform(image)
46
+ # Accepts a File object, performs the +transformations+ against it, and
47
+ # saves the transformed image into a temporary file. If +format+ is specified
48
+ # it will be the format of the result image, otherwise the result image
49
+ # retains the source format.
50
+ def transform(file, format: nil, &block)
60
51
  ActiveSupport::Notifications.instrument("transform.active_storage") do
61
- transformations.each do |name, argument_or_subtransformations|
62
- validate_transformation(name, argument_or_subtransformations)
63
- image.mogrify do |command|
64
- if name.to_s == "combine_options"
65
- argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
66
- validate_transformation(subtransformation_name, subtransformation_argument)
67
- pass_transform_argument(command, subtransformation_name, subtransformation_argument)
68
- end
69
- else
70
- validate_transformation(name, argument_or_subtransformations)
71
- pass_transform_argument(command, name, argument_or_subtransformations)
72
- end
73
- end
74
- end
52
+ transformer.transform(file, format: format, &block)
75
53
  end
76
54
  end
77
55
 
@@ -81,69 +59,22 @@ class ActiveStorage::Variation
81
59
  end
82
60
 
83
61
  private
84
- def pass_transform_argument(command, method, argument)
85
- if eligible_argument?(argument)
86
- command.public_send(method, argument)
87
- else
88
- command.public_send(method)
89
- end
90
- end
91
-
92
- def eligible_argument?(argument)
93
- argument.present? && argument != true
94
- end
95
-
96
- def validate_transformation(name, argument)
97
- method_name = name.to_s.gsub("-","_")
98
-
99
- unless ActiveStorage.supported_image_processing_methods.any? { |method| method_name == method }
100
- raise UnsupportedImageProcessingMethod, <<~ERROR.squish
101
- One or more of the provided transformation methods is not supported.
102
- ERROR
103
- end
104
-
105
- if argument.present?
106
- if argument.is_a?(String) || argument.is_a?(Symbol)
107
- validate_arg_string(argument)
108
- elsif argument.is_a?(Array)
109
- validate_arg_array(argument)
110
- elsif argument.is_a?(Hash)
111
- validate_arg_hash(argument)
112
- end
113
- end
114
- end
115
-
116
- def validate_arg_string(argument)
117
- if ActiveStorage.unsupported_image_processing_arguments.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
118
- end
119
-
120
- def validate_arg_array(argument)
121
- argument.each do |arg|
122
- if arg.is_a?(Integer) || arg.is_a?(Float)
123
- next
124
- elsif arg.is_a?(String) || arg.is_a?(Symbol)
125
- validate_arg_string(arg)
126
- elsif arg.is_a?(Array)
127
- validate_arg_array(arg)
128
- elsif arg.is_a?(Hash)
129
- validate_arg_hash(arg)
130
- end
131
- end
132
- end
133
-
134
- def validate_arg_hash(argument)
135
- argument.each do |key, value|
136
- validate_arg_string(key)
137
-
138
- if value.is_a?(Integer) || value.is_a?(Float)
139
- next
140
- elsif value.is_a?(String) || value.is_a?(Symbol)
141
- validate_arg_string(value)
142
- elsif value.is_a?(Array)
143
- validate_arg_array(value)
144
- elsif value.is_a?(Hash)
145
- validate_arg_hash(value)
62
+ def transformer
63
+ if ActiveStorage.variant_processor
64
+ begin
65
+ require "image_processing"
66
+ rescue LoadError
67
+ ActiveSupport::Deprecation.warn <<~WARNING
68
+ Generating image variants will require the image_processing gem in Rails 6.1.
69
+ Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
70
+ WARNING
71
+
72
+ ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
73
+ else
74
+ ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
146
75
  end
76
+ else
77
+ ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
147
78
  end
148
79
  end
149
80
  end
data/config/routes.rb CHANGED
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
5
-
6
- direct :rails_blob do |blob, options|
7
- route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
8
- end
9
-
10
- resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
11
- resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
4
+ scope ActiveStorage.routes_prefix do
5
+ get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
12
6
 
7
+ get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
13
8
 
14
- get "/rails/active_storage/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
9
+ get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
10
+ put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
11
+ post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
12
+ end
15
13
 
16
14
  direct :rails_representation do |representation, options|
17
15
  signed_blob_id = representation.blob.signed_id
@@ -25,7 +23,10 @@ Rails.application.routes.draw do
25
23
  resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
26
24
 
27
25
 
28
- get "/rails/active_storage/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
29
- put "/rails/active_storage/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
30
- post "/rails/active_storage/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
26
+ direct :rails_blob do |blob, options|
27
+ route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
28
+ end
29
+
30
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
31
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
31
32
  end
@@ -0,0 +1,7 @@
1
+ class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
2
+ def up
3
+ unless foreign_key_exists?(:active_storage_attachments, column: :blob_id)
4
+ add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
5
+ end
6
+ end
7
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash/compact"
4
-
5
3
  module ActiveStorage
6
4
  # Extracts the following from a video blob:
7
5
  #
@@ -18,7 +16,7 @@ module ActiveStorage
18
16
  #
19
17
  # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
20
18
  #
21
- # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
19
+ # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
22
20
  class Analyzer::VideoAnalyzer < Analyzer
23
21
  def self.accept?(blob)
24
22
  blob.video?
@@ -109,7 +107,7 @@ module ActiveStorage
109
107
  JSON.parse(output.read)
110
108
  end
111
109
  rescue Errno::ENOENT
112
- logger.info "Skipping video analysis because ffmpeg isn't installed"
110
+ logger.info "Skipping video analysis because FFmpeg isn't installed"
113
111
  {}
114
112
  end
115
113
 
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage/downloading"
4
-
5
3
  module ActiveStorage
6
4
  # This is an abstract base class for analyzers, which extract metadata from blobs. See
7
5
  # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
8
6
  class Analyzer
9
- include Downloading
10
-
11
7
  attr_reader :blob
12
8
 
13
9
  # Implement this method in a concrete subclass. Have it return true when given a blob from which
@@ -26,8 +22,17 @@ module ActiveStorage
26
22
  end
27
23
 
28
24
  private
25
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
26
+ def download_blob_to_tempfile(&block) #:doc:
27
+ blob.open tempdir: tempdir, &block
28
+ end
29
+
29
30
  def logger #:doc:
30
31
  ActiveStorage.logger
31
32
  end
33
+
34
+ def tempdir #:doc:
35
+ Dir.tmpdir
36
+ end
32
37
  end
33
38
  end