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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -257
- data/MIT-LICENSE +1 -1
- data/README.md +25 -11
- data/app/assets/javascripts/activestorage.esm.js +856 -0
- data/app/assets/javascripts/activestorage.js +270 -377
- data/app/controllers/active_storage/base_controller.rb +1 -10
- data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
- data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
- data/app/controllers/active_storage/direct_uploads_controller.rb +7 -1
- data/app/controllers/active_storage/disk_controller.rb +1 -0
- data/app/controllers/active_storage/representations/base_controller.rb +5 -1
- data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
- data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
- data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
- data/app/controllers/concerns/active_storage/set_current.rb +3 -3
- data/app/controllers/concerns/active_storage/streaming.rb +65 -0
- data/app/javascript/activestorage/blob_record.js +10 -3
- data/app/javascript/activestorage/direct_upload.js +4 -2
- data/app/javascript/activestorage/direct_upload_controller.js +9 -1
- data/app/javascript/activestorage/ujs.js +1 -1
- data/app/models/active_storage/attachment.rb +35 -2
- data/app/models/active_storage/blob/representable.rb +7 -5
- data/app/models/active_storage/blob.rb +92 -36
- data/app/models/active_storage/current.rb +12 -2
- data/app/models/active_storage/preview.rb +6 -4
- data/app/models/active_storage/record.rb +1 -1
- data/app/models/active_storage/variant.rb +6 -9
- data/app/models/active_storage/variant_record.rb +2 -0
- data/app/models/active_storage/variant_with_record.rb +9 -5
- data/app/models/active_storage/variation.rb +3 -3
- data/config/routes.rb +10 -10
- data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +0 -4
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +0 -2
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
- data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
- data/lib/active_storage/analyzer.rb +8 -4
- data/lib/active_storage/attached/changes/create_many.rb +7 -3
- data/lib/active_storage/attached/changes/create_one.rb +1 -1
- data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_one.rb +1 -1
- data/lib/active_storage/attached/changes/detach_many.rb +18 -0
- data/lib/active_storage/attached/changes/detach_one.rb +24 -0
- data/lib/active_storage/attached/changes/purge_many.rb +27 -0
- data/lib/active_storage/attached/changes/purge_one.rb +27 -0
- data/lib/active_storage/attached/changes.rb +7 -1
- data/lib/active_storage/attached/many.rb +27 -15
- data/lib/active_storage/attached/model.rb +35 -7
- data/lib/active_storage/attached/one.rb +32 -27
- data/lib/active_storage/direct_upload_token.rb +59 -0
- data/lib/active_storage/downloader.rb +4 -4
- data/lib/active_storage/engine.rb +42 -16
- data/lib/active_storage/errors.rb +3 -0
- data/lib/active_storage/fixture_set.rb +76 -0
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/previewer/video_previewer.rb +0 -2
- data/lib/active_storage/previewer.rb +4 -4
- data/lib/active_storage/reflection.rb +12 -2
- data/lib/active_storage/service/azure_storage_service.rb +28 -6
- data/lib/active_storage/service/configurator.rb +1 -1
- data/lib/active_storage/service/disk_service.rb +24 -19
- data/lib/active_storage/service/gcs_service.rb +109 -11
- data/lib/active_storage/service/mirror_service.rb +2 -2
- data/lib/active_storage/service/registry.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +37 -15
- data/lib/active_storage/service.rb +13 -5
- data/lib/active_storage/transformers/image_processing_transformer.rb +1 -66
- data/lib/active_storage/transformers/transformer.rb +1 -1
- data/lib/active_storage.rb +6 -292
- metadata +30 -19
- 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
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
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)
|
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 }
|
@@ -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
|
-
# :
|
39
|
-
# root: Pathname("/foo/
|
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)
|
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
|
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:)
|
34
|
+
def process(file, format:) # :doc:
|
35
35
|
raise NotImplementedError
|
36
36
|
end
|
37
37
|
end
|