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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -276
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +29 -15
  5. data/app/assets/javascripts/activestorage.esm.js +848 -0
  6. data/app/assets/javascripts/activestorage.js +263 -376
  7. data/app/controllers/active_storage/base_controller.rb +0 -9
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +16 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/disk_controller.rb +5 -2
  11. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +8 -3
  13. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  14. data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
  15. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  16. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  17. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  18. data/app/controllers/concerns/active_storage/streaming.rb +66 -0
  19. data/app/javascript/activestorage/blob_record.js +4 -1
  20. data/app/javascript/activestorage/direct_upload.js +3 -2
  21. data/app/javascript/activestorage/index.js +3 -1
  22. data/app/javascript/activestorage/ujs.js +1 -1
  23. data/app/jobs/active_storage/analyze_job.rb +1 -1
  24. data/app/jobs/active_storage/mirror_job.rb +1 -1
  25. data/app/jobs/active_storage/purge_job.rb +1 -1
  26. data/app/jobs/active_storage/transform_job.rb +12 -0
  27. data/app/models/active_storage/attachment.rb +111 -4
  28. data/app/models/active_storage/blob/analyzable.rb +4 -3
  29. data/app/models/active_storage/blob/identifiable.rb +1 -0
  30. data/app/models/active_storage/blob/representable.rb +14 -8
  31. data/app/models/active_storage/blob.rb +93 -57
  32. data/app/models/active_storage/current.rb +2 -2
  33. data/app/models/active_storage/filename.rb +2 -0
  34. data/app/models/active_storage/named_variant.rb +21 -0
  35. data/app/models/active_storage/preview.rb +11 -7
  36. data/app/models/active_storage/record.rb +1 -1
  37. data/app/models/active_storage/variant.rb +10 -12
  38. data/app/models/active_storage/variant_record.rb +2 -0
  39. data/app/models/active_storage/variant_with_record.rb +28 -12
  40. data/app/models/active_storage/variation.rb +7 -5
  41. data/config/routes.rb +12 -10
  42. data/db/migrate/20170806125915_create_active_storage_tables.rb +15 -6
  43. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
  44. data/lib/active_storage/analyzer/audio_analyzer.rb +77 -0
  45. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +41 -0
  46. data/lib/active_storage/analyzer/image_analyzer/vips.rb +51 -0
  47. data/lib/active_storage/analyzer/image_analyzer.rb +4 -30
  48. data/lib/active_storage/analyzer/video_analyzer.rb +41 -17
  49. data/lib/active_storage/analyzer.rb +10 -4
  50. data/lib/active_storage/attached/changes/create_many.rb +14 -5
  51. data/lib/active_storage/attached/changes/create_one.rb +46 -4
  52. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  53. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  54. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  55. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  56. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  57. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  58. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  59. data/lib/active_storage/attached/changes.rb +7 -1
  60. data/lib/active_storage/attached/many.rb +32 -19
  61. data/lib/active_storage/attached/model.rb +80 -29
  62. data/lib/active_storage/attached/one.rb +37 -31
  63. data/lib/active_storage/attached.rb +2 -0
  64. data/lib/active_storage/deprecator.rb +7 -0
  65. data/lib/active_storage/downloader.rb +4 -4
  66. data/lib/active_storage/engine.rb +55 -7
  67. data/lib/active_storage/fixture_set.rb +75 -0
  68. data/lib/active_storage/gem_version.rb +3 -3
  69. data/lib/active_storage/log_subscriber.rb +12 -0
  70. data/lib/active_storage/previewer.rb +12 -5
  71. data/lib/active_storage/reflection.rb +12 -2
  72. data/lib/active_storage/service/azure_storage_service.rb +30 -6
  73. data/lib/active_storage/service/configurator.rb +1 -1
  74. data/lib/active_storage/service/disk_service.rb +26 -19
  75. data/lib/active_storage/service/gcs_service.rb +100 -11
  76. data/lib/active_storage/service/mirror_service.rb +12 -7
  77. data/lib/active_storage/service/registry.rb +1 -1
  78. data/lib/active_storage/service/s3_service.rb +39 -15
  79. data/lib/active_storage/service.rb +17 -7
  80. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  81. data/lib/active_storage/transformers/transformer.rb +3 -1
  82. data/lib/active_storage/version.rb +1 -1
  83. data/lib/active_storage.rb +22 -2
  84. metadata +30 -30
  85. 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
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
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
- 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
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) #:nodoc:
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. If a +checksum+ is provided, all services will
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
- each_service.collect do |service|
37
- io.rewind
38
- service.upload key, io, checksum: checksum, **options
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:)
@@ -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 = {}
@@ -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
- # :Disk,
39
- # root: Pathname("/foo/bar/storage")
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) #:nodoc:
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 ImageMagick command.
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:) #:doc:
36
+ def process(file, format:) # :doc:
35
37
  raise NotImplementedError
36
38
  end
37
39
  end
@@ -3,7 +3,7 @@
3
3
  require_relative "gem_version"
4
4
 
5
5
  module ActiveStorage
6
- # Returns the version of the currently loaded ActiveStorage as a <tt>Gem::Version</tt>
6
+ # Returns the currently loaded version of Active Storage as a +Gem::Version+.
7
7
  def self.version
8
8
  gem_version
9
9
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2017-2022 David Heinemeier Hansson, Basecamp
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