activestorage 6.1.4 → 7.0.0.rc1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -200
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +856 -0
  6. data/app/assets/javascripts/activestorage.js +270 -377
  7. data/app/controllers/active_storage/base_controller.rb +1 -10
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/direct_uploads_controller.rb +7 -1
  11. data/app/controllers/active_storage/disk_controller.rb +1 -0
  12. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  13. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
  14. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  15. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  16. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  17. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  18. data/app/javascript/activestorage/blob_record.js +10 -3
  19. data/app/javascript/activestorage/direct_upload.js +4 -2
  20. data/app/javascript/activestorage/direct_upload_controller.js +9 -1
  21. data/app/javascript/activestorage/ujs.js +1 -1
  22. data/app/models/active_storage/attachment.rb +36 -3
  23. data/app/models/active_storage/blob/representable.rb +7 -5
  24. data/app/models/active_storage/blob.rb +92 -36
  25. data/app/models/active_storage/current.rb +12 -2
  26. data/app/models/active_storage/preview.rb +6 -4
  27. data/app/models/active_storage/record.rb +1 -1
  28. data/app/models/active_storage/variant.rb +3 -6
  29. data/app/models/active_storage/variant_record.rb +2 -0
  30. data/app/models/active_storage/variant_with_record.rb +9 -5
  31. data/app/models/active_storage/variation.rb +2 -2
  32. data/config/routes.rb +10 -10
  33. data/db/migrate/20170806125915_create_active_storage_tables.rb +32 -11
  34. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +15 -2
  35. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +5 -0
  36. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  37. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  38. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  39. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  40. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  41. data/lib/active_storage/analyzer.rb +8 -4
  42. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  43. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  44. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  45. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  46. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  47. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  48. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  49. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  50. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  51. data/lib/active_storage/attached/changes.rb +7 -1
  52. data/lib/active_storage/attached/many.rb +27 -15
  53. data/lib/active_storage/attached/model.rb +31 -5
  54. data/lib/active_storage/attached/one.rb +32 -27
  55. data/lib/active_storage/direct_upload_token.rb +59 -0
  56. data/lib/active_storage/downloader.rb +4 -4
  57. data/lib/active_storage/engine.rb +30 -1
  58. data/lib/active_storage/errors.rb +3 -0
  59. data/lib/active_storage/fixture_set.rb +76 -0
  60. data/lib/active_storage/gem_version.rb +4 -4
  61. data/lib/active_storage/previewer.rb +4 -4
  62. data/lib/active_storage/reflection.rb +12 -2
  63. data/lib/active_storage/service/azure_storage_service.rb +28 -6
  64. data/lib/active_storage/service/configurator.rb +1 -1
  65. data/lib/active_storage/service/disk_service.rb +24 -19
  66. data/lib/active_storage/service/gcs_service.rb +109 -11
  67. data/lib/active_storage/service/mirror_service.rb +2 -2
  68. data/lib/active_storage/service/registry.rb +1 -1
  69. data/lib/active_storage/service/s3_service.rb +37 -15
  70. data/lib/active_storage/service.rb +13 -5
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
  72. data/lib/active_storage/transformers/transformer.rb +1 -1
  73. data/lib/active_storage.rb +6 -1
  74. metadata +32 -20
  75. 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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module DirectUploadToken
5
+ extend self
6
+
7
+ SEPARATOR = "."
8
+ DIRECT_UPLOAD_TOKEN_LENGTH = 32
9
+
10
+ def generate_direct_upload_token(attachment_name, service_name, session)
11
+ token = direct_upload_token(session, attachment_name)
12
+ encode_direct_upload_token([service_name, token].join(SEPARATOR))
13
+ end
14
+
15
+ def verify_direct_upload_token(token, attachment_name, session)
16
+ raise ActiveStorage::InvalidDirectUploadTokenError if token.nil?
17
+
18
+ service_name, *token_components = decode_token(token).split(SEPARATOR)
19
+ decoded_token = token_components.join(SEPARATOR)
20
+
21
+ return service_name if valid_direct_upload_token?(decoded_token, attachment_name, session)
22
+
23
+ raise ActiveStorage::InvalidDirectUploadTokenError
24
+ end
25
+
26
+ private
27
+ def direct_upload_token(session, attachment_name) # :doc:
28
+ direct_upload_token_hmac(session, "direct_upload##{attachment_name}")
29
+ end
30
+
31
+ def valid_direct_upload_token?(token, attachment_name, session) # :doc:
32
+ correct_token = direct_upload_token(session, attachment_name)
33
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
34
+ rescue ArgumentError
35
+ raise ActiveStorage::InvalidDirectUploadTokenError
36
+ end
37
+
38
+ def direct_upload_token_hmac(session, identifier) # :doc:
39
+ OpenSSL::HMAC.digest(
40
+ OpenSSL::Digest::SHA256.new,
41
+ real_direct_upload_token(session),
42
+ identifier
43
+ )
44
+ end
45
+
46
+ def real_direct_upload_token(session) # :doc:
47
+ session[:_direct_upload_token] ||= SecureRandom.urlsafe_base64(DIRECT_UPLOAD_TOKEN_LENGTH, padding: false)
48
+ encode_direct_upload_token(session[:_direct_upload_token])
49
+ end
50
+
51
+ def decode_token(encoded_token) # :nodoc:
52
+ Base64.urlsafe_decode64(encoded_token)
53
+ end
54
+
55
+ def encode_direct_upload_token(raw_token) # :nodoc:
56
+ Base64.urlsafe_encode64(raw_token)
57
+ end
58
+ end
59
+ end
@@ -1,17 +1,17 @@
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)
8
8
  @service = service
9
9
  end
10
10
 
11
- def open(key, checksum:, name: "ActiveStorage-", tmpdir: nil)
11
+ def open(key, checksum: nil, verify: true, name: "ActiveStorage-", tmpdir: nil)
12
12
  open_tempfile(name, tmpdir) do |file|
13
13
  download key, file
14
- verify_integrity_of file, checksum: checksum
14
+ verify_integrity_of(file, checksum: checksum) if verify
15
15
  yield file
16
16
  end
17
17
  end
@@ -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,10 +96,13 @@ 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"
96
103
 
104
+ ActiveStorage.silence_invalid_content_types_warning = app.config.active_storage.silence_invalid_content_types_warning || false
105
+
97
106
  ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
98
107
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
99
108
  end
@@ -144,5 +153,25 @@ module ActiveStorage
144
153
  ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
145
154
  end
146
155
  end
156
+
157
+ initializer "active_storage.asset" do
158
+ if Rails.application.config.respond_to?(:assets)
159
+ Rails.application.config.assets.precompile += %w( activestorage activestorage.esm )
160
+ end
161
+ end
162
+
163
+ initializer "active_storage.fixture_set" do
164
+ ActiveSupport.on_load(:active_record_fixture_set) do
165
+ ActiveStorage::FixtureSet.file_fixture_path ||= Rails.root.join(*[
166
+ ENV.fetch("FIXTURES_PATH") { File.join("test", "fixtures") },
167
+ ENV["FIXTURES_DIR"],
168
+ "files"
169
+ ].compact_blank)
170
+ end
171
+
172
+ ActiveSupport.on_load(:active_support_test_case) do
173
+ ActiveStorage::FixtureSet.file_fixture_path = ActiveSupport::TestCase.file_fixture_path
174
+ end
175
+ end
147
176
  end
148
177
  end
@@ -26,4 +26,7 @@ module ActiveStorage
26
26
 
27
27
  # Raised when a Previewer is unable to generate a preview image.
28
28
  class PreviewError < Error; end
29
+
30
+ # Raised when direct upload fails because of the invalid token
31
+ class InvalidDirectUploadTokenError < Error; end
29
32
  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 = nil
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "rc1"
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"
@@ -19,12 +19,12 @@ module ActiveStorage
19
19
  @public = public
20
20
  end
21
21
 
22
- def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
22
+ def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
23
23
  instrument :upload, key: key, checksum: checksum do
24
24
  handle_errors do
25
25
  content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
26
26
 
27
- client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
27
+ client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
28
28
  end
29
29
  end
30
30
  end
@@ -86,7 +86,7 @@ module ActiveStorage
86
86
  end
87
87
  end
88
88
 
89
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
89
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
90
90
  instrument :url, key: key do |payload|
91
91
  generated_url = signer.signed_uri(
92
92
  uri_for(key), false,
@@ -101,10 +101,28 @@ module ActiveStorage
101
101
  end
102
102
  end
103
103
 
104
- def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
104
+ def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
105
105
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
106
106
 
107
- { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob" }
107
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob", **custom_metadata_headers(custom_metadata) }
108
+ end
109
+
110
+ def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
111
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
112
+
113
+ client.create_append_blob(
114
+ container,
115
+ destination_key,
116
+ content_type: content_type,
117
+ content_disposition: content_disposition,
118
+ metadata: custom_metadata,
119
+ ).tap do |blob|
120
+ source_keys.each do |source_key|
121
+ stream(source_key) do |chunk|
122
+ client.append_blob_block(container, blob.name, chunk)
123
+ end
124
+ end
125
+ end
108
126
  end
109
127
 
110
128
  private
@@ -166,5 +184,9 @@ module ActiveStorage
166
184
  raise
167
185
  end
168
186
  end
187
+
188
+ def custom_metadata_headers(metadata)
189
+ metadata.transform_keys { |key| "x-ms-meta-#{key}" }
190
+ end
169
191
  end
170
192
  end
@@ -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
@@ -72,7 +72,7 @@ module ActiveStorage
72
72
  end
73
73
  end
74
74
 
75
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
75
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
76
76
  instrument :url, key: key do |payload|
77
77
  verified_token_with_expiration = ActiveStorage.verifier.generate(
78
78
  {
@@ -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,10 +96,20 @@ 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
 
103
+ def compose(source_keys, destination_key, **)
104
+ File.open(make_path_for(destination_key), "w") do |destination_file|
105
+ source_keys.each do |source_key|
106
+ File.open(path_for(source_key), "rb") do |source_file|
107
+ IO.copy_stream(source_file, destination_file)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
105
113
  private
106
114
  def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
107
115
  generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition)
@@ -124,14 +132,11 @@ module ActiveStorage
124
132
  purpose: :blob_key
125
133
  )
126
134
 
127
- current_uri = URI.parse(current_host)
135
+ if url_options.blank?
136
+ raise ArgumentError, "Cannot generate URL for #{filename} using Disk service, please set ActiveStorage::Current.url_options."
137
+ end
128
138
 
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
- )
139
+ url_helpers.rails_disk_service_url(verified_key_with_expiration, filename: filename, **url_options)
135
140
  end
136
141
 
137
142
 
@@ -154,7 +159,7 @@ module ActiveStorage
154
159
  end
155
160
 
156
161
  def ensure_integrity_of(key, checksum)
157
- unless Digest::MD5.file(path_for(key)).base64digest == checksum
162
+ unless OpenSSL::Digest::MD5.file(path_for(key)).base64digest == checksum
158
163
  delete key
159
164
  raise ActiveStorage::IntegrityError
160
165
  end
@@ -164,8 +169,8 @@ module ActiveStorage
164
169
  @url_helpers ||= Rails.application.routes.url_helpers
165
170
  end
166
171
 
167
- def current_host
168
- ActiveStorage::Current.host
172
+ def url_options
173
+ ActiveStorage::Current.url_options
169
174
  end
170
175
  end
171
176
  end