activestorage 6.0.4.6 → 6.1.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +137 -213
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +36 -4
  5. data/app/controllers/active_storage/base_controller.rb +11 -0
  6. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  7. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
  8. data/app/controllers/active_storage/disk_controller.rb +8 -20
  9. data/app/controllers/active_storage/representations/proxy_controller.rb +19 -0
  10. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -2
  11. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  12. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  13. data/app/controllers/concerns/active_storage/set_current.rb +2 -2
  14. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  15. data/app/jobs/active_storage/mirror_job.rb +15 -0
  16. data/app/models/active_storage/attachment.rb +18 -10
  17. data/app/models/active_storage/blob/analyzable.rb +6 -2
  18. data/app/models/active_storage/blob/identifiable.rb +7 -6
  19. data/app/models/active_storage/blob/representable.rb +34 -4
  20. data/app/models/active_storage/blob.rb +114 -57
  21. data/app/models/active_storage/preview.rb +31 -10
  22. data/app/models/active_storage/record.rb +7 -0
  23. data/app/models/active_storage/variant.rb +28 -41
  24. data/app/models/active_storage/variant_record.rb +8 -0
  25. data/app/models/active_storage/variant_with_record.rb +54 -0
  26. data/app/models/active_storage/variation.rb +25 -20
  27. data/config/routes.rb +58 -8
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  31. data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
  32. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  33. data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
  34. data/lib/active_storage/analyzer.rb +6 -0
  35. data/lib/active_storage/attached/changes/create_many.rb +1 -0
  36. data/lib/active_storage/attached/changes/create_one.rb +17 -4
  37. data/lib/active_storage/attached/many.rb +4 -3
  38. data/lib/active_storage/attached/model.rb +49 -10
  39. data/lib/active_storage/attached/one.rb +4 -3
  40. data/lib/active_storage/engine.rb +25 -27
  41. data/lib/active_storage/gem_version.rb +3 -3
  42. data/lib/active_storage/log_subscriber.rb +6 -0
  43. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  44. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  45. data/lib/active_storage/previewer/video_previewer.rb +2 -2
  46. data/lib/active_storage/previewer.rb +3 -2
  47. data/lib/active_storage/service/azure_storage_service.rb +40 -35
  48. data/lib/active_storage/service/configurator.rb +3 -1
  49. data/lib/active_storage/service/disk_service.rb +36 -31
  50. data/lib/active_storage/service/gcs_service.rb +18 -16
  51. data/lib/active_storage/service/mirror_service.rb +31 -7
  52. data/lib/active_storage/service/registry.rb +32 -0
  53. data/lib/active_storage/service/s3_service.rb +51 -23
  54. data/lib/active_storage/service.rb +35 -7
  55. data/lib/active_storage/transformers/image_processing_transformer.rb +13 -7
  56. data/lib/active_storage/transformers/transformer.rb +0 -3
  57. data/lib/active_storage.rb +5 -2
  58. metadata +60 -24
  59. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
  60. data/lib/active_storage/downloading.rb +0 -47
  61. data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
@@ -3,7 +3,7 @@
3
3
  module ActiveStorage
4
4
  # Representation of a single attachment to a model.
5
5
  class Attached::One < Attached
6
- delegate_missing_to :attachment
6
+ delegate_missing_to :attachment, allow_nil: true
7
7
 
8
8
  # Returns the associated attachment record.
9
9
  #
@@ -29,7 +29,8 @@ module ActiveStorage
29
29
  # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
30
30
  def attach(attachable)
31
31
  if record.persisted? && !record.changed?
32
- record.update(name => attachable)
32
+ record.public_send("#{name}=", attachable)
33
+ record.save
33
34
  else
34
35
  record.public_send("#{name}=", attachable)
35
36
  end
@@ -37,7 +38,7 @@ module ActiveStorage
37
38
 
38
39
  # Returns +true+ if an attachment has been made.
39
40
  #
40
- # class User < ActiveRecord::Base
41
+ # class User < ApplicationRecord
41
42
  # has_one_attached :avatar
42
43
  # end
43
44
  #
@@ -14,6 +14,8 @@ require "active_storage/previewer/video_previewer"
14
14
  require "active_storage/analyzer/image_analyzer"
15
15
  require "active_storage/analyzer/video_analyzer"
16
16
 
17
+ require "active_storage/service/registry"
18
+
17
19
  require "active_storage/reflection"
18
20
 
19
21
  module ActiveStorage
@@ -24,7 +26,7 @@ module ActiveStorage
24
26
  config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
25
27
  config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
26
28
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
27
- config.active_storage.queues = ActiveSupport::OrderedOptions.new
29
+ config.active_storage.queues = ActiveSupport::InheritableOptions.new(mirror: :active_storage_mirror)
28
30
 
29
31
  config.active_storage.variable_content_types = %w(
30
32
  image/png
@@ -36,6 +38,14 @@ module ActiveStorage
36
38
  image/bmp
37
39
  image/vnd.adobe.photoshop
38
40
  image/vnd.microsoft.icon
41
+ image/webp
42
+ )
43
+
44
+ config.active_storage.web_image_content_types = %w(
45
+ image/png
46
+ image/jpeg
47
+ image/jpg
48
+ image/gif
39
49
  )
40
50
 
41
51
  config.active_storage.content_types_to_serve_as_binary = %w(
@@ -73,14 +83,18 @@ module ActiveStorage
73
83
  ActiveStorage.analyzers = app.config.active_storage.analyzers || []
74
84
  ActiveStorage.paths = app.config.active_storage.paths || {}
75
85
  ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
86
+ ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
87
+ ActiveStorage.resolve_model_to_route = app.config.active_storage.resolve_model_to_route || :rails_storage_redirect
76
88
 
77
89
  ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
90
+ ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
78
91
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
79
92
  ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
80
93
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
81
94
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
82
95
 
83
96
  ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
97
+ ActiveStorage.track_variants = app.config.active_storage.track_variants || false
84
98
  end
85
99
  end
86
100
 
@@ -100,42 +114,26 @@ module ActiveStorage
100
114
 
101
115
  initializer "active_storage.services" do
102
116
  ActiveSupport.on_load(:active_storage_blob) do
103
- if config_choice = Rails.configuration.active_storage.service
104
- configs = Rails.configuration.active_storage.service_configurations ||= begin
105
- config_file = Pathname.new(Rails.root.join("config/storage.yml"))
117
+ configs = Rails.configuration.active_storage.service_configurations ||=
118
+ begin
119
+ config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
120
+ config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
106
121
  raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
107
122
 
108
- require "yaml"
109
- require "erb"
110
-
111
- YAML.load(ERB.new(config_file.read).result) || {}
112
- rescue Psych::SyntaxError => e
113
- raise "YAML syntax error occurred while parsing #{config_file}. " \
114
- "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
115
- "Error: #{e.message}"
123
+ ActiveSupport::ConfigurationFile.parse(config_file)
116
124
  end
117
125
 
118
- ActiveStorage::Blob.service =
119
- begin
120
- ActiveStorage::Service.configure config_choice, configs
121
- rescue => e
122
- raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
123
- end
126
+ ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
127
+
128
+ if config_choice = Rails.configuration.active_storage.service
129
+ ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
124
130
  end
125
131
  end
126
132
  end
127
133
 
128
134
  initializer "active_storage.queues" do
129
135
  config.after_initialize do |app|
130
- if queue = app.config.active_storage.queue
131
- ActiveSupport::Deprecation.warn \
132
- "config.active_storage.queue is deprecated and will be removed in Rails 6.1. " \
133
- "Set config.active_storage.queues.purge and config.active_storage.queues.analysis instead."
134
-
135
- ActiveStorage.queues = { purge: queue, analysis: queue }
136
- else
137
- ActiveStorage.queues = app.config.active_storage.queues || {}
138
- end
136
+ ActiveStorage.queues = app.config.active_storage.queues || {}
139
137
  end
140
138
  end
141
139
 
@@ -8,9 +8,9 @@ module ActiveStorage
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 6
11
- MINOR = 0
12
- TINY = 4
13
- PRE = "6"
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "rc1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,6 +32,12 @@ module ActiveStorage
32
32
  debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
33
33
  end
34
34
 
35
+ def service_mirror(event)
36
+ message = "Mirrored file at key: #{key_in(event)}"
37
+ message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
38
+ debug event, color(message, GREEN)
39
+ end
40
+
35
41
  def logger
36
42
  ActiveStorage.logger
37
43
  end
@@ -12,7 +12,7 @@ module ActiveStorage
12
12
  end
13
13
 
14
14
  def mutool_exists?
15
- return @mutool_exists unless @mutool_exists.nil?
15
+ return @mutool_exists if defined?(@mutool_exists) && !@mutool_exists.nil?
16
16
 
17
17
  system mutool_path, out: File::NULL, err: File::NULL
18
18
 
@@ -20,10 +20,10 @@ module ActiveStorage
20
20
  end
21
21
  end
22
22
 
23
- def preview
23
+ def preview(**options)
24
24
  download_blob_to_tempfile do |input|
25
25
  draw_first_page_from input do |output|
26
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
26
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png", **options
27
27
  end
28
28
  end
29
29
  end
@@ -18,10 +18,10 @@ module ActiveStorage
18
18
  end
19
19
  end
20
20
 
21
- def preview
21
+ def preview(**options)
22
22
  download_blob_to_tempfile do |input|
23
23
  draw_first_page_from input do |output|
24
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
24
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png", **options
25
25
  end
26
26
  end
27
27
  end
@@ -18,10 +18,10 @@ module ActiveStorage
18
18
  end
19
19
  end
20
20
 
21
- def preview
21
+ def preview(**options)
22
22
  download_blob_to_tempfile do |input|
23
23
  draw_relevant_frame_from input do |output|
24
- yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg"
24
+ yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg", **options
25
25
  end
26
26
  end
27
27
  end
@@ -18,8 +18,9 @@ module ActiveStorage
18
18
  end
19
19
 
20
20
  # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
21
- # anything accepted by ActiveStorage::Attached::One#attach).
22
- def preview
21
+ # anything accepted by ActiveStorage::Attached::One#attach). Pass the additional options to
22
+ # the underlying blob that is created.
23
+ def preview(**options)
23
24
  raise NotImplementedError
24
25
  end
25
26
 
@@ -1,26 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "azure-storage-blob", ">= 1.1"
4
+
3
5
  require "active_support/core_ext/numeric/bytes"
4
- require "azure/storage"
5
- require "azure/storage/core/auth/shared_access_signature"
6
+ require "azure/storage/blob"
7
+ require "azure/storage/common/core/auth/shared_access_signature"
6
8
 
7
9
  module ActiveStorage
8
10
  # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
9
11
  # See ActiveStorage::Service for the generic API documentation that applies to all services.
10
12
  class Service::AzureStorageService < Service
11
- attr_reader :client, :blobs, :container, :signer
13
+ attr_reader :client, :container, :signer
12
14
 
13
- def initialize(storage_account_name:, storage_access_key:, container:, **options)
14
- @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
15
- @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
16
- @blobs = client.blob_client
15
+ def initialize(storage_account_name:, storage_access_key:, container:, public: false, **options)
16
+ @client = Azure::Storage::Blob::BlobService.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
17
+ @signer = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
17
18
  @container = container
19
+ @public = public
18
20
  end
19
21
 
20
- def upload(key, io, checksum: nil, **)
22
+ def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
21
23
  instrument :upload, key: key, checksum: checksum do
22
24
  handle_errors do
23
- blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
25
+ content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
26
+
27
+ client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
24
28
  end
25
29
  end
26
30
  end
@@ -33,7 +37,7 @@ module ActiveStorage
33
37
  else
34
38
  instrument :download, key: key do
35
39
  handle_errors do
36
- _, io = blobs.get_blob(container, key)
40
+ _, io = client.get_blob(container, key)
37
41
  io.force_encoding(Encoding::BINARY)
38
42
  end
39
43
  end
@@ -43,7 +47,7 @@ module ActiveStorage
43
47
  def download_chunk(key, range)
44
48
  instrument :download_chunk, key: key, range: range do
45
49
  handle_errors do
46
- _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
50
+ _, io = client.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
47
51
  io.force_encoding(Encoding::BINARY)
48
52
  end
49
53
  end
@@ -51,7 +55,7 @@ module ActiveStorage
51
55
 
52
56
  def delete(key)
53
57
  instrument :delete, key: key do
54
- blobs.delete_blob(container, key)
58
+ client.delete_blob(container, key)
55
59
  rescue Azure::Core::Http::HTTPError => e
56
60
  raise unless e.type == "BlobNotFound"
57
61
  # Ignore files already deleted
@@ -63,10 +67,10 @@ module ActiveStorage
63
67
  marker = nil
64
68
 
65
69
  loop do
66
- results = blobs.list_blobs(container, prefix: prefix, marker: marker)
70
+ results = client.list_blobs(container, prefix: prefix, marker: marker)
67
71
 
68
72
  results.each do |blob|
69
- blobs.delete_blob(container, blob.name)
73
+ client.delete_blob(container, blob.name)
70
74
  end
71
75
 
72
76
  break unless marker = results.continuation_token.presence
@@ -82,15 +86,13 @@ module ActiveStorage
82
86
  end
83
87
  end
84
88
 
85
- def url(key, expires_in:, filename:, disposition:, content_type:)
89
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
86
90
  instrument :url, key: key do |payload|
87
91
  generated_url = signer.signed_uri(
88
92
  uri_for(key), false,
89
93
  service: "b",
90
- permissions: "r",
91
- expiry: format_expiry(expires_in),
92
- content_disposition: content_disposition_with(type: disposition, filename: filename),
93
- content_type: content_type
94
+ permissions: "rw",
95
+ expiry: format_expiry(expires_in)
94
96
  ).to_s
95
97
 
96
98
  payload[:url] = generated_url
@@ -99,32 +101,35 @@ module ActiveStorage
99
101
  end
100
102
  end
101
103
 
102
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
103
- instrument :url, key: key do |payload|
104
- generated_url = signer.signed_uri(
104
+ def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
105
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
106
+
107
+ { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob" }
108
+ end
109
+
110
+ private
111
+ def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
112
+ signer.signed_uri(
105
113
  uri_for(key), false,
106
114
  service: "b",
107
- permissions: "rw",
108
- expiry: format_expiry(expires_in)
115
+ permissions: "r",
116
+ expiry: format_expiry(expires_in),
117
+ content_disposition: content_disposition_with(type: disposition, filename: filename),
118
+ content_type: content_type
109
119
  ).to_s
120
+ end
110
121
 
111
- payload[:url] = generated_url
112
-
113
- generated_url
122
+ def public_url(key, **)
123
+ uri_for(key).to_s
114
124
  end
115
- end
116
125
 
117
- def headers_for_direct_upload(key, content_type:, checksum:, **)
118
- { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" }
119
- end
120
126
 
121
- private
122
127
  def uri_for(key)
123
- blobs.generate_uri("#{container}/#{key}")
128
+ client.generate_uri("#{container}/#{key}")
124
129
  end
125
130
 
126
131
  def blob_for(key)
127
- blobs.get_blob_properties(container, key)
132
+ client.get_blob_properties(container, key)
128
133
  rescue Azure::Core::Http::HTTPError
129
134
  false
130
135
  end
@@ -143,7 +148,7 @@ module ActiveStorage
143
148
  raise ActiveStorage::FileNotFoundError unless blob.present?
144
149
 
145
150
  while offset < blob.properties[:content_length]
146
- _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
151
+ _, chunk = client.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
147
152
  yield chunk.force_encoding(Encoding::BINARY)
148
153
  offset += chunk_size
149
154
  end
@@ -14,7 +14,9 @@ module ActiveStorage
14
14
 
15
15
  def build(service_name)
16
16
  config = config_for(service_name.to_sym)
17
- resolve(config.fetch(:service)).build(**config, configurator: self)
17
+ resolve(config.fetch(:service)).build(
18
+ **config, configurator: self, name: service_name
19
+ )
18
20
  end
19
21
 
20
22
  private
@@ -11,8 +11,9 @@ module ActiveStorage
11
11
  class Service::DiskService < Service
12
12
  attr_reader :root
13
13
 
14
- def initialize(root:)
14
+ def initialize(root:, public: false, **options)
15
15
  @root = root
16
+ @public = public
16
17
  end
17
18
 
18
19
  def upload(key, io, checksum: nil, **)
@@ -71,35 +72,6 @@ module ActiveStorage
71
72
  end
72
73
  end
73
74
 
74
- def url(key, expires_in:, filename:, disposition:, content_type:)
75
- instrument :url, key: key do |payload|
76
- content_disposition = content_disposition_with(type: disposition, filename: filename)
77
- verified_key_with_expiration = ActiveStorage.verifier.generate(
78
- {
79
- key: key,
80
- disposition: content_disposition,
81
- content_type: content_type
82
- },
83
- expires_in: expires_in,
84
- purpose: :blob_key
85
- )
86
-
87
- current_uri = URI.parse(current_host)
88
-
89
- generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
90
- protocol: current_uri.scheme,
91
- host: current_uri.host,
92
- port: current_uri.port,
93
- disposition: content_disposition,
94
- content_type: content_type,
95
- filename: filename
96
- )
97
- payload[:url] = generated_url
98
-
99
- generated_url
100
- end
101
- end
102
-
103
75
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
104
76
  instrument :url, key: key do |payload|
105
77
  verified_token_with_expiration = ActiveStorage.verifier.generate(
@@ -107,7 +79,8 @@ module ActiveStorage
107
79
  key: key,
108
80
  content_type: content_type,
109
81
  content_length: content_length,
110
- checksum: checksum
82
+ checksum: checksum,
83
+ service_name: name
111
84
  },
112
85
  expires_in: expires_in,
113
86
  purpose: :blob_token
@@ -130,6 +103,38 @@ module ActiveStorage
130
103
  end
131
104
 
132
105
  private
106
+ def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
107
+ generate_url(key, expires_in: expires_in, filename: filename, content_type: content_type, disposition: disposition)
108
+ end
109
+
110
+ def public_url(key, filename:, content_type: nil, disposition: :attachment, **)
111
+ generate_url(key, expires_in: nil, filename: filename, content_type: content_type, disposition: disposition)
112
+ end
113
+
114
+ def generate_url(key, expires_in:, filename:, content_type:, disposition:)
115
+ content_disposition = content_disposition_with(type: disposition, filename: filename)
116
+ verified_key_with_expiration = ActiveStorage.verifier.generate(
117
+ {
118
+ key: key,
119
+ disposition: content_disposition,
120
+ content_type: content_type,
121
+ service_name: name
122
+ },
123
+ expires_in: expires_in,
124
+ purpose: :blob_key
125
+ )
126
+
127
+ current_uri = URI.parse(current_host)
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
+ )
135
+ end
136
+
137
+
133
138
  def stream(key)
134
139
  File.open(path_for(key), "rb") do |file|
135
140
  while data = file.read(5.megabytes)
@@ -7,8 +7,9 @@ module ActiveStorage
7
7
  # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
8
8
  # documentation that applies to all services.
9
9
  class Service::GCSService < Service
10
- def initialize(**config)
10
+ def initialize(public: false, **config)
11
11
  @config = config
12
+ @public = public
12
13
  end
13
14
 
14
15
  def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
@@ -81,19 +82,6 @@ module ActiveStorage
81
82
  end
82
83
  end
83
84
 
84
- def url(key, expires_in:, filename:, content_type:, disposition:)
85
- instrument :url, key: key do |payload|
86
- generated_url = file_for(key).signed_url expires: expires_in, query: {
87
- "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
88
- "response-content-type" => content_type
89
- }
90
-
91
- payload[:url] = generated_url
92
-
93
- generated_url
94
- end
95
- end
96
-
97
85
  def url_for_direct_upload(key, expires_in:, checksum:, **)
98
86
  instrument :url, key: key do |payload|
99
87
  generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
@@ -104,11 +92,25 @@ module ActiveStorage
104
92
  end
105
93
  end
106
94
 
107
- def headers_for_direct_upload(key, checksum:, **)
108
- { "Content-MD5" => checksum }
95
+ def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, **)
96
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
97
+
98
+ { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
109
99
  end
110
100
 
111
101
  private
102
+ 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
106
+ }
107
+ end
108
+
109
+ def public_url(key, **)
110
+ file_for(key).public_url
111
+ end
112
+
113
+
112
114
  attr_reader :config
113
115
 
114
116
  def file_for(key, skip_lookup: true)
@@ -4,18 +4,26 @@ require "active_support/core_ext/module/delegation"
4
4
 
5
5
  module ActiveStorage
6
6
  # Wraps a set of mirror services and provides a single ActiveStorage::Service object that will all
7
- # have the files uploaded to them. A +primary+ service is designated to answer calls to +download+, +exists?+,
8
- # and +url+.
7
+ # have the files uploaded to them. A +primary+ service is designated to answer calls to:
8
+ # * +download+
9
+ # * +exists?+
10
+ # * +url+
11
+ # * +url_for_direct_upload+
12
+ # * +headers_for_direct_upload+
9
13
  class Service::MirrorService < Service
10
14
  attr_reader :primary, :mirrors
11
15
 
12
- delegate :download, :download_chunk, :exist?, :url, :path_for, to: :primary
16
+ delegate :download, :download_chunk, :exist?, :url,
17
+ :url_for_direct_upload, :headers_for_direct_upload, :path_for, to: :primary
13
18
 
14
19
  # Stitch together from named services.
15
- def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
16
- new \
20
+ def self.build(primary:, mirrors:, name:, configurator:, **options) #:nodoc:
21
+ new(
17
22
  primary: configurator.build(primary),
18
- mirrors: mirrors.collect { |name| configurator.build name }
23
+ mirrors: mirrors.collect { |mirror_name| configurator.build mirror_name }
24
+ ).tap do |service_instance|
25
+ service_instance.name = name
26
+ end
19
27
  end
20
28
 
21
29
  def initialize(primary:, mirrors:)
@@ -26,7 +34,8 @@ module ActiveStorage
26
34
  # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
27
35
  def upload(key, io, checksum: nil, **options)
28
36
  each_service.collect do |service|
29
- service.upload key, io.tap(&:rewind), checksum: checksum, **options
37
+ io.rewind
38
+ service.upload key, io, checksum: checksum, **options
30
39
  end
31
40
  end
32
41
 
@@ -40,6 +49,21 @@ module ActiveStorage
40
49
  perform_across_services :delete_prefixed, prefix
41
50
  end
42
51
 
52
+
53
+ # Copy the file at the +key+ from the primary service to each of the mirrors where it doesn't already exist.
54
+ def mirror(key, checksum:)
55
+ instrument :mirror, key: key, checksum: checksum do
56
+ if (mirrors_in_need_of_mirroring = mirrors.select { |service| !service.exist?(key) }).any?
57
+ primary.open(key, checksum: checksum) do |io|
58
+ mirrors_in_need_of_mirroring.each do |service|
59
+ io.rewind
60
+ service.upload key, io, checksum: checksum
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
43
67
  private
44
68
  def each_service(&block)
45
69
  [ primary, *mirrors ].each(&block)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Service::Registry #:nodoc:
5
+ def initialize(configurations)
6
+ @configurations = configurations.deep_symbolize_keys
7
+ @services = {}
8
+ end
9
+
10
+ def fetch(name)
11
+ services.fetch(name.to_sym) do |key|
12
+ if configurations.include?(key)
13
+ services[key] = configurator.build(key)
14
+ else
15
+ if block_given?
16
+ yield key
17
+ else
18
+ raise KeyError, "Missing configuration for the #{key} Active Storage service. " \
19
+ "Configurations available for the #{configurations.keys.to_sentence} services."
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+ attr_reader :configurations, :services
27
+
28
+ def configurator
29
+ @configurator ||= ActiveStorage::Service::Configurator.new(configurations)
30
+ end
31
+ end
32
+ end