activestorage 6.0.5.1 → 6.1.7.3

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +223 -165
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +35 -3
  5. data/app/controllers/active_storage/base_controller.rb +11 -0
  6. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  7. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
  8. data/app/controllers/active_storage/direct_uploads_controller.rb +1 -1
  9. data/app/controllers/active_storage/disk_controller.rb +8 -20
  10. data/app/controllers/active_storage/representations/base_controller.rb +14 -0
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
  12. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -4
  13. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  14. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  15. data/app/controllers/concerns/active_storage/set_current.rb +2 -2
  16. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  17. data/app/jobs/active_storage/mirror_job.rb +15 -0
  18. data/app/models/active_storage/attachment.rb +19 -11
  19. data/app/models/active_storage/blob/analyzable.rb +6 -2
  20. data/app/models/active_storage/blob/identifiable.rb +7 -6
  21. data/app/models/active_storage/blob/representable.rb +34 -4
  22. data/app/models/active_storage/blob.rb +122 -57
  23. data/app/models/active_storage/preview.rb +31 -10
  24. data/app/models/active_storage/record.rb +7 -0
  25. data/app/models/active_storage/variant.rb +31 -44
  26. data/app/models/active_storage/variant_record.rb +8 -0
  27. data/app/models/active_storage/variant_with_record.rb +54 -0
  28. data/app/models/active_storage/variation.rb +26 -21
  29. data/config/routes.rb +58 -8
  30. data/db/migrate/20170806125915_create_active_storage_tables.rb +30 -9
  31. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +21 -0
  32. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +26 -0
  33. data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
  34. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  35. data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
  36. data/lib/active_storage/analyzer.rb +6 -0
  37. data/lib/active_storage/attached/changes/create_many.rb +1 -0
  38. data/lib/active_storage/attached/changes/create_one.rb +17 -4
  39. data/lib/active_storage/attached/many.rb +4 -3
  40. data/lib/active_storage/attached/model.rb +67 -14
  41. data/lib/active_storage/attached/one.rb +4 -3
  42. data/lib/active_storage/engine.rb +41 -43
  43. data/lib/active_storage/errors.rb +3 -0
  44. data/lib/active_storage/gem_version.rb +3 -3
  45. data/lib/active_storage/log_subscriber.rb +6 -0
  46. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  47. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  48. data/lib/active_storage/previewer/video_previewer.rb +5 -3
  49. data/lib/active_storage/previewer.rb +13 -3
  50. data/lib/active_storage/service/azure_storage_service.rb +40 -35
  51. data/lib/active_storage/service/configurator.rb +3 -1
  52. data/lib/active_storage/service/disk_service.rb +36 -31
  53. data/lib/active_storage/service/gcs_service.rb +18 -16
  54. data/lib/active_storage/service/mirror_service.rb +31 -7
  55. data/lib/active_storage/service/registry.rb +32 -0
  56. data/lib/active_storage/service/s3_service.rb +51 -23
  57. data/lib/active_storage/service.rb +35 -7
  58. data/lib/active_storage/transformers/image_processing_transformer.rb +21 -308
  59. data/lib/active_storage/transformers/transformer.rb +0 -3
  60. data/lib/active_storage.rb +301 -7
  61. data/lib/tasks/activestorage.rake +5 -1
  62. metadata +54 -17
  63. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
  64. data/lib/active_storage/downloading.rb +0 -47
  65. data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
@@ -14,80 +14,117 @@
14
14
  # Blobs are intended to be immutable in as-so-far as their reference to a specific file goes. You're allowed to
15
15
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
16
16
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
17
- class ActiveStorage::Blob < ActiveRecord::Base
18
- unless Rails.autoloaders.zeitwerk_enabled?
19
- require_dependency "active_storage/blob/analyzable"
20
- require_dependency "active_storage/blob/identifiable"
21
- require_dependency "active_storage/blob/representable"
22
- end
23
-
24
- include Analyzable
25
- include Identifiable
26
- include Representable
17
+ class ActiveStorage::Blob < ActiveStorage::Record
18
+ # We use constant paths in the following include calls to avoid a gotcha of
19
+ # classic mode: If the parent application defines a top-level Analyzable, for
20
+ # example, and ActiveStorage::Blob::Analyzable is not yet loaded, a bare
21
+ #
22
+ # include Analyzable
23
+ #
24
+ # would resolve to the top-level one, const_missing would not be triggered,
25
+ # and therefore ActiveStorage::Blob::Analyzable would not be autoloaded.
26
+ #
27
+ # By using qualified names, we ensure const_missing is invoked if needed.
28
+ # Please, note that Ruby 2.5 or newer is required, so Object is not checked
29
+ # when looking up the ancestors of ActiveStorage::Blob.
30
+ #
31
+ # Zeitwerk mode does not have this gotcha. If we ever drop classic mode, this
32
+ # can be simplified, bare constant names would just work.
33
+ include ActiveStorage::Blob::Analyzable
34
+ include ActiveStorage::Blob::Identifiable
35
+ include ActiveStorage::Blob::Representable
27
36
 
28
37
  self.table_name = "active_storage_blobs"
29
38
 
30
- has_secure_token :key
39
+ MINIMUM_TOKEN_LENGTH = 28
40
+
41
+ has_secure_token :key, length: MINIMUM_TOKEN_LENGTH
31
42
  store :metadata, accessors: [ :analyzed, :identified ], coder: ActiveRecord::Coders::JSON
32
43
 
33
- class_attribute :service
44
+ class_attribute :services, default: {}
45
+ class_attribute :service, instance_accessor: false
34
46
 
35
47
  has_many :attachments
36
48
 
37
- scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
49
+ scope :unattached, -> { where.missing(:attachments) }
50
+
51
+ after_initialize do
52
+ self.service_name ||= self.class.service&.name
53
+ end
54
+
55
+ after_update_commit :update_service_metadata, if: :content_type_previously_changed?
38
56
 
39
57
  before_destroy(prepend: true) do
40
58
  raise ActiveRecord::InvalidForeignKey if attachments.exists?
41
59
  end
42
60
 
61
+ validates :service_name, presence: true
62
+
63
+ validate do
64
+ if service_name_changed? && service_name.present?
65
+ services.fetch(service_name) do
66
+ errors.add(:service_name, :invalid)
67
+ end
68
+ end
69
+ end
70
+
43
71
  class << self
44
72
  # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
45
73
  # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
46
74
  # that was created ahead of the upload itself on form submission.
47
75
  #
48
76
  # The signed ID is also used to create stable URLs for the blob through the BlobsController.
49
- def find_signed(id)
50
- find ActiveStorage.verifier.verify(id, purpose: :blob_id)
77
+ def find_signed(id, record: nil, purpose: :blob_id)
78
+ super(id, purpose: purpose)
51
79
  end
52
80
 
53
- # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
54
- # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
55
- def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
56
- new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
81
+ # Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
82
+ # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
83
+ # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
84
+ # the valid signed id can't find a record.
85
+ def find_signed!(id, record: nil, purpose: :blob_id)
86
+ super(id, purpose: purpose)
87
+ end
88
+
89
+ def build_after_upload(io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
90
+ new(filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
57
91
  blob.upload(io, identify: identify)
58
92
  end
59
93
  end
60
94
 
61
- def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
62
- new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
95
+ deprecate :build_after_upload
96
+
97
+ def build_after_unfurling(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
98
+ new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob|
63
99
  blob.unfurl(io, identify: identify)
64
100
  end
65
101
  end
66
102
 
67
- def create_after_unfurling!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil) #:nodoc:
68
- build_after_unfurling(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!)
103
+ def create_after_unfurling!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil) #:nodoc:
104
+ build_after_unfurling(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap(&:save!)
69
105
  end
70
106
 
71
- # Creates a new blob instance and then uploads the contents of the given <tt>io</tt> to the
72
- # service. The blob instance is saved before the upload begins to avoid clobbering another due
73
- # to key collisions.
74
- #
75
- # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
76
- def create_and_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true, record: nil)
77
- create_after_unfurling!(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap do |blob|
107
+ # Creates a new blob instance and then uploads the contents of
108
+ # the given <tt>io</tt> to the service. The blob instance is going to
109
+ # be saved before the upload begins to prevent the upload clobbering another due to key collisions.
110
+ # When providing a content type, pass <tt>identify: false</tt> to bypass
111
+ # automatic content type inference.
112
+ def create_and_upload!(key: nil, io:, filename:, content_type: nil, metadata: nil, service_name: nil, identify: true, record: nil)
113
+ create_after_unfurling!(key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify).tap do |blob|
78
114
  blob.upload_without_unfurling(io)
79
115
  end
80
116
  end
81
117
 
82
118
  alias_method :create_after_upload!, :create_and_upload!
119
+ deprecate create_after_upload!: :create_and_upload!
83
120
 
84
121
  # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
85
122
  # no file yet. It's intended to be used together with a client-side upload, which will first create the blob
86
123
  # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob.
87
124
  # Once the form using the direct upload is submitted, the blob can be associated with the right record using
88
125
  # the signed ID.
89
- def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
90
- create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
126
+ def create_before_direct_upload!(key: nil, filename:, byte_size:, checksum:, content_type: nil, metadata: nil, service_name: nil, record: nil)
127
+ create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name
91
128
  end
92
129
 
93
130
  # To prevent problems with case-insensitive filesystems, especially in combination
@@ -95,15 +132,27 @@ class ActiveStorage::Blob < ActiveRecord::Base
95
132
  # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
96
133
  # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
97
134
  # the number of bytes used is increased to 28 from the standard 24
98
- def generate_unique_secure_token
99
- SecureRandom.base36(28)
135
+ def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
136
+ SecureRandom.base36(length)
137
+ end
138
+
139
+ # Customize signed ID purposes for backwards compatibility.
140
+ def combine_signed_id_purposes(purpose) #:nodoc:
141
+ purpose.to_s
142
+ end
143
+
144
+ # Customize the default signed ID verifier for backwards compatibility.
145
+ #
146
+ # We override the reader (.signed_id_verifier) instead of just calling the writer (.signed_id_verifier=)
147
+ # to guard against the case where ActiveStorage.verifier isn't yet initialized at load time.
148
+ def signed_id_verifier #:nodoc:
149
+ @signed_id_verifier ||= ActiveStorage.verifier
100
150
  end
101
151
  end
102
152
 
103
153
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
104
- # It uses the framework-wide verifier on <tt>ActiveStorage.verifier</tt>, but with a dedicated purpose.
105
154
  def signed_id
106
- ActiveStorage.verifier.generate(id, purpose: :blob_id)
155
+ super(purpose: :blob_id)
107
156
  end
108
157
 
109
158
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
@@ -112,7 +161,7 @@ class ActiveStorage::Blob < ActiveRecord::Base
112
161
  # Always refer to blobs using the signed_id or a verified form of the key.
113
162
  def key
114
163
  # We can't wait until the record is first saved to have a key for it
115
- self[:key] ||= self.class.generate_unique_secure_token
164
+ self[:key] ||= self.class.generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
116
165
  end
117
166
 
118
167
  # Returns an ActiveStorage::Filename instance of the filename that can be
@@ -142,18 +191,18 @@ class ActiveStorage::Blob < ActiveRecord::Base
142
191
  content_type.start_with?("text")
143
192
  end
144
193
 
145
-
146
- # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly
147
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
148
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
149
- # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
150
- def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
151
- filename = ActiveStorage::Filename.wrap(filename || self.filename)
152
-
153
- service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
154
- disposition: forced_disposition_for_service_url || disposition, **options
194
+ # Returns the URL of the blob on the service. This returns a permanent URL for public files, and returns a
195
+ # short-lived URL for private files. Private files are signed, and not for public use. Instead,
196
+ # the URL should only be exposed as a redirect from a stable, possibly authenticated URL. Hiding the
197
+ # URL behind a redirect also allows you to change services without updating all URLs.
198
+ def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
199
+ service.url key, expires_in: expires_in, filename: ActiveStorage::Filename.wrap(filename || self.filename),
200
+ content_type: content_type_for_serving, disposition: forced_disposition_for_serving || disposition, **options
155
201
  end
156
202
 
203
+ alias_method :service_url, :url
204
+ deprecate service_url: :url
205
+
157
206
  # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
158
207
  # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
159
208
  def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
@@ -165,6 +214,16 @@ class ActiveStorage::Blob < ActiveRecord::Base
165
214
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum
166
215
  end
167
216
 
217
+ def content_type_for_serving #:nodoc:
218
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
219
+ end
220
+
221
+ def forced_disposition_for_serving #:nodoc:
222
+ if forcibly_serve_as_binary? || !allowed_inline?
223
+ :attachment
224
+ end
225
+ end
226
+
168
227
 
169
228
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
170
229
  # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
@@ -218,6 +277,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
218
277
  name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &block
219
278
  end
220
279
 
280
+ def mirror_later #:nodoc:
281
+ ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
282
+ end
221
283
 
222
284
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
223
285
  # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
@@ -227,8 +289,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
227
289
  service.delete_prefixed("variants/#{key}/") if image?
228
290
  end
229
291
 
230
- # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
231
- # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
292
+ # Destroys the blob record and then deletes the file on the service. This is the recommended way to dispose of unwanted
293
+ # blobs. Note, though, that deleting the file off the service will initiate an HTTP connection to the service, which may
232
294
  # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
233
295
  def purge
234
296
  destroy
@@ -242,6 +304,11 @@ class ActiveStorage::Blob < ActiveRecord::Base
242
304
  ActiveStorage::PurgeJob.perform_later(self)
243
305
  end
244
306
 
307
+ # Returns an instance of service, which can be configured globally or per attachment
308
+ def service
309
+ services.fetch(service_name)
310
+ end
311
+
245
312
  private
246
313
  def compute_checksum_in_chunks(io)
247
314
  Digest::MD5.new.tap do |checksum|
@@ -265,14 +332,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
265
332
  ActiveStorage.content_types_allowed_inline.include?(content_type)
266
333
  end
267
334
 
268
- def content_type_for_service_url
269
- forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
270
- end
271
-
272
- def forced_disposition_for_service_url
273
- if forcibly_serve_as_binary? || !allowed_inline?
274
- :attachment
275
- end
335
+ def web_image?
336
+ ActiveStorage.web_image_content_types.include?(content_type)
276
337
  end
277
338
 
278
339
  def service_metadata
@@ -284,6 +345,10 @@ class ActiveStorage::Blob < ActiveRecord::Base
284
345
  { content_type: content_type }
285
346
  end
286
347
  end
348
+
349
+ def update_service_metadata
350
+ service.update_metadata key, **service_metadata if service_metadata.any?
351
+ end
287
352
  end
288
353
 
289
354
  ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob
@@ -3,8 +3,9 @@
3
3
  # Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
4
4
  # extracting its first frame, and a PDF blob can be previewed by extracting its first page.
5
5
  #
6
- # A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
7
- # ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
6
+ # A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs.
7
+ # ActiveStorage::Previewer::VideoPreviewer is used for videos whereas ActiveStorage::Previewer::PopplerPDFPreviewer
8
+ # and ActiveStorage::Previewer::MuPDFPreviewer are used for PDFs. Build custom previewers by
8
9
  # subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
9
10
  # documentation for more details on what's required of previewers.
10
11
  #
@@ -13,11 +14,11 @@
13
14
  # by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
14
15
  #
15
16
  # Rails.application.config.active_storage.previewers
16
- # # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
17
+ # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
17
18
  #
18
19
  # # Add a custom previewer for Microsoft Office documents:
19
20
  # Rails.application.config.active_storage.previewers << DOCXPreviewer
20
- # # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
21
+ # # => [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
21
22
  #
22
23
  # Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
23
24
  #
@@ -38,7 +39,7 @@ class ActiveStorage::Preview
38
39
 
39
40
  # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
40
41
  #
41
- # blob.preview(resize_to_limit: [100, 100]).processed.service_url
42
+ # blob.preview(resize_to_limit: [100, 100]).processed.url
42
43
  #
43
44
  # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
44
45
  # image is stored with the blob, it is only generated once.
@@ -56,10 +57,30 @@ class ActiveStorage::Preview
56
57
  # preview has not been processed yet.
57
58
  #
58
59
  # This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
59
- # a stable URL that redirects to the short-lived URL returned by this method.
60
- def service_url(**options)
60
+ # a stable URL that redirects to the URL returned by this method.
61
+ def url(**options)
61
62
  if processed?
62
- variant.service_url(**options)
63
+ variant.url(**options)
64
+ else
65
+ raise UnprocessedError
66
+ end
67
+ end
68
+
69
+ alias_method :service_url, :url
70
+ deprecate service_url: :url
71
+
72
+ # Returns a combination key of the blob and the variation that together identifies a specific variant.
73
+ def key
74
+ if processed?
75
+ variant.key
76
+ else
77
+ raise UnprocessedError
78
+ end
79
+ end
80
+
81
+ def download(&block)
82
+ if processed?
83
+ variant.download(&block)
63
84
  else
64
85
  raise UnprocessedError
65
86
  end
@@ -71,7 +92,7 @@ class ActiveStorage::Preview
71
92
  end
72
93
 
73
94
  def process
74
- previewer.preview do |attachable|
95
+ previewer.preview(service_name: blob.service_name) do |attachable|
75
96
  ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
76
97
  image.attach(attachable)
77
98
  end
@@ -79,7 +100,7 @@ class ActiveStorage::Preview
79
100
  end
80
101
 
81
102
  def variant
82
- ActiveStorage::Variant.new(image, variation).processed
103
+ image.variant(variation).processed
83
104
  end
84
105
 
85
106
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::Record < ActiveRecord::Base #:nodoc:
4
+ self.abstract_class = true
5
+ end
6
+
7
+ ActiveSupport.run_load_hooks :active_storage_record, ActiveStorage::Record
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
4
-
5
3
  # Image blobs can have variants that are the result of a set of transformations applied to the original.
6
4
  # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
7
5
  # original.
8
6
  #
9
- # Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
7
+ # Variants rely on {ImageProcessing}[https://github.com/janko/image_processing] gem for the actual transformations
10
8
  # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
11
9
  # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
12
10
  # {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]
11
+ # {libvips}[http://libvips.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/libvips/ruby-vips]
14
12
  # gem).
15
13
  #
16
14
  # Rails.application.config.active_storage.variant_processor
@@ -36,7 +34,7 @@ require "ostruct"
36
34
  # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
37
35
  # the transformations, upload the variant to the service, and return itself again. Example:
38
36
  #
39
- # avatar.variant(resize_to_limit: [100, 100]).processed.service_url
37
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
40
38
  #
41
39
  # This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
42
40
  # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
@@ -48,15 +46,14 @@ require "ostruct"
48
46
  #
49
47
  # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
50
48
  #
51
- # * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
49
+ # * {ImageProcessing::MiniMagick}[https://github.com/janko/image_processing/blob/master/doc/minimagick.md#methods]
52
50
  # * {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]
51
+ # * {ImageProcessing::Vips}[https://github.com/janko/image_processing/blob/master/doc/vips.md#methods]
54
52
  # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
55
53
  class ActiveStorage::Variant
56
- WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ]
57
-
58
54
  attr_reader :blob, :variation
59
55
  delegate :service, to: :blob
56
+ delegate :content_type, to: :variation
60
57
 
61
58
  def initialize(blob, variation_or_variation_key)
62
59
  @blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
@@ -73,18 +70,34 @@ class ActiveStorage::Variant
73
70
  "variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
74
71
  end
75
72
 
76
- # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
77
- # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
78
- # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
79
- # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
73
+ # Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
80
74
  #
81
75
  # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
82
76
  # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
83
77
  # for its redirection.
84
- def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
78
+ def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
85
79
  service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
86
80
  end
87
81
 
82
+ alias_method :service_url, :url
83
+ deprecate service_url: :url
84
+
85
+ # Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
86
+ # That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
87
+ def download(&block)
88
+ service.download key, &block
89
+ end
90
+
91
+ def filename
92
+ ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
93
+ end
94
+
95
+ alias_method :content_type_for_serving, :content_type
96
+
97
+ def forced_disposition_for_serving #:nodoc:
98
+ nil
99
+ end
100
+
88
101
  # Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
89
102
  def image
90
103
  self
@@ -96,36 +109,10 @@ class ActiveStorage::Variant
96
109
  end
97
110
 
98
111
  def process
99
- blob.open do |image|
100
- transform(image) { |output| upload(output) }
101
- end
102
- end
103
-
104
- def transform(image, &block)
105
- variation.transform(image, format: format, &block)
106
- end
107
-
108
- def upload(file)
109
- service.upload(key, file)
110
- end
111
-
112
-
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"
112
+ blob.open do |input|
113
+ variation.transform(input) do |output|
114
+ service.upload(key, output, content_type: content_type)
125
115
  end
116
+ end
126
117
  end
127
-
128
- delegate :filename, :content_type, :format, to: :specification
129
-
130
- class Specification < OpenStruct; end
131
118
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::VariantRecord < ActiveStorage::Record
4
+ self.table_name = "active_storage_variant_records"
5
+
6
+ belongs_to :blob
7
+ has_one_attached :image
8
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::VariantWithRecord
4
+ attr_reader :blob, :variation
5
+
6
+ def initialize(blob, variation)
7
+ @blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
8
+ end
9
+
10
+ def processed
11
+ process
12
+ self
13
+ end
14
+
15
+ def process
16
+ transform_blob { |image| create_or_find_record(image: image) } unless processed?
17
+ end
18
+
19
+ def processed?
20
+ record.present?
21
+ end
22
+
23
+ def image
24
+ record&.image
25
+ end
26
+
27
+ delegate :key, :url, :download, to: :image, allow_nil: true
28
+
29
+ alias_method :service_url, :url
30
+ deprecate service_url: :url
31
+
32
+ private
33
+ def transform_blob
34
+ blob.open do |input|
35
+ variation.transform(input) do |output|
36
+ yield io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
37
+ content_type: variation.content_type, service_name: blob.service.name
38
+ end
39
+ end
40
+ end
41
+
42
+ def create_or_find_record(image:)
43
+ @record =
44
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
45
+ blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
46
+ record.image.attach(image)
47
+ end
48
+ end
49
+ end
50
+
51
+ def record
52
+ @record ||= blob.variant_records.find_by(variation_digest: variation.digest)
53
+ end
54
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "mini_mime"
4
+
3
5
  # A set of transformations that can be applied to a blob to create a variant. This class is exposed via
4
6
  # the ActiveStorage::Blob#variant method and should rarely be used directly.
5
7
  #
@@ -8,7 +10,7 @@
8
10
  #
9
11
  # ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
10
12
  #
11
- # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
13
+ # The options map directly to {ImageProcessing}[https://github.com/janko/image_processing] commands.
12
14
  class ActiveStorage::Variation
13
15
  attr_reader :transformations
14
16
 
@@ -43,38 +45,41 @@ class ActiveStorage::Variation
43
45
  @transformations = transformations.deep_symbolize_keys
44
46
  end
45
47
 
48
+ def default_to(defaults)
49
+ self.class.new transformations.reverse_merge(defaults)
50
+ end
51
+
46
52
  # 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)
53
+ # saves the transformed image into a temporary file.
54
+ def transform(file, &block)
51
55
  ActiveSupport::Notifications.instrument("transform.active_storage") do
52
56
  transformer.transform(file, format: format, &block)
53
57
  end
54
58
  end
55
59
 
60
+ def format
61
+ transformations.fetch(:format, :png).tap do |format|
62
+ if MiniMime.lookup_by_extension(format.to_s).nil?
63
+ raise ArgumentError, "Invalid variant format (#{format.inspect})"
64
+ end
65
+ end
66
+ end
67
+
68
+ def content_type
69
+ MiniMime.lookup_by_extension(format.to_s).content_type
70
+ end
71
+
56
72
  # Returns a signed key for all the +transformations+ that this variation was instantiated with.
57
73
  def key
58
74
  self.class.encode(transformations)
59
75
  end
60
76
 
77
+ def digest
78
+ Digest::SHA1.base64digest Marshal.dump(transformations)
79
+ end
80
+
61
81
  private
62
82
  def transformer
63
- if ActiveStorage.variant_processor
64
- begin
65
- require "image_processing"
66
- rescue LoadError
67
- ActiveSupport::Deprecation.warn <<~WARNING.squish
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)
75
- end
76
- else
77
- ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
78
- end
83
+ ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations.except(:format))
79
84
  end
80
85
  end