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
@@ -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,33 +27,33 @@ 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
33
+ config.active_storage.precompile_assets = true
30
34
 
31
35
  config.active_storage.variable_content_types = %w(
32
36
  image/png
33
37
  image/gif
34
- image/jpg
35
38
  image/jpeg
36
- image/pjpeg
37
39
  image/tiff
38
40
  image/bmp
39
41
  image/vnd.adobe.photoshop
40
42
  image/vnd.microsoft.icon
41
43
  image/webp
44
+ image/avif
45
+ image/heic
46
+ image/heif
42
47
  )
43
48
 
44
49
  config.active_storage.web_image_content_types = %w(
45
50
  image/png
46
51
  image/jpeg
47
- image/jpg
48
52
  image/gif
49
53
  )
50
54
 
51
55
  config.active_storage.content_types_to_serve_as_binary = %w(
52
56
  text/html
53
- text/javascript
54
57
  image/svg+xml
55
58
  application/postscript
56
59
  application/x-shockwave-flash
@@ -64,7 +67,6 @@ module ActiveStorage
64
67
  config.active_storage.content_types_allowed_inline = %w(
65
68
  image/png
66
69
  image/gif
67
- image/jpg
68
70
  image/jpeg
69
71
  image/tiff
70
72
  image/bmp
@@ -75,6 +77,10 @@ module ActiveStorage
75
77
 
76
78
  config.eager_load_namespaces << ActiveStorage
77
79
 
80
+ initializer "active_storage.deprecator", before: :load_environment_config do |app|
81
+ app.deprecators[:active_storage] = ActiveStorage.deprecator
82
+ end
83
+
78
84
  initializer "active_storage.configs" do
79
85
  config.after_initialize do |app|
80
86
  ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
@@ -105,11 +111,19 @@ module ActiveStorage
105
111
  ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
106
112
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
107
113
  ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
114
+ ActiveStorage.urls_expire_in = app.config.active_storage.urls_expire_in
108
115
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
109
116
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
110
117
  ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
111
118
 
112
- ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
119
+ unless app.config.active_storage.silence_invalid_content_types_warning.nil?
120
+ ActiveStorage.silence_invalid_content_types_warning = app.config.active_storage.silence_invalid_content_types_warning
121
+ end
122
+
123
+ unless app.config.active_storage.replace_on_assign_to_many.nil?
124
+ ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many
125
+ end
126
+
113
127
  ActiveStorage.track_variants = app.config.active_storage.track_variants || false
114
128
  end
115
129
  end
@@ -159,5 +173,39 @@ module ActiveStorage
159
173
  ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
160
174
  end
161
175
  end
176
+
177
+ initializer "action_view.configuration" do
178
+ config.after_initialize do |app|
179
+ ActiveSupport.on_load(:action_view) do
180
+ multiple_file_field_include_hidden = app.config.active_storage.delete(:multiple_file_field_include_hidden)
181
+
182
+ unless multiple_file_field_include_hidden.nil?
183
+ ActionView::Helpers::FormHelper.multiple_file_field_include_hidden = multiple_file_field_include_hidden
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ initializer "active_storage.asset" do
190
+ config.after_initialize do |app|
191
+ if app.config.respond_to?(:assets) && app.config.active_storage.precompile_assets
192
+ app.config.assets.precompile += %w( activestorage activestorage.esm )
193
+ end
194
+ end
195
+ end
196
+
197
+ initializer "active_storage.fixture_set" do
198
+ ActiveSupport.on_load(:active_record_fixture_set) do
199
+ ActiveStorage::FixtureSet.file_fixture_path ||= Rails.root.join(*[
200
+ ENV.fetch("FIXTURES_PATH") { File.join("test", "fixtures") },
201
+ ENV["FIXTURES_DIR"],
202
+ "files"
203
+ ].compact_blank)
204
+ end
205
+
206
+ ActiveSupport.on_load(:active_support_test_case) do
207
+ ActiveStorage::FixtureSet.file_fixture_path = ActiveSupport::TestCase.file_fixture_path
208
+ end
209
+ end
162
210
  end
163
211
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/testing/file_fixtures"
4
+ require "active_record/secure_token"
5
+
6
+ module ActiveStorage
7
+ # = Active Storage \FixtureSet
8
+ #
9
+ # Fixtures are a way of organizing data that you want to test against; in
10
+ # short, sample data.
11
+ #
12
+ # To learn more about fixtures, read the ActiveRecord::FixtureSet documentation.
13
+ #
14
+ # === YAML
15
+ #
16
+ # Like other Active Record-backed models, ActiveStorage::Attachment and
17
+ # ActiveStorage::Blob records inherit from ActiveRecord::Base instances and
18
+ # therefore can be populated by fixtures.
19
+ #
20
+ # Consider a hypothetical <tt>Article</tt> model class, its related
21
+ # fixture data, as well as fixture data for related ActiveStorage::Attachment
22
+ # and ActiveStorage::Blob records:
23
+ #
24
+ # # app/models/article.rb
25
+ # class Article < ApplicationRecord
26
+ # has_one_attached :thumbnail
27
+ # end
28
+ #
29
+ # # fixtures/active_storage/blobs.yml
30
+ # first_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename: "first.png" %>
31
+ #
32
+ # # fixtures/active_storage/attachments.yml
33
+ # first_thumbnail_attachment:
34
+ # name: thumbnail
35
+ # record: first (Article)
36
+ # blob: first_thumbnail_blob
37
+ #
38
+ # When processed, Active Record will insert database records for each fixture
39
+ # entry and will ensure the Active Storage relationship is intact.
40
+ class FixtureSet
41
+ include ActiveSupport::Testing::FileFixtures
42
+ include ActiveRecord::SecureToken
43
+
44
+ # Generate a YAML-encoded representation of an ActiveStorage::Blob
45
+ # instance's attributes, resolve the file relative to the directory mentioned
46
+ # by ActiveSupport::Testing::FileFixtures.file_fixture, and upload
47
+ # the file to the Service
48
+ #
49
+ # === Examples
50
+ #
51
+ # # tests/fixtures/action_text/blobs.yml
52
+ # second_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
53
+ # filename: "second.svg",
54
+ # ) %>
55
+ #
56
+ # third_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob(
57
+ # filename: "third.svg",
58
+ # content_type: "image/svg+xml",
59
+ # service_name: "public"
60
+ # ) %>
61
+ #
62
+ def self.blob(filename:, **attributes)
63
+ new.prepare Blob.new(filename: filename, key: generate_unique_secure_token), **attributes
64
+ end
65
+
66
+ def prepare(instance, **attributes)
67
+ io = file_fixture(instance.filename.to_s).open
68
+ instance.unfurl(io)
69
+ instance.assign_attributes(attributes)
70
+ instance.upload_without_unfurling(io)
71
+
72
+ instance.attributes.transform_values { |value| value.is_a?(Hash) ? value.to_json : value }.compact.to_json
73
+ end
74
+ end
75
+ end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- # Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of Active Storage as a +Gem::Version+.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
10
+ MAJOR = 7
11
11
  MINOR = 1
12
- TINY = 7
12
+ TINY = 0
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -9,34 +9,46 @@ module ActiveStorage
9
9
  message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
10
10
  info event, color(message, GREEN)
11
11
  end
12
+ subscribe_log_level :service_upload, :info
12
13
 
13
14
  def service_download(event)
14
15
  info event, color("Downloaded file from key: #{key_in(event)}", BLUE)
15
16
  end
17
+ subscribe_log_level :service_download, :info
16
18
 
17
19
  alias_method :service_streaming_download, :service_download
18
20
 
21
+ def preview(event)
22
+ info event, color("Previewed file from key: #{key_in(event)}", BLUE)
23
+ end
24
+ subscribe_log_level :preview, :info
25
+
19
26
  def service_delete(event)
20
27
  info event, color("Deleted file from key: #{key_in(event)}", RED)
21
28
  end
29
+ subscribe_log_level :service_delete, :info
22
30
 
23
31
  def service_delete_prefixed(event)
24
32
  info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED)
25
33
  end
34
+ subscribe_log_level :service_delete_prefixed, :info
26
35
 
27
36
  def service_exist(event)
28
37
  debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
29
38
  end
39
+ subscribe_log_level :service_exist, :debug
30
40
 
31
41
  def service_url(event)
32
42
  debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
33
43
  end
44
+ subscribe_log_level :service_url, :debug
34
45
 
35
46
  def service_mirror(event)
36
47
  message = "Mirrored file at key: #{key_in(event)}"
37
48
  message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
38
49
  debug event, color(message, GREEN)
39
50
  end
51
+ subscribe_log_level :service_mirror, :debug
40
52
 
41
53
  def logger
42
54
  ActiveStorage.logger
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
+ # = Active Storage \Previewer
5
+ #
4
6
  # This is an abstract base class for previewers, which generate images from blobs. See
5
7
  # ActiveStorage::Previewer::MuPDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for
6
8
  # examples of concrete subclasses.
@@ -26,7 +28,7 @@ module ActiveStorage
26
28
 
27
29
  private
28
30
  # Downloads the blob to a tempfile on disk. Yields the tempfile.
29
- def download_blob_to_tempfile(&block) #:doc:
31
+ def download_blob_to_tempfile(&block) # :doc:
30
32
  blob.open tmpdir: tmpdir, &block
31
33
  end
32
34
 
@@ -44,7 +46,7 @@ module ActiveStorage
44
46
  # end
45
47
  #
46
48
  # The output tempfile is opened in the directory returned by #tmpdir.
47
- def draw(*argv) #:doc:
49
+ def draw(*argv) # :doc:
48
50
  open_tempfile do |file|
49
51
  instrument :preview, key: blob.key do
50
52
  capture(*argv, to: file)
@@ -65,7 +67,12 @@ module ActiveStorage
65
67
  end
66
68
 
67
69
  def instrument(operation, payload = {}, &block)
68
- ActiveSupport::Notifications.instrument "#{operation}.active_storage", payload, &block
70
+ ActiveSupport::Notifications.instrument "#{operation}.active_storage", payload.merge(service: service_name), &block
71
+ end
72
+
73
+ def service_name
74
+ # ActiveStorage::Service::DiskService => Disk
75
+ blob.service.class.to_s.split("::").third.remove("Service")
69
76
  end
70
77
 
71
78
  def capture(*argv, to:)
@@ -83,11 +90,11 @@ module ActiveStorage
83
90
  to.rewind
84
91
  end
85
92
 
86
- def logger #:doc:
93
+ def logger # :doc:
87
94
  ActiveStorage.logger
88
95
  end
89
96
 
90
- def tmpdir #:doc:
97
+ def tmpdir # :doc:
91
98
  Dir.tmpdir
92
99
  end
93
100
  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
+ named_variants[name] = NamedVariant.new(transformations)
8
+ end
9
+
10
+ def named_variants
11
+ @named_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,12 +1,14 @@
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"
7
7
  require "azure/storage/common/core/auth/shared_access_signature"
8
8
 
9
9
  module ActiveStorage
10
+ # = Active Storage \Azure Storage \Service
11
+ #
10
12
  # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
11
13
  # See ActiveStorage::Service for the generic API documentation that applies to all services.
12
14
  class Service::AzureStorageService < Service
@@ -19,12 +21,12 @@ module ActiveStorage
19
21
  @public = public
20
22
  end
21
23
 
22
- def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
24
+ def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
23
25
  instrument :upload, key: key, checksum: checksum do
24
26
  handle_errors do
25
27
  content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
26
28
 
27
- client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
29
+ 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
30
  end
29
31
  end
30
32
  end
@@ -86,7 +88,7 @@ module ActiveStorage
86
88
  end
87
89
  end
88
90
 
89
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
91
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
90
92
  instrument :url, key: key do |payload|
91
93
  generated_url = signer.signed_uri(
92
94
  uri_for(key), false,
@@ -101,10 +103,28 @@ module ActiveStorage
101
103
  end
102
104
  end
103
105
 
104
- def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
106
+ def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
105
107
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
106
108
 
107
- { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob" }
109
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob", **custom_metadata_headers(custom_metadata) }
110
+ end
111
+
112
+ def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
113
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
114
+
115
+ client.create_append_blob(
116
+ container,
117
+ destination_key,
118
+ content_type: content_type,
119
+ content_disposition: content_disposition,
120
+ metadata: custom_metadata,
121
+ ).tap do |blob|
122
+ source_keys.each do |source_key|
123
+ stream(source_key) do |chunk|
124
+ client.append_blob_block(container, blob.name, chunk)
125
+ end
126
+ end
127
+ end
108
128
  end
109
129
 
110
130
  private
@@ -166,5 +186,9 @@ module ActiveStorage
166
186
  raise
167
187
  end
168
188
  end
189
+
190
+ def custom_metadata_headers(metadata)
191
+ metadata.transform_keys { |key| "x-ms-meta-#{key}" }
192
+ end
169
193
  end
170
194
  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,16 @@
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
+ # = Active Storage \Disk \Service
10
+ #
9
11
  # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
10
12
  # documentation that applies to all services.
11
13
  class Service::DiskService < Service
12
- attr_reader :root
14
+ attr_accessor :root
13
15
 
14
16
  def initialize(root:, public: false, **options)
15
17
  @root = root
@@ -72,7 +74,7 @@ module ActiveStorage
72
74
  end
73
75
  end
74
76
 
75
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
77
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
76
78
  instrument :url, key: key do |payload|
77
79
  verified_token_with_expiration = ActiveStorage.verifier.generate(
78
80
  {
@@ -86,11 +88,9 @@ module ActiveStorage
86
88
  purpose: :blob_token
87
89
  )
88
90
 
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
91
+ url_helpers.update_rails_disk_service_url(verified_token_with_expiration, url_options).tap do |generated_url|
92
+ payload[:url] = generated_url
93
+ end
94
94
  end
95
95
  end
96
96
 
@@ -98,10 +98,20 @@ module ActiveStorage
98
98
  { "Content-Type" => content_type }
99
99
  end
100
100
 
101
- def path_for(key) #:nodoc:
101
+ def path_for(key) # :nodoc:
102
102
  File.join root, folder_for(key), key
103
103
  end
104
104
 
105
+ def compose(source_keys, destination_key, **)
106
+ File.open(make_path_for(destination_key), "w") do |destination_file|
107
+ source_keys.each do |source_key|
108
+ File.open(path_for(source_key), "rb") do |source_file|
109
+ IO.copy_stream(source_file, destination_file)
110
+ end
111
+ end
112
+ end
113
+ end
114
+
105
115
  private
106
116
  def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
107
117
  generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition)
@@ -124,14 +134,11 @@ module ActiveStorage
124
134
  purpose: :blob_key
125
135
  )
126
136
 
127
- current_uri = URI.parse(current_host)
137
+ if url_options.blank?
138
+ raise ArgumentError, "Cannot generate URL for #{filename} using Disk service, please set ActiveStorage::Current.url_options."
139
+ end
128
140
 
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
- )
141
+ url_helpers.rails_disk_service_url(verified_key_with_expiration, filename: filename, **url_options)
135
142
  end
136
143
 
137
144
 
@@ -154,7 +161,7 @@ module ActiveStorage
154
161
  end
155
162
 
156
163
  def ensure_integrity_of(key, checksum)
157
- unless Digest::MD5.file(path_for(key)).base64digest == checksum
164
+ unless OpenSSL::Digest::MD5.file(path_for(key)).base64digest == checksum
158
165
  delete key
159
166
  raise ActiveStorage::IntegrityError
160
167
  end
@@ -164,8 +171,8 @@ module ActiveStorage
164
171
  @url_helpers ||= Rails.application.routes.url_helpers
165
172
  end
166
173
 
167
- def current_host
168
- ActiveStorage::Current.host
174
+ def url_options
175
+ ActiveStorage::Current.url_options
169
176
  end
170
177
  end
171
178
  end