activestorage 6.1.7 → 7.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -276
- data/MIT-LICENSE +1 -1
- data/README.md +29 -15
- data/app/assets/javascripts/activestorage.esm.js +848 -0
- data/app/assets/javascripts/activestorage.js +263 -376
- data/app/controllers/active_storage/base_controller.rb +0 -9
- data/app/controllers/active_storage/blobs/proxy_controller.rb +16 -4
- data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
- data/app/controllers/active_storage/disk_controller.rb +5 -2
- data/app/controllers/active_storage/representations/base_controller.rb +5 -1
- data/app/controllers/active_storage/representations/proxy_controller.rb +8 -3
- data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
- data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
- data/app/controllers/concerns/active_storage/file_server.rb +4 -1
- 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 +66 -0
- data/app/javascript/activestorage/blob_record.js +4 -1
- data/app/javascript/activestorage/direct_upload.js +3 -2
- data/app/javascript/activestorage/index.js +3 -1
- data/app/javascript/activestorage/ujs.js +1 -1
- data/app/jobs/active_storage/analyze_job.rb +1 -1
- data/app/jobs/active_storage/mirror_job.rb +1 -1
- data/app/jobs/active_storage/purge_job.rb +1 -1
- data/app/jobs/active_storage/transform_job.rb +12 -0
- data/app/models/active_storage/attachment.rb +111 -4
- data/app/models/active_storage/blob/analyzable.rb +4 -3
- data/app/models/active_storage/blob/identifiable.rb +1 -0
- data/app/models/active_storage/blob/representable.rb +14 -8
- data/app/models/active_storage/blob.rb +93 -57
- data/app/models/active_storage/current.rb +2 -2
- data/app/models/active_storage/filename.rb +2 -0
- data/app/models/active_storage/named_variant.rb +21 -0
- data/app/models/active_storage/preview.rb +11 -7
- data/app/models/active_storage/record.rb +1 -1
- data/app/models/active_storage/variant.rb +10 -12
- data/app/models/active_storage/variant_record.rb +2 -0
- data/app/models/active_storage/variant_with_record.rb +28 -12
- data/app/models/active_storage/variation.rb +7 -5
- data/config/routes.rb +12 -10
- data/db/migrate/20170806125915_create_active_storage_tables.rb +15 -6
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +77 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +41 -0
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +51 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +4 -30
- data/lib/active_storage/analyzer/video_analyzer.rb +41 -17
- data/lib/active_storage/analyzer.rb +10 -4
- data/lib/active_storage/attached/changes/create_many.rb +14 -5
- data/lib/active_storage/attached/changes/create_one.rb +46 -4
- 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 +32 -19
- data/lib/active_storage/attached/model.rb +80 -29
- data/lib/active_storage/attached/one.rb +37 -31
- data/lib/active_storage/attached.rb +2 -0
- data/lib/active_storage/deprecator.rb +7 -0
- data/lib/active_storage/downloader.rb +4 -4
- data/lib/active_storage/engine.rb +55 -7
- data/lib/active_storage/fixture_set.rb +75 -0
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/log_subscriber.rb +12 -0
- data/lib/active_storage/previewer.rb +12 -5
- data/lib/active_storage/reflection.rb +12 -2
- data/lib/active_storage/service/azure_storage_service.rb +30 -6
- data/lib/active_storage/service/configurator.rb +1 -1
- data/lib/active_storage/service/disk_service.rb +26 -19
- data/lib/active_storage/service/gcs_service.rb +100 -11
- data/lib/active_storage/service/mirror_service.rb +12 -7
- data/lib/active_storage/service/registry.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +39 -15
- data/lib/active_storage/service.rb +17 -7
- data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
- data/lib/active_storage/transformers/transformer.rb +3 -1
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +22 -2
- metadata +30 -30
- data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -1,25 +1,31 @@
|
|
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
|
8
|
+
# = Active Storage \GCS \Service
|
9
|
+
#
|
7
10
|
# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
|
8
11
|
# documentation that applies to all services.
|
9
12
|
class Service::GCSService < Service
|
13
|
+
class MetadataServerError < ActiveStorage::Error; end
|
14
|
+
class MetadataServerNotFoundError < ActiveStorage::Error; end
|
15
|
+
|
10
16
|
def initialize(public: false, **config)
|
11
17
|
@config = config
|
12
18
|
@public = public
|
13
19
|
end
|
14
20
|
|
15
|
-
def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
|
21
|
+
def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})
|
16
22
|
instrument :upload, key: key, checksum: checksum do
|
17
23
|
# GCS's signed URLs don't include params such as response-content-type response-content_disposition
|
18
24
|
# in the signature, which means an attacker can modify them and bypass our effort to force these to
|
19
25
|
# binary and attachment when the file's content type requires it. The only way to force them is to
|
20
26
|
# store them as object's metadata.
|
21
27
|
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)
|
28
|
+
bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
|
23
29
|
rescue Google::Cloud::InvalidArgumentError
|
24
30
|
raise ActiveStorage::IntegrityError
|
25
31
|
end
|
@@ -39,11 +45,12 @@ module ActiveStorage
|
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
42
|
-
def update_metadata(key, content_type:, disposition: nil, filename: nil)
|
48
|
+
def update_metadata(key, content_type:, disposition: nil, filename: nil, custom_metadata: {})
|
43
49
|
instrument :update_metadata, key: key, content_type: content_type, disposition: disposition do
|
44
50
|
file_for(key).update do |file|
|
45
51
|
file.content_type = content_type
|
46
52
|
file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
53
|
+
file.metadata = custom_metadata
|
47
54
|
end
|
48
55
|
end
|
49
56
|
end
|
@@ -82,9 +89,35 @@ module ActiveStorage
|
|
82
89
|
end
|
83
90
|
end
|
84
91
|
|
85
|
-
def url_for_direct_upload(key, expires_in:, checksum:, **)
|
92
|
+
def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
|
86
93
|
instrument :url, key: key do |payload|
|
87
|
-
|
94
|
+
headers = {}
|
95
|
+
version = :v2
|
96
|
+
|
97
|
+
if @config[:cache_control].present?
|
98
|
+
headers["Cache-Control"] = @config[:cache_control]
|
99
|
+
# v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
|
100
|
+
# if necessary for back-compat; v4 limits the expiration of the URL to 7 days
|
101
|
+
# whereas v2 has no limit
|
102
|
+
version = :v4
|
103
|
+
end
|
104
|
+
|
105
|
+
headers.merge!(custom_metadata_headers(custom_metadata))
|
106
|
+
|
107
|
+
args = {
|
108
|
+
content_md5: checksum,
|
109
|
+
expires: expires_in,
|
110
|
+
headers: headers,
|
111
|
+
method: "PUT",
|
112
|
+
version: version,
|
113
|
+
}
|
114
|
+
|
115
|
+
if @config[:iam]
|
116
|
+
args[:issuer] = issuer
|
117
|
+
args[:signer] = signer
|
118
|
+
end
|
119
|
+
|
120
|
+
generated_url = bucket.signed_url(key, **args)
|
88
121
|
|
89
122
|
payload[:url] = generated_url
|
90
123
|
|
@@ -92,18 +125,41 @@ module ActiveStorage
|
|
92
125
|
end
|
93
126
|
end
|
94
127
|
|
95
|
-
def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, **)
|
128
|
+
def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
|
96
129
|
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
|
97
130
|
|
98
|
-
{ "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
|
131
|
+
headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
|
132
|
+
if @config[:cache_control].present?
|
133
|
+
headers["Cache-Control"] = @config[:cache_control]
|
134
|
+
end
|
135
|
+
|
136
|
+
headers
|
137
|
+
end
|
138
|
+
|
139
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
140
|
+
bucket.compose(source_keys, destination_key).update do |file|
|
141
|
+
file.content_type = content_type
|
142
|
+
file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
143
|
+
file.metadata = custom_metadata
|
144
|
+
end
|
99
145
|
end
|
100
146
|
|
101
147
|
private
|
102
148
|
def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
|
103
|
-
|
104
|
-
|
105
|
-
|
149
|
+
args = {
|
150
|
+
expires: expires_in,
|
151
|
+
query: {
|
152
|
+
"response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
|
153
|
+
"response-content-type" => content_type
|
154
|
+
}
|
106
155
|
}
|
156
|
+
|
157
|
+
if @config[:iam]
|
158
|
+
args[:issuer] = issuer
|
159
|
+
args[:signer] = signer
|
160
|
+
end
|
161
|
+
|
162
|
+
file_for(key).signed_url(**args)
|
107
163
|
end
|
108
164
|
|
109
165
|
def public_url(key, **)
|
@@ -137,7 +193,40 @@ module ActiveStorage
|
|
137
193
|
end
|
138
194
|
|
139
195
|
def client
|
140
|
-
@client ||= Google::Cloud::Storage.new(**config.except(:bucket))
|
196
|
+
@client ||= Google::Cloud::Storage.new(**config.except(:bucket, :cache_control, :iam, :gsa_email))
|
197
|
+
end
|
198
|
+
|
199
|
+
def issuer
|
200
|
+
@issuer ||= @config[:gsa_email].presence || email_from_metadata_server
|
201
|
+
end
|
202
|
+
|
203
|
+
def email_from_metadata_server
|
204
|
+
env = Google::Cloud.env
|
205
|
+
raise MetadataServerNotFoundError if !env.metadata?
|
206
|
+
|
207
|
+
email = env.lookup_metadata("instance", "service-accounts/default/email")
|
208
|
+
email.presence or raise MetadataServerError
|
209
|
+
end
|
210
|
+
|
211
|
+
def signer
|
212
|
+
# https://googleapis.dev/ruby/google-cloud-storage/latest/Google/Cloud/Storage/Project.html#signed_url-instance_method
|
213
|
+
lambda do |string_to_sign|
|
214
|
+
iam_client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new
|
215
|
+
|
216
|
+
scopes = ["https://www.googleapis.com/auth/iam"]
|
217
|
+
iam_client.authorization = Google::Auth.get_application_default(scopes)
|
218
|
+
|
219
|
+
request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
|
220
|
+
payload: string_to_sign
|
221
|
+
)
|
222
|
+
resource = "projects/-/serviceAccounts/#{issuer}"
|
223
|
+
response = iam_client.sign_service_account_blob(resource, request)
|
224
|
+
response.signed_blob
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def custom_metadata_headers(metadata)
|
229
|
+
metadata.transform_keys { |key| "x-goog-meta-#{key}" }
|
141
230
|
end
|
142
231
|
end
|
143
232
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
4
|
|
5
5
|
module ActiveStorage
|
6
|
+
# = Active Storage Mirror \Service
|
7
|
+
#
|
6
8
|
# Wraps a set of mirror services and provides a single ActiveStorage::Service object that will all
|
7
9
|
# have the files uploaded to them. A +primary+ service is designated to answer calls to:
|
8
10
|
# * +download+
|
@@ -14,10 +16,10 @@ module ActiveStorage
|
|
14
16
|
attr_reader :primary, :mirrors
|
15
17
|
|
16
18
|
delegate :download, :download_chunk, :exist?, :url,
|
17
|
-
:url_for_direct_upload, :headers_for_direct_upload, :path_for, to: :primary
|
19
|
+
:url_for_direct_upload, :headers_for_direct_upload, :path_for, :compose, to: :primary
|
18
20
|
|
19
21
|
# Stitch together from named services.
|
20
|
-
def self.build(primary:, mirrors:, name:, configurator:, **options)
|
22
|
+
def self.build(primary:, mirrors:, name:, configurator:, **options) # :nodoc:
|
21
23
|
new(
|
22
24
|
primary: configurator.build(primary),
|
23
25
|
mirrors: mirrors.collect { |mirror_name| configurator.build mirror_name }
|
@@ -30,13 +32,13 @@ module ActiveStorage
|
|
30
32
|
@primary, @mirrors = primary, mirrors
|
31
33
|
end
|
32
34
|
|
33
|
-
# Upload the +io+ to the +key+ specified to all services.
|
35
|
+
# Upload the +io+ to the +key+ specified to all services. The upload to the primary service is done synchronously
|
36
|
+
# whereas the upload to the mirrors is done asynchronously. If a +checksum+ is provided, all services will
|
34
37
|
# ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
|
35
38
|
def upload(key, io, checksum: nil, **options)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
39
|
+
io.rewind
|
40
|
+
primary.upload key, io, checksum: checksum, **options
|
41
|
+
mirror_later key, checksum: checksum
|
40
42
|
end
|
41
43
|
|
42
44
|
# Delete the file at the +key+ on all services.
|
@@ -49,6 +51,9 @@ module ActiveStorage
|
|
49
51
|
perform_across_services :delete_prefixed, prefix
|
50
52
|
end
|
51
53
|
|
54
|
+
def mirror_later(key, checksum:) # :nodoc:
|
55
|
+
ActiveStorage::MirrorJob.perform_later key, checksum: checksum
|
56
|
+
end
|
52
57
|
|
53
58
|
# Copy the file at the +key+ from the primary service to each of the mirrors where it doesn't already exist.
|
54
59
|
def mirror(key, checksum:)
|
@@ -6,6 +6,8 @@ require "aws-sdk-s3"
|
|
6
6
|
require "active_support/core_ext/numeric/bytes"
|
7
7
|
|
8
8
|
module ActiveStorage
|
9
|
+
# = Active Storage \S3 \Service
|
10
|
+
#
|
9
11
|
# Wraps the Amazon Simple Storage Service (S3) as an Active Storage service.
|
10
12
|
# See ActiveStorage::Service for the generic API documentation that applies to all services.
|
11
13
|
class Service::S3Service < Service
|
@@ -23,14 +25,14 @@ module ActiveStorage
|
|
23
25
|
@upload_options[:acl] = "public-read" if public?
|
24
26
|
end
|
25
27
|
|
26
|
-
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
|
28
|
+
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
|
27
29
|
instrument :upload, key: key, checksum: checksum do
|
28
30
|
content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
|
29
31
|
|
30
32
|
if io.size < multipart_upload_threshold
|
31
|
-
upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition
|
33
|
+
upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition, custom_metadata: custom_metadata
|
32
34
|
else
|
33
|
-
upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition
|
35
|
+
upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition, custom_metadata: custom_metadata
|
34
36
|
end
|
35
37
|
end
|
36
38
|
end
|
@@ -77,11 +79,11 @@ module ActiveStorage
|
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
80
|
-
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
82
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
81
83
|
instrument :url, key: key do |payload|
|
82
84
|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
|
83
85
|
content_type: content_type, content_length: content_length, content_md5: checksum,
|
84
|
-
whitelist_headers: ["content-length"], **upload_options
|
86
|
+
metadata: custom_metadata, whitelist_headers: ["content-length"], **upload_options
|
85
87
|
|
86
88
|
payload[:url] = generated_url
|
87
89
|
|
@@ -89,37 +91,55 @@ module ActiveStorage
|
|
89
91
|
end
|
90
92
|
end
|
91
93
|
|
92
|
-
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
|
94
|
+
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
|
93
95
|
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
|
94
96
|
|
95
|
-
{ "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
|
97
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition, **custom_metadata_headers(custom_metadata) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
101
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
102
|
+
|
103
|
+
object_for(destination_key).upload_stream(
|
104
|
+
content_type: content_type,
|
105
|
+
content_disposition: content_disposition,
|
106
|
+
part_size: MINIMUM_UPLOAD_PART_SIZE,
|
107
|
+
metadata: custom_metadata,
|
108
|
+
**upload_options
|
109
|
+
) do |out|
|
110
|
+
source_keys.each do |source_key|
|
111
|
+
stream(source_key) do |chunk|
|
112
|
+
IO.copy_stream(StringIO.new(chunk), out)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
96
116
|
end
|
97
117
|
|
98
118
|
private
|
99
|
-
def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
|
119
|
+
def private_url(key, expires_in:, filename:, disposition:, content_type:, **client_opts)
|
100
120
|
object_for(key).presigned_url :get, expires_in: expires_in.to_i,
|
101
121
|
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
|
102
|
-
response_content_type: content_type
|
122
|
+
response_content_type: content_type, **client_opts
|
103
123
|
end
|
104
124
|
|
105
|
-
def public_url(key, **)
|
106
|
-
object_for(key).public_url
|
125
|
+
def public_url(key, **client_opts)
|
126
|
+
object_for(key).public_url(**client_opts)
|
107
127
|
end
|
108
128
|
|
109
129
|
|
110
130
|
MAXIMUM_UPLOAD_PARTS_COUNT = 10000
|
111
131
|
MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
|
112
132
|
|
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)
|
133
|
+
def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil, custom_metadata: {})
|
134
|
+
object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata, **upload_options)
|
115
135
|
rescue Aws::S3::Errors::BadDigest
|
116
136
|
raise ActiveStorage::IntegrityError
|
117
137
|
end
|
118
138
|
|
119
|
-
def upload_with_multipart(key, io, content_type: nil, content_disposition: nil)
|
139
|
+
def upload_with_multipart(key, io, content_type: nil, content_disposition: nil, custom_metadata: {})
|
120
140
|
part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
|
121
141
|
|
122
|
-
object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, **upload_options) do |out|
|
142
|
+
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
143
|
IO.copy_stream(io, out)
|
124
144
|
end
|
125
145
|
end
|
@@ -143,5 +163,9 @@ module ActiveStorage
|
|
143
163
|
offset += chunk_size
|
144
164
|
end
|
145
165
|
end
|
166
|
+
|
167
|
+
def custom_metadata_headers(metadata)
|
168
|
+
metadata.transform_keys { |key| "x-amz-meta-#{key}" }
|
169
|
+
end
|
146
170
|
end
|
147
171
|
end
|
@@ -6,6 +6,8 @@ require "action_dispatch"
|
|
6
6
|
require "action_dispatch/http/content_disposition"
|
7
7
|
|
8
8
|
module ActiveStorage
|
9
|
+
# = Active Storage \Service
|
10
|
+
#
|
9
11
|
# Abstract class serving as an interface for concrete services.
|
10
12
|
#
|
11
13
|
# The available services are:
|
@@ -16,7 +18,7 @@ module ActiveStorage
|
|
16
18
|
# * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
|
17
19
|
# * +Mirror+, to be able to use several services to manage attachments.
|
18
20
|
#
|
19
|
-
# Inside a Rails application, you can set-up your services through the
|
21
|
+
# Inside a \Rails application, you can set-up your services through the
|
20
22
|
# generated <tt>config/storage.yml</tt> file and reference one
|
21
23
|
# of the aforementioned constant under the +service+ key. For example:
|
22
24
|
#
|
@@ -31,12 +33,12 @@ module ActiveStorage
|
|
31
33
|
#
|
32
34
|
# config.active_storage.service = :local
|
33
35
|
#
|
34
|
-
# If you are using Active Storage outside of a Ruby on Rails application, you
|
36
|
+
# If you are using Active Storage outside of a Ruby on \Rails application, you
|
35
37
|
# can configure the service to use like this:
|
36
38
|
#
|
37
39
|
# ActiveStorage::Blob.service = ActiveStorage::Service.configure(
|
38
|
-
# :
|
39
|
-
# root: Pathname("/foo/
|
40
|
+
# :local,
|
41
|
+
# { local: {service: "Disk", root: Pathname("/tmp/foo/storage") } }
|
40
42
|
# )
|
41
43
|
class Service
|
42
44
|
extend ActiveSupport::Autoload
|
@@ -57,7 +59,7 @@ module ActiveStorage
|
|
57
59
|
# Passes the configurator and all of the service's config as keyword args.
|
58
60
|
#
|
59
61
|
# See MirrorService for an example.
|
60
|
-
def build(configurator:, name:, service: nil, **service_config)
|
62
|
+
def build(configurator:, name:, service: nil, **service_config) # :nodoc:
|
61
63
|
new(**service_config).tap do |service_instance|
|
62
64
|
service_instance.name = name
|
63
65
|
end
|
@@ -90,6 +92,11 @@ module ActiveStorage
|
|
90
92
|
ActiveStorage::Downloader.new(self).open(*args, **options, &block)
|
91
93
|
end
|
92
94
|
|
95
|
+
# Concatenate multiple files into a single "composed" file.
|
96
|
+
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
97
|
+
raise NotImplementedError
|
98
|
+
end
|
99
|
+
|
93
100
|
# Delete the file at the +key+.
|
94
101
|
def delete(key)
|
95
102
|
raise NotImplementedError
|
@@ -128,12 +135,12 @@ module ActiveStorage
|
|
128
135
|
# The URL will be valid for the amount of seconds specified in +expires_in+.
|
129
136
|
# You must also provide the +content_type+, +content_length+, and +checksum+ of the file
|
130
137
|
# 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:)
|
138
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
|
132
139
|
raise NotImplementedError
|
133
140
|
end
|
134
141
|
|
135
142
|
# Returns a Hash of headers for +url_for_direct_upload+ requests.
|
136
|
-
def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
|
143
|
+
def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:, custom_metadata: {})
|
137
144
|
{}
|
138
145
|
end
|
139
146
|
|
@@ -150,6 +157,9 @@ module ActiveStorage
|
|
150
157
|
raise NotImplementedError
|
151
158
|
end
|
152
159
|
|
160
|
+
def custom_metadata_headers(metadata)
|
161
|
+
raise NotImplementedError
|
162
|
+
end
|
153
163
|
|
154
164
|
def instrument(operation, payload = {}, &block)
|
155
165
|
ActiveSupport::Notifications.instrument(
|
@@ -38,7 +38,7 @@ module ActiveStorage
|
|
38
38
|
if name.to_s == "combine_options"
|
39
39
|
raise ArgumentError, <<~ERROR.squish
|
40
40
|
Active Storage's ImageProcessing transformer doesn't support :combine_options,
|
41
|
-
as it always generates a single
|
41
|
+
as it always generates a single command.
|
42
42
|
ERROR
|
43
43
|
end
|
44
44
|
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module ActiveStorage
|
4
4
|
module Transformers
|
5
|
+
# = Active Storage \Transformers \Transformer
|
6
|
+
#
|
5
7
|
# A Transformer applies a set of transformations to an image.
|
6
8
|
#
|
7
9
|
# The following concrete subclasses are included in Active Storage:
|
@@ -31,7 +33,7 @@ module ActiveStorage
|
|
31
33
|
private
|
32
34
|
# Returns an open Tempfile containing a transformed image in the given +format+.
|
33
35
|
# All subclasses implement this method.
|
34
|
-
def process(file, format:)
|
36
|
+
def process(file, format:) # :doc:
|
35
37
|
raise NotImplementedError
|
36
38
|
end
|
37
39
|
end
|
data/lib/active_storage.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#--
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) David Heinemeier Hansson, 37signals LLC
|
5
5
|
#
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining
|
7
7
|
# a copy of this software and associated documentation files (the
|
@@ -29,14 +29,18 @@ require "active_support/rails"
|
|
29
29
|
require "active_support/core_ext/numeric/time"
|
30
30
|
|
31
31
|
require "active_storage/version"
|
32
|
+
require "active_storage/deprecator"
|
32
33
|
require "active_storage/errors"
|
33
34
|
|
34
35
|
require "marcel"
|
35
36
|
|
37
|
+
# :markup: markdown
|
38
|
+
# :include: activestorage/README.md
|
36
39
|
module ActiveStorage
|
37
40
|
extend ActiveSupport::Autoload
|
38
41
|
|
39
42
|
autoload :Attached
|
43
|
+
autoload :FixtureSet
|
40
44
|
autoload :Service
|
41
45
|
autoload :Previewer
|
42
46
|
autoload :Analyzer
|
@@ -350,16 +354,32 @@ module ActiveStorage
|
|
350
354
|
mattr_accessor :unsupported_image_processing_arguments
|
351
355
|
|
352
356
|
mattr_accessor :service_urls_expire_in, default: 5.minutes
|
357
|
+
mattr_accessor :urls_expire_in
|
353
358
|
|
354
359
|
mattr_accessor :routes_prefix, default: "/rails/active_storage"
|
355
360
|
mattr_accessor :draw_routes, default: true
|
356
361
|
mattr_accessor :resolve_model_to_route, default: :rails_storage_redirect
|
357
362
|
|
358
|
-
mattr_accessor :replace_on_assign_to_many, default: false
|
359
363
|
mattr_accessor :track_variants, default: false
|
360
364
|
|
361
365
|
mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
|
362
366
|
|
367
|
+
def self.replace_on_assign_to_many
|
368
|
+
ActiveStorage.deprecator.warn("config.active_storage.replace_on_assign_to_many is deprecated and has no effect.")
|
369
|
+
end
|
370
|
+
|
371
|
+
def self.replace_on_assign_to_many=(value)
|
372
|
+
ActiveStorage.deprecator.warn("config.active_storage.replace_on_assign_to_many is deprecated and has no effect.")
|
373
|
+
end
|
374
|
+
|
375
|
+
def self.silence_invalid_content_types_warning
|
376
|
+
ActiveStorage.deprecator.warn("config.active_storage.silence_invalid_content_types_warning is deprecated and has no effect.")
|
377
|
+
end
|
378
|
+
|
379
|
+
def self.silence_invalid_content_types_warning=(value)
|
380
|
+
ActiveStorage.deprecator.warn("config.active_storage.silence_invalid_content_types_warning is deprecated and has no effect.")
|
381
|
+
end
|
382
|
+
|
363
383
|
module Transformers
|
364
384
|
extend ActiveSupport::Autoload
|
365
385
|
|