activestorage 6.1.4.1 → 7.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +130 -220
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +844 -0
  6. data/app/assets/javascripts/activestorage.js +257 -376
  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/representations/base_controller.rb +5 -1
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
  12. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  13. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  14. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  15. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  16. data/app/javascript/activestorage/ujs.js +1 -1
  17. data/app/models/active_storage/attachment.rb +36 -3
  18. data/app/models/active_storage/blob/representable.rb +7 -5
  19. data/app/models/active_storage/blob.rb +26 -27
  20. data/app/models/active_storage/current.rb +12 -2
  21. data/app/models/active_storage/preview.rb +6 -4
  22. data/app/models/active_storage/record.rb +1 -1
  23. data/app/models/active_storage/variant.rb +3 -6
  24. data/app/models/active_storage/variant_record.rb +2 -0
  25. data/app/models/active_storage/variant_with_record.rb +9 -5
  26. data/app/models/active_storage/variation.rb +2 -2
  27. data/config/routes.rb +10 -10
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +29 -8
  29. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +15 -2
  30. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  31. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  32. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  33. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  34. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  35. data/lib/active_storage/analyzer.rb +8 -4
  36. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  37. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  38. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  39. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  40. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  41. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  42. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  43. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  44. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  45. data/lib/active_storage/attached/changes.rb +7 -1
  46. data/lib/active_storage/attached/many.rb +27 -15
  47. data/lib/active_storage/attached/model.rb +31 -5
  48. data/lib/active_storage/attached/one.rb +32 -27
  49. data/lib/active_storage/downloader.rb +2 -2
  50. data/lib/active_storage/engine.rb +28 -1
  51. data/lib/active_storage/fixture_set.rb +76 -0
  52. data/lib/active_storage/gem_version.rb +4 -4
  53. data/lib/active_storage/previewer.rb +4 -4
  54. data/lib/active_storage/reflection.rb +12 -2
  55. data/lib/active_storage/service/azure_storage_service.rb +1 -1
  56. data/lib/active_storage/service/configurator.rb +1 -1
  57. data/lib/active_storage/service/disk_service.rb +13 -18
  58. data/lib/active_storage/service/gcs_service.rb +91 -7
  59. data/lib/active_storage/service/mirror_service.rb +1 -1
  60. data/lib/active_storage/service/registry.rb +1 -1
  61. data/lib/active_storage/service/s3_service.rb +4 -4
  62. data/lib/active_storage/service.rb +3 -3
  63. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  64. data/lib/active_storage/transformers/transformer.rb +1 -1
  65. data/lib/active_storage.rb +3 -1
  66. metadata +30 -21
  67. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -3,6 +3,25 @@
3
3
  module ActiveStorage
4
4
  # Representation of a single attachment to a model.
5
5
  class Attached::One < Attached
6
+ ##
7
+ # :method: purge
8
+ #
9
+ # Directly purges the attachment (i.e. destroys the blob and
10
+ # attachment and deletes the file on the service).
11
+ delegate :purge, to: :purge_one
12
+
13
+ ##
14
+ # :method: purge_later
15
+ #
16
+ # Purges the attachment through the queuing system.
17
+ delegate :purge_later, to: :purge_one
18
+
19
+ ##
20
+ # :method: detach
21
+ #
22
+ # Deletes the attachment without purging it, leaving its blob in place.
23
+ delegate :detach, to: :detach_one
24
+
6
25
  delegate_missing_to :attachment, allow_nil: true
7
26
 
8
27
  # Returns the associated attachment record.
@@ -13,6 +32,13 @@ module ActiveStorage
13
32
  change.present? ? change.attachment : record.public_send("#{name}_attachment")
14
33
  end
15
34
 
35
+ # Returns +true+ if an attachment is not attached.
36
+ #
37
+ # class User < ApplicationRecord
38
+ # has_one_attached :avatar
39
+ # end
40
+ #
41
+ # User.new.avatar.blank? # => true
16
42
  def blank?
17
43
  !attached?
18
44
  end
@@ -25,7 +51,7 @@ module ActiveStorage
25
51
  #
26
52
  # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object
27
53
  # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload
28
- # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
54
+ # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg")
29
55
  # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
30
56
  def attach(attachable)
31
57
  if record.persisted? && !record.changed?
@@ -47,34 +73,13 @@ module ActiveStorage
47
73
  attachment.present?
48
74
  end
49
75
 
50
- # Deletes the attachment without purging it, leaving its blob in place.
51
- def detach
52
- if attached?
53
- attachment.delete
54
- write_attachment nil
55
- end
56
- end
57
-
58
- # Directly purges the attachment (i.e. destroys the blob and
59
- # attachment and deletes the file on the service).
60
- def purge
61
- if attached?
62
- attachment.purge
63
- write_attachment nil
64
- end
65
- end
66
-
67
- # Purges the attachment through the queuing system.
68
- def purge_later
69
- if attached?
70
- attachment.purge_later
71
- write_attachment nil
76
+ private
77
+ def purge_one
78
+ Attached::Changes::PurgeOne.new(name, record, attachment)
72
79
  end
73
- end
74
80
 
75
- private
76
- def write_attachment(attachment)
77
- record.public_send("#{name}_attachment=", attachment)
81
+ def detach_one
82
+ Attached::Changes::DetachOne.new(name, record, attachment)
78
83
  end
79
84
  end
80
85
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Downloader #:nodoc:
4
+ class Downloader # :nodoc:
5
5
  attr_reader :service
6
6
 
7
7
  def initialize(service)
@@ -35,7 +35,7 @@ module ActiveStorage
35
35
  end
36
36
 
37
37
  def verify_integrity_of(file, checksum:)
38
- unless Digest::MD5.file(file).base64digest == checksum
38
+ unless OpenSSL::Digest::MD5.file(file).base64digest == checksum
39
39
  raise ActiveStorage::IntegrityError
40
40
  end
41
41
  end
@@ -12,7 +12,10 @@ require "active_storage/previewer/mupdf_previewer"
12
12
  require "active_storage/previewer/video_previewer"
13
13
 
14
14
  require "active_storage/analyzer/image_analyzer"
15
+ require "active_storage/analyzer/image_analyzer/image_magick"
16
+ require "active_storage/analyzer/image_analyzer/vips"
15
17
  require "active_storage/analyzer/video_analyzer"
18
+ require "active_storage/analyzer/audio_analyzer"
16
19
 
17
20
  require "active_storage/service/registry"
18
21
 
@@ -24,7 +27,7 @@ module ActiveStorage
24
27
 
25
28
  config.active_storage = ActiveSupport::OrderedOptions.new
26
29
  config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
27
- config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
30
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer::Vips, ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick, ActiveStorage::Analyzer::VideoAnalyzer, ActiveStorage::Analyzer::AudioAnalyzer ]
28
31
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
29
32
  config.active_storage.queues = ActiveSupport::InheritableOptions.new
30
33
 
@@ -39,6 +42,9 @@ module ActiveStorage
39
42
  image/vnd.adobe.photoshop
40
43
  image/vnd.microsoft.icon
41
44
  image/webp
45
+ image/avif
46
+ image/heic
47
+ image/heif
42
48
  )
43
49
 
44
50
  config.active_storage.web_image_content_types = %w(
@@ -90,6 +96,7 @@ module ActiveStorage
90
96
  ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
91
97
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
92
98
  ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
99
+ ActiveStorage.urls_expire_in = app.config.active_storage.urls_expire_in
93
100
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
94
101
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
95
102
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
@@ -144,5 +151,25 @@ module ActiveStorage
144
151
  ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
145
152
  end
146
153
  end
154
+
155
+ initializer "active_storage.asset" do
156
+ if Rails.application.config.respond_to?(:assets)
157
+ Rails.application.config.assets.precompile += %w( activestorage activestorage.esm )
158
+ end
159
+ end
160
+
161
+ initializer "active_storage.fixture_set" do
162
+ ActiveSupport.on_load(:active_record_fixture_set) do
163
+ ActiveStorage::FixtureSet.file_fixture_path ||= Rails.root.join(*[
164
+ ENV.fetch("FIXTURES_PATH") { File.join("test", "fixtures") },
165
+ ENV["FIXTURES_DIR"],
166
+ "files"
167
+ ].compact_blank)
168
+ end
169
+
170
+ ActiveSupport.on_load(:active_support_test_case) do
171
+ ActiveStorage::FixtureSet.file_fixture_path = ActiveSupport::TestCase.file_fixture_path
172
+ end
173
+ end
147
174
  end
148
175
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/testing/file_fixtures"
4
+ require "active_record/secure_token"
5
+
6
+ module ActiveStorage
7
+ # Fixtures are a way of organizing data that you want to test against; in
8
+ # short, sample data.
9
+ #
10
+ # To learn more about fixtures, read the
11
+ # {ActiveRecord::FixtureSet}[rdoc-ref:ActiveRecord::FixtureSet] documentation.
12
+ #
13
+ # === YAML
14
+ #
15
+ # Like other Active Record-backed models,
16
+ # {ActiveStorage::Attachment}[rdoc-ref:ActiveStorage::Attachment] and
17
+ # {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob] records inherit from
18
+ # {ActiveRecord::Base}[rdoc-ref:ActiveRecord::Base] instances and therefore
19
+ # can be populated by fixtures.
20
+ #
21
+ # Consider a hypothetical <tt>Article</tt> model class, its related
22
+ # fixture data, as well as fixture data for related ActiveStorage::Attachment
23
+ # and ActiveStorage::Blob records:
24
+ #
25
+ # # app/models/article.rb
26
+ # class Article < ApplicationRecord
27
+ # has_one_attached :thumbnail
28
+ # end
29
+ #
30
+ # # fixtures/active_storage/blobs.yml
31
+ # first_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename: "first.png" %>
32
+ #
33
+ # # fixtures/active_storage/attachments.yml
34
+ # first_thumbnail_attachment:
35
+ # name: thumbnail
36
+ # record: first (Article)
37
+ # blob: first_thumbnail_blob
38
+ #
39
+ # When processed, Active Record will insert database records for each fixture
40
+ # entry and will ensure the Active Storage relationship is intact.
41
+ class FixtureSet
42
+ include ActiveSupport::Testing::FileFixtures
43
+ include ActiveRecord::SecureToken
44
+
45
+ # Generate a YAML-encoded representation of an ActiveStorage::Blob
46
+ # instance's attributes, resolve the file relative to the directory mentioned
47
+ # by <tt>ActiveSupport::Testing::FileFixtures.file_fixture</tt>, and upload
48
+ # the file to the Service
49
+ #
50
+ # === Examples
51
+ #
52
+ # # tests/fixtures/action_text/blobs.yml
53
+ # second_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
54
+ # filename: "second.svg",
55
+ # ) %>
56
+ #
57
+ # third_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
58
+ # filename: "third.svg",
59
+ # content_type: "image/svg+xml",
60
+ # service_name: "public"
61
+ # ) %>
62
+ #
63
+ def self.blob(filename:, **attributes)
64
+ new.prepare Blob.new(filename: filename, key: generate_unique_secure_token), **attributes
65
+ end
66
+
67
+ def prepare(instance, **attributes)
68
+ io = file_fixture(instance.filename.to_s).open
69
+ instance.unfurl(io)
70
+ instance.assign_attributes(attributes)
71
+ instance.upload_without_unfurling(io)
72
+
73
+ instance.attributes.transform_values { |value| value.is_a?(Hash) ? value.to_json : value }.compact.to_json
74
+ end
75
+ end
76
+ end
@@ -7,10 +7,10 @@ module ActiveStorage
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 4
13
- PRE = "1"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "alpha1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -26,7 +26,7 @@ module ActiveStorage
26
26
 
27
27
  private
28
28
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
29
- def download_blob_to_tempfile(&block) #:doc:
29
+ def download_blob_to_tempfile(&block) # :doc:
30
30
  blob.open tmpdir: tmpdir, &block
31
31
  end
32
32
 
@@ -44,7 +44,7 @@ module ActiveStorage
44
44
  # end
45
45
  #
46
46
  # The output tempfile is opened in the directory returned by #tmpdir.
47
- def draw(*argv) #:doc:
47
+ def draw(*argv) # :doc:
48
48
  open_tempfile do |file|
49
49
  instrument :preview, key: blob.key do
50
50
  capture(*argv, to: file)
@@ -83,11 +83,11 @@ module ActiveStorage
83
83
  to.rewind
84
84
  end
85
85
 
86
- def logger #:doc:
86
+ def logger # :doc:
87
87
  ActiveStorage.logger
88
88
  end
89
89
 
90
- def tmpdir #:doc:
90
+ def tmpdir # :doc:
91
91
  Dir.tmpdir
92
92
  end
93
93
  end
@@ -2,9 +2,19 @@
2
2
 
3
3
  module ActiveStorage
4
4
  module Reflection
5
+ class HasAttachedReflection < ActiveRecord::Reflection::MacroReflection # :nodoc:
6
+ def variant(name, transformations)
7
+ variants[name] = transformations
8
+ end
9
+
10
+ def variants
11
+ @variants ||= {}
12
+ end
13
+ end
14
+
5
15
  # Holds all the metadata about a has_one_attached attachment as it was
6
16
  # specified in the Active Record class.
7
- class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
17
+ class HasOneAttachedReflection < HasAttachedReflection # :nodoc:
8
18
  def macro
9
19
  :has_one_attached
10
20
  end
@@ -12,7 +22,7 @@ module ActiveStorage
12
22
 
13
23
  # Holds all the metadata about a has_many_attached attachment as it was
14
24
  # specified in the Active Record class.
15
- class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
25
+ class HasManyAttachedReflection < HasAttachedReflection # :nodoc:
16
26
  def macro
17
27
  :has_many_attached
18
28
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "azure-storage-blob", ">= 1.1"
3
+ gem "azure-storage-blob", ">= 2.0"
4
4
 
5
5
  require "active_support/core_ext/numeric/bytes"
6
6
  require "azure/storage/blob"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Service::Configurator #:nodoc:
4
+ class Service::Configurator # :nodoc:
5
5
  attr_reader :configurations
6
6
 
7
7
  def self.build(service_name, configurations)
@@ -2,14 +2,14 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "pathname"
5
- require "digest/md5"
5
+ require "openssl"
6
6
  require "active_support/core_ext/numeric/bytes"
7
7
 
8
8
  module ActiveStorage
9
9
  # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
10
10
  # documentation that applies to all services.
11
11
  class Service::DiskService < Service
12
- attr_reader :root
12
+ attr_accessor :root
13
13
 
14
14
  def initialize(root:, public: false, **options)
15
15
  @root = root
@@ -86,11 +86,9 @@ module ActiveStorage
86
86
  purpose: :blob_token
87
87
  )
88
88
 
89
- generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
90
-
91
- payload[:url] = generated_url
92
-
93
- generated_url
89
+ url_helpers.update_rails_disk_service_url(verified_token_with_expiration, url_options).tap do |generated_url|
90
+ payload[:url] = generated_url
91
+ end
94
92
  end
95
93
  end
96
94
 
@@ -98,7 +96,7 @@ module ActiveStorage
98
96
  { "Content-Type" => content_type }
99
97
  end
100
98
 
101
- def path_for(key) #:nodoc:
99
+ def path_for(key) # :nodoc:
102
100
  File.join root, folder_for(key), key
103
101
  end
104
102
 
@@ -124,14 +122,11 @@ module ActiveStorage
124
122
  purpose: :blob_key
125
123
  )
126
124
 
127
- current_uri = URI.parse(current_host)
125
+ if url_options.blank?
126
+ raise ArgumentError, "Cannot generate URL for #{filename} using Disk service, please set ActiveStorage::Current.url_options."
127
+ end
128
128
 
129
- url_helpers.rails_disk_service_url(verified_key_with_expiration,
130
- protocol: current_uri.scheme,
131
- host: current_uri.host,
132
- port: current_uri.port,
133
- filename: filename
134
- )
129
+ url_helpers.rails_disk_service_url(verified_key_with_expiration, filename: filename, **url_options)
135
130
  end
136
131
 
137
132
 
@@ -154,7 +149,7 @@ module ActiveStorage
154
149
  end
155
150
 
156
151
  def ensure_integrity_of(key, checksum)
157
- unless Digest::MD5.file(path_for(key)).base64digest == checksum
152
+ unless OpenSSL::Digest::MD5.file(path_for(key)).base64digest == checksum
158
153
  delete key
159
154
  raise ActiveStorage::IntegrityError
160
155
  end
@@ -164,8 +159,8 @@ module ActiveStorage
164
159
  @url_helpers ||= Rails.application.routes.url_helpers
165
160
  end
166
161
 
167
- def current_host
168
- ActiveStorage::Current.host
162
+ def url_options
163
+ ActiveStorage::Current.url_options
169
164
  end
170
165
  end
171
166
  end
@@ -1,12 +1,16 @@
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
@@ -19,7 +23,7 @@ module ActiveStorage
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)
23
27
  rescue Google::Cloud::InvalidArgumentError
24
28
  raise ActiveStorage::IntegrityError
25
29
  end
@@ -84,7 +88,31 @@ module ActiveStorage
84
88
 
85
89
  def url_for_direct_upload(key, expires_in:, checksum:, **)
86
90
  instrument :url, key: key do |payload|
87
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
91
+ headers = {}
92
+ version = :v2
93
+
94
+ if @config[:cache_control].present?
95
+ headers["Cache-Control"] = @config[:cache_control]
96
+ # v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
97
+ # if necessary for back-compat; v4 limits the expiration of the URL to 7 days
98
+ # whereas v2 has no limit
99
+ version = :v4
100
+ end
101
+
102
+ args = {
103
+ content_md5: checksum,
104
+ expires: expires_in,
105
+ headers: headers,
106
+ method: "PUT",
107
+ version: version,
108
+ }
109
+
110
+ if @config[:iam]
111
+ args[:issuer] = issuer
112
+ args[:signer] = signer
113
+ end
114
+
115
+ generated_url = bucket.signed_url(key, **args)
88
116
 
89
117
  payload[:url] = generated_url
90
118
 
@@ -95,15 +123,31 @@ module ActiveStorage
95
123
  def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, **)
96
124
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
97
125
 
98
- { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
126
+ headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
127
+
128
+ if @config[:cache_control].present?
129
+ headers["Cache-Control"] = @config[:cache_control]
130
+ end
131
+
132
+ headers
99
133
  end
100
134
 
101
135
  private
102
136
  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
137
+ args = {
138
+ expires: expires_in,
139
+ query: {
140
+ "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
141
+ "response-content-type" => content_type
142
+ }
106
143
  }
144
+
145
+ if @config[:iam]
146
+ args[:issuer] = issuer
147
+ args[:signer] = signer
148
+ end
149
+
150
+ file_for(key).signed_url(**args)
107
151
  end
108
152
 
109
153
  def public_url(key, **)
@@ -137,7 +181,47 @@ module ActiveStorage
137
181
  end
138
182
 
139
183
  def client
140
- @client ||= Google::Cloud::Storage.new(**config.except(:bucket))
184
+ @client ||= Google::Cloud::Storage.new(**config.except(:bucket, :cache_control, :iam, :gsa_email))
185
+ end
186
+
187
+ def issuer
188
+ @issuer ||= if @config[:gsa_email]
189
+ @config[:gsa_email]
190
+ else
191
+ uri = URI.parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email")
192
+ http = Net::HTTP.new(uri.host, uri.port)
193
+ request = Net::HTTP::Get.new(uri.request_uri)
194
+ request["Metadata-Flavor"] = "Google"
195
+
196
+ begin
197
+ response = http.request(request)
198
+ rescue SocketError
199
+ raise MetadataServerNotFoundError
200
+ end
201
+
202
+ if response.is_a?(Net::HTTPSuccess)
203
+ response.body
204
+ else
205
+ raise MetadataServerError
206
+ end
207
+ end
208
+ end
209
+
210
+ def signer
211
+ # https://googleapis.dev/ruby/google-cloud-storage/latest/Google/Cloud/Storage/Project.html#signed_url-instance_method
212
+ lambda do |string_to_sign|
213
+ iam_client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new
214
+
215
+ scopes = ["https://www.googleapis.com/auth/iam"]
216
+ iam_client.authorization = Google::Auth.get_application_default(scopes)
217
+
218
+ request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
219
+ payload: string_to_sign
220
+ )
221
+ resource = "projects/-/serviceAccounts/#{issuer}"
222
+ response = iam_client.sign_service_account_blob(resource, request)
223
+ response.signed_blob
224
+ end
141
225
  end
142
226
  end
143
227
  end