activestorage 6.1.7 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -257
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +856 -0
  6. data/app/assets/javascripts/activestorage.js +270 -377
  7. data/app/controllers/active_storage/base_controller.rb +1 -10
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/direct_uploads_controller.rb +7 -1
  11. data/app/controllers/active_storage/disk_controller.rb +1 -0
  12. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  13. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
  14. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  15. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  16. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  17. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  18. data/app/javascript/activestorage/blob_record.js +10 -3
  19. data/app/javascript/activestorage/direct_upload.js +4 -2
  20. data/app/javascript/activestorage/direct_upload_controller.js +9 -1
  21. data/app/javascript/activestorage/ujs.js +1 -1
  22. data/app/models/active_storage/attachment.rb +35 -2
  23. data/app/models/active_storage/blob/representable.rb +7 -5
  24. data/app/models/active_storage/blob.rb +92 -36
  25. data/app/models/active_storage/current.rb +12 -2
  26. data/app/models/active_storage/preview.rb +6 -4
  27. data/app/models/active_storage/record.rb +1 -1
  28. data/app/models/active_storage/variant.rb +6 -9
  29. data/app/models/active_storage/variant_record.rb +2 -0
  30. data/app/models/active_storage/variant_with_record.rb +9 -5
  31. data/app/models/active_storage/variation.rb +3 -3
  32. data/config/routes.rb +10 -10
  33. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  34. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +0 -4
  35. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +0 -2
  36. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
  37. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  38. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  39. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  40. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  41. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  42. data/lib/active_storage/analyzer.rb +8 -4
  43. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  44. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  45. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  46. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  47. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  48. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  49. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  50. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  51. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  52. data/lib/active_storage/attached/changes.rb +7 -1
  53. data/lib/active_storage/attached/many.rb +27 -15
  54. data/lib/active_storage/attached/model.rb +35 -7
  55. data/lib/active_storage/attached/one.rb +32 -27
  56. data/lib/active_storage/direct_upload_token.rb +59 -0
  57. data/lib/active_storage/downloader.rb +4 -4
  58. data/lib/active_storage/engine.rb +42 -16
  59. data/lib/active_storage/errors.rb +3 -0
  60. data/lib/active_storage/fixture_set.rb +76 -0
  61. data/lib/active_storage/gem_version.rb +3 -3
  62. data/lib/active_storage/previewer/video_previewer.rb +0 -2
  63. data/lib/active_storage/previewer.rb +4 -4
  64. data/lib/active_storage/reflection.rb +12 -2
  65. data/lib/active_storage/service/azure_storage_service.rb +28 -6
  66. data/lib/active_storage/service/configurator.rb +1 -1
  67. data/lib/active_storage/service/disk_service.rb +24 -19
  68. data/lib/active_storage/service/gcs_service.rb +109 -11
  69. data/lib/active_storage/service/mirror_service.rb +2 -2
  70. data/lib/active_storage/service/registry.rb +1 -1
  71. data/lib/active_storage/service/s3_service.rb +37 -15
  72. data/lib/active_storage/service.rb +13 -5
  73. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -66
  74. data/lib/active_storage/transformers/transformer.rb +1 -1
  75. data/lib/active_storage.rb +6 -292
  76. metadata +30 -19
  77. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -1,25 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  gem "google-cloud-storage", "~> 1.11"
4
+ require "google/apis/iamcredentials_v1"
4
5
  require "google/cloud/storage"
5
6
 
6
7
  module ActiveStorage
7
8
  # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
8
9
  # documentation that applies to all services.
9
10
  class Service::GCSService < Service
11
+ class MetadataServerError < ActiveStorage::Error; end
12
+ class MetadataServerNotFoundError < ActiveStorage::Error; end
13
+
10
14
  def initialize(public: false, **config)
11
15
  @config = config
12
16
  @public = public
13
17
  end
14
18
 
15
- def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
19
+ def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})
16
20
  instrument :upload, key: key, checksum: checksum do
17
21
  # GCS's signed URLs don't include params such as response-content-type response-content_disposition
18
22
  # in the signature, which means an attacker can modify them and bypass our effort to force these to
19
23
  # binary and attachment when the file's content type requires it. The only way to force them is to
20
24
  # store them as object's metadata.
21
25
  content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
22
- bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
26
+ bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
23
27
  rescue Google::Cloud::InvalidArgumentError
24
28
  raise ActiveStorage::IntegrityError
25
29
  end
@@ -39,11 +43,12 @@ module ActiveStorage
39
43
  end
40
44
  end
41
45
 
42
- def update_metadata(key, content_type:, disposition: nil, filename: nil)
46
+ def update_metadata(key, content_type:, disposition: nil, filename: nil, custom_metadata: {})
43
47
  instrument :update_metadata, key: key, content_type: content_type, disposition: disposition do
44
48
  file_for(key).update do |file|
45
49
  file.content_type = content_type
46
50
  file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
51
+ file.metadata = custom_metadata
47
52
  end
48
53
  end
49
54
  end
@@ -82,9 +87,35 @@ module ActiveStorage
82
87
  end
83
88
  end
84
89
 
85
- def url_for_direct_upload(key, expires_in:, checksum:, **)
90
+ def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
86
91
  instrument :url, key: key do |payload|
87
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
92
+ headers = {}
93
+ version = :v2
94
+
95
+ if @config[:cache_control].present?
96
+ headers["Cache-Control"] = @config[:cache_control]
97
+ # v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
98
+ # if necessary for back-compat; v4 limits the expiration of the URL to 7 days
99
+ # whereas v2 has no limit
100
+ version = :v4
101
+ end
102
+
103
+ headers.merge!(custom_metadata_headers(custom_metadata))
104
+
105
+ args = {
106
+ content_md5: checksum,
107
+ expires: expires_in,
108
+ headers: headers,
109
+ method: "PUT",
110
+ version: version,
111
+ }
112
+
113
+ if @config[:iam]
114
+ args[:issuer] = issuer
115
+ args[:signer] = signer
116
+ end
117
+
118
+ generated_url = bucket.signed_url(key, **args)
88
119
 
89
120
  payload[:url] = generated_url
90
121
 
@@ -92,18 +123,41 @@ module ActiveStorage
92
123
  end
93
124
  end
94
125
 
95
- def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, **)
126
+ def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
96
127
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
97
128
 
98
- { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
129
+ headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
130
+ if @config[:cache_control].present?
131
+ headers["Cache-Control"] = @config[:cache_control]
132
+ end
133
+
134
+ headers
135
+ end
136
+
137
+ def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
138
+ bucket.compose(source_keys, destination_key).update do |file|
139
+ file.content_type = content_type
140
+ file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
141
+ file.metadata = custom_metadata
142
+ end
99
143
  end
100
144
 
101
145
  private
102
146
  def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
103
- file_for(key).signed_url expires: expires_in, query: {
104
- "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
105
- "response-content-type" => content_type
147
+ args = {
148
+ expires: expires_in,
149
+ query: {
150
+ "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
151
+ "response-content-type" => content_type
152
+ }
106
153
  }
154
+
155
+ if @config[:iam]
156
+ args[:issuer] = issuer
157
+ args[:signer] = signer
158
+ end
159
+
160
+ file_for(key).signed_url(**args)
107
161
  end
108
162
 
109
163
  def public_url(key, **)
@@ -137,7 +191,51 @@ module ActiveStorage
137
191
  end
138
192
 
139
193
  def client
140
- @client ||= Google::Cloud::Storage.new(**config.except(:bucket))
194
+ @client ||= Google::Cloud::Storage.new(**config.except(:bucket, :cache_control, :iam, :gsa_email))
195
+ end
196
+
197
+ def issuer
198
+ @issuer ||= if @config[:gsa_email]
199
+ @config[:gsa_email]
200
+ else
201
+ uri = URI.parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email")
202
+ http = Net::HTTP.new(uri.host, uri.port)
203
+ request = Net::HTTP::Get.new(uri.request_uri)
204
+ request["Metadata-Flavor"] = "Google"
205
+
206
+ begin
207
+ response = http.request(request)
208
+ rescue SocketError
209
+ raise MetadataServerNotFoundError
210
+ end
211
+
212
+ if response.is_a?(Net::HTTPSuccess)
213
+ response.body
214
+ else
215
+ raise MetadataServerError
216
+ end
217
+ end
218
+ end
219
+
220
+ def signer
221
+ # https://googleapis.dev/ruby/google-cloud-storage/latest/Google/Cloud/Storage/Project.html#signed_url-instance_method
222
+ lambda do |string_to_sign|
223
+ iam_client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new
224
+
225
+ scopes = ["https://www.googleapis.com/auth/iam"]
226
+ iam_client.authorization = Google::Auth.get_application_default(scopes)
227
+
228
+ request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
229
+ payload: string_to_sign
230
+ )
231
+ resource = "projects/-/serviceAccounts/#{issuer}"
232
+ response = iam_client.sign_service_account_blob(resource, request)
233
+ response.signed_blob
234
+ end
235
+ end
236
+
237
+ def custom_metadata_headers(metadata)
238
+ metadata.transform_keys { |key| "x-goog-meta-#{key}" }
141
239
  end
142
240
  end
143
241
  end
@@ -14,10 +14,10 @@ module ActiveStorage
14
14
  attr_reader :primary, :mirrors
15
15
 
16
16
  delegate :download, :download_chunk, :exist?, :url,
17
- :url_for_direct_upload, :headers_for_direct_upload, :path_for, to: :primary
17
+ :url_for_direct_upload, :headers_for_direct_upload, :path_for, :compose, to: :primary
18
18
 
19
19
  # Stitch together from named services.
20
- def self.build(primary:, mirrors:, name:, configurator:, **options) #:nodoc:
20
+ def self.build(primary:, mirrors:, name:, configurator:, **options) # :nodoc:
21
21
  new(
22
22
  primary: configurator.build(primary),
23
23
  mirrors: mirrors.collect { |mirror_name| configurator.build mirror_name }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Service::Registry #:nodoc:
4
+ class Service::Registry # :nodoc:
5
5
  def initialize(configurations)
6
6
  @configurations = configurations.deep_symbolize_keys
7
7
  @services = {}
@@ -23,14 +23,14 @@ module ActiveStorage
23
23
  @upload_options[:acl] = "public-read" if public?
24
24
  end
25
25
 
26
- def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
26
+ def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
27
27
  instrument :upload, key: key, checksum: checksum do
28
28
  content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
29
29
 
30
30
  if io.size < multipart_upload_threshold
31
- upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition
31
+ upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition, custom_metadata: custom_metadata
32
32
  else
33
- upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition
33
+ upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition, custom_metadata: custom_metadata
34
34
  end
35
35
  end
36
36
  end
@@ -77,11 +77,11 @@ module ActiveStorage
77
77
  end
78
78
  end
79
79
 
80
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
80
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
81
81
  instrument :url, key: key do |payload|
82
82
  generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
83
83
  content_type: content_type, content_length: content_length, content_md5: checksum,
84
- whitelist_headers: ["content-length"], **upload_options
84
+ metadata: custom_metadata, whitelist_headers: ["content-length"], **upload_options
85
85
 
86
86
  payload[:url] = generated_url
87
87
 
@@ -89,37 +89,55 @@ module ActiveStorage
89
89
  end
90
90
  end
91
91
 
92
- def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
92
+ def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
93
93
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
94
94
 
95
- { "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
95
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
96
+ end
97
+
98
+ def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
99
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
100
+
101
+ object_for(destination_key).upload_stream(
102
+ content_type: content_type,
103
+ content_disposition: content_disposition,
104
+ part_size: MINIMUM_UPLOAD_PART_SIZE,
105
+ metadata: custom_metadata,
106
+ **upload_options
107
+ ) do |out|
108
+ source_keys.each do |source_key|
109
+ stream(source_key) do |chunk|
110
+ IO.copy_stream(StringIO.new(chunk), out)
111
+ end
112
+ end
113
+ end
96
114
  end
97
115
 
98
116
  private
99
- def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
117
+ def private_url(key, expires_in:, filename:, disposition:, content_type:, **client_opts)
100
118
  object_for(key).presigned_url :get, expires_in: expires_in.to_i,
101
119
  response_content_disposition: content_disposition_with(type: disposition, filename: filename),
102
- response_content_type: content_type
120
+ response_content_type: content_type, **client_opts
103
121
  end
104
122
 
105
- def public_url(key, **)
106
- object_for(key).public_url
123
+ def public_url(key, **client_opts)
124
+ object_for(key).public_url(**client_opts)
107
125
  end
108
126
 
109
127
 
110
128
  MAXIMUM_UPLOAD_PARTS_COUNT = 10000
111
129
  MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
112
130
 
113
- def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil)
114
- object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, **upload_options)
131
+ def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil, custom_metadata: {})
132
+ object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata, **upload_options)
115
133
  rescue Aws::S3::Errors::BadDigest
116
134
  raise ActiveStorage::IntegrityError
117
135
  end
118
136
 
119
- def upload_with_multipart(key, io, content_type: nil, content_disposition: nil)
137
+ def upload_with_multipart(key, io, content_type: nil, content_disposition: nil, custom_metadata: {})
120
138
  part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
121
139
 
122
- object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, **upload_options) do |out|
140
+ object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, metadata: custom_metadata, **upload_options) do |out|
123
141
  IO.copy_stream(io, out)
124
142
  end
125
143
  end
@@ -143,5 +161,9 @@ module ActiveStorage
143
161
  offset += chunk_size
144
162
  end
145
163
  end
164
+
165
+ def custom_metadata_headers(metadata)
166
+ metadata.transform_keys { |key| "x-amz-meta-#{key}" }
167
+ end
146
168
  end
147
169
  end
@@ -35,8 +35,8 @@ module ActiveStorage
35
35
  # can configure the service to use like this:
36
36
  #
37
37
  # ActiveStorage::Blob.service = ActiveStorage::Service.configure(
38
- # :Disk,
39
- # root: Pathname("/foo/bar/storage")
38
+ # :local,
39
+ # { local: {service: "Disk", root: Pathname("/tmp/foo/storage") } }
40
40
  # )
41
41
  class Service
42
42
  extend ActiveSupport::Autoload
@@ -57,7 +57,7 @@ module ActiveStorage
57
57
  # Passes the configurator and all of the service's config as keyword args.
58
58
  #
59
59
  # See MirrorService for an example.
60
- def build(configurator:, name:, service: nil, **service_config) #:nodoc:
60
+ def build(configurator:, name:, service: nil, **service_config) # :nodoc:
61
61
  new(**service_config).tap do |service_instance|
62
62
  service_instance.name = name
63
63
  end
@@ -90,6 +90,11 @@ module ActiveStorage
90
90
  ActiveStorage::Downloader.new(self).open(*args, **options, &block)
91
91
  end
92
92
 
93
+ # Concatenate multiple files into a single "composed" file.
94
+ def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
95
+ raise NotImplementedError
96
+ end
97
+
93
98
  # Delete the file at the +key+.
94
99
  def delete(key)
95
100
  raise NotImplementedError
@@ -128,12 +133,12 @@ module ActiveStorage
128
133
  # The URL will be valid for the amount of seconds specified in +expires_in+.
129
134
  # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
130
135
  # that will be uploaded. All these attributes will be validated by the service upon upload.
131
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
136
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
132
137
  raise NotImplementedError
133
138
  end
134
139
 
135
140
  # Returns a Hash of headers for +url_for_direct_upload+ requests.
136
- def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
141
+ def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:, custom_metadata: {})
137
142
  {}
138
143
  end
139
144
 
@@ -150,6 +155,9 @@ module ActiveStorage
150
155
  raise NotImplementedError
151
156
  end
152
157
 
158
+ def custom_metadata_headers(metadata)
159
+ raise NotImplementedError
160
+ end
153
161
 
154
162
  def instrument(operation, payload = {}, &block)
155
163
  ActiveSupport::Notifications.instrument(
@@ -13,9 +13,6 @@ module ActiveStorage
13
13
  module Transformers
14
14
  class ImageProcessingTransformer < Transformer
15
15
  private
16
- class UnsupportedImageProcessingMethod < StandardError; end
17
- class UnsupportedImageProcessingArgument < StandardError; end
18
-
19
16
  def process(file, format:)
20
17
  processor.
21
18
  source(file).
@@ -31,14 +28,10 @@ module ActiveStorage
31
28
 
32
29
  def operations
33
30
  transformations.each_with_object([]) do |(name, argument), list|
34
- if ActiveStorage.variant_processor == :mini_magick
35
- validate_transformation(name, argument)
36
- end
37
-
38
31
  if name.to_s == "combine_options"
39
32
  raise ArgumentError, <<~ERROR.squish
40
33
  Active Storage's ImageProcessing transformer doesn't support :combine_options,
41
- as it always generates a single ImageMagick command.
34
+ as it always generates a single command.
42
35
  ERROR
43
36
  end
44
37
 
@@ -47,64 +40,6 @@ module ActiveStorage
47
40
  end
48
41
  end
49
42
  end
50
-
51
- def validate_transformation(name, argument)
52
- method_name = name.to_s.tr("-", "_")
53
-
54
- unless ActiveStorage.supported_image_processing_methods.any? { |method| method_name == method }
55
- raise UnsupportedImageProcessingMethod, <<~ERROR.squish
56
- One or more of the provided transformation methods is not supported.
57
- ERROR
58
- end
59
-
60
- if argument.present?
61
- if argument.is_a?(String) || argument.is_a?(Symbol)
62
- validate_arg_string(argument)
63
- elsif argument.is_a?(Array)
64
- validate_arg_array(argument)
65
- elsif argument.is_a?(Hash)
66
- validate_arg_hash(argument)
67
- end
68
- end
69
- end
70
-
71
- def validate_arg_string(argument)
72
- unsupported_arguments = ActiveStorage.unsupported_image_processing_arguments.any? do |bad_arg|
73
- argument.to_s.downcase.include?(bad_arg)
74
- end
75
-
76
- raise UnsupportedImageProcessingArgument if unsupported_arguments
77
- end
78
-
79
- def validate_arg_array(argument)
80
- argument.each do |arg|
81
- if arg.is_a?(Integer) || arg.is_a?(Float)
82
- next
83
- elsif arg.is_a?(String) || arg.is_a?(Symbol)
84
- validate_arg_string(arg)
85
- elsif arg.is_a?(Array)
86
- validate_arg_array(arg)
87
- elsif arg.is_a?(Hash)
88
- validate_arg_hash(arg)
89
- end
90
- end
91
- end
92
-
93
- def validate_arg_hash(argument)
94
- argument.each do |key, value|
95
- validate_arg_string(key)
96
-
97
- if value.is_a?(Integer) || value.is_a?(Float)
98
- next
99
- elsif value.is_a?(String) || value.is_a?(Symbol)
100
- validate_arg_string(value)
101
- elsif value.is_a?(Array)
102
- validate_arg_array(value)
103
- elsif value.is_a?(Hash)
104
- validate_arg_hash(value)
105
- end
106
- end
107
- end
108
43
  end
109
44
  end
110
45
  end
@@ -31,7 +31,7 @@ module ActiveStorage
31
31
  private
32
32
  # Returns an open Tempfile containing a transformed image in the given +format+.
33
33
  # All subclasses implement this method.
34
- def process(file, format:) #:doc:
34
+ def process(file, format:) # :doc:
35
35
  raise NotImplementedError
36
36
  end
37
37
  end