activestorage 6.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +198 -0
- data/MIT-LICENSE +20 -0
- data/README.md +162 -0
- data/app/assets/javascripts/activestorage.js +942 -0
- data/app/controllers/active_storage/base_controller.rb +8 -0
- data/app/controllers/active_storage/blobs_controller.rb +14 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
- data/app/controllers/active_storage/disk_controller.rb +66 -0
- data/app/controllers/active_storage/representations_controller.rb +14 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +73 -0
- data/app/javascript/activestorage/blob_upload.js +35 -0
- data/app/javascript/activestorage/direct_upload.js +48 -0
- data/app/javascript/activestorage/direct_upload_controller.js +67 -0
- data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/activestorage/helpers.js +51 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +86 -0
- data/app/jobs/active_storage/analyze_job.rb +12 -0
- data/app/jobs/active_storage/base_job.rb +4 -0
- data/app/jobs/active_storage/purge_job.rb +13 -0
- data/app/models/active_storage/attachment.rb +50 -0
- data/app/models/active_storage/blob.rb +278 -0
- data/app/models/active_storage/blob/analyzable.rb +57 -0
- data/app/models/active_storage/blob/identifiable.rb +31 -0
- data/app/models/active_storage/blob/representable.rb +93 -0
- data/app/models/active_storage/current.rb +5 -0
- data/app/models/active_storage/filename.rb +77 -0
- data/app/models/active_storage/preview.rb +89 -0
- data/app/models/active_storage/variant.rb +131 -0
- data/app/models/active_storage/variation.rb +80 -0
- data/config/routes.rb +32 -0
- data/db/migrate/20170806125915_create_active_storage_tables.rb +26 -0
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
- data/lib/active_storage.rb +73 -0
- data/lib/active_storage/analyzer.rb +38 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +52 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +118 -0
- data/lib/active_storage/attached.rb +25 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +69 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +27 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/many.rb +65 -0
- data/lib/active_storage/attached/model.rb +147 -0
- data/lib/active_storage/attached/one.rb +79 -0
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/downloading.rb +47 -0
- data/lib/active_storage/engine.rb +149 -0
- data/lib/active_storage/errors.rb +26 -0
- data/lib/active_storage/gem_version.rb +17 -0
- data/lib/active_storage/log_subscriber.rb +58 -0
- data/lib/active_storage/previewer.rb +84 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +36 -0
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +35 -0
- data/lib/active_storage/previewer/video_previewer.rb +26 -0
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +141 -0
- data/lib/active_storage/service/azure_storage_service.rb +165 -0
- data/lib/active_storage/service/configurator.rb +34 -0
- data/lib/active_storage/service/disk_service.rb +166 -0
- data/lib/active_storage/service/gcs_service.rb +141 -0
- data/lib/active_storage/service/mirror_service.rb +55 -0
- data/lib/active_storage/service/s3_service.rb +116 -0
- data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
- data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
- data/lib/active_storage/transformers/transformer.rb +42 -0
- data/lib/active_storage/version.rb +10 -0
- data/lib/tasks/activestorage.rake +22 -0
- metadata +174 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
class Previewer::PopplerPDFPreviewer < Previewer
|
5
|
+
class << self
|
6
|
+
def accept?(blob)
|
7
|
+
blob.content_type == "application/pdf" && pdftoppm_exists?
|
8
|
+
end
|
9
|
+
|
10
|
+
def pdftoppm_path
|
11
|
+
ActiveStorage.paths[:pdftoppm] || "pdftoppm"
|
12
|
+
end
|
13
|
+
|
14
|
+
def pdftoppm_exists?
|
15
|
+
return @pdftoppm_exists if defined?(@pdftoppm_exists)
|
16
|
+
|
17
|
+
@pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def preview
|
22
|
+
download_blob_to_tempfile do |input|
|
23
|
+
draw_first_page_from input do |output|
|
24
|
+
yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def draw_first_page_from(file, &block)
|
31
|
+
# use 72 dpi to match thumbnail dimensions of the PDF
|
32
|
+
draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
class Previewer::VideoPreviewer < Previewer
|
5
|
+
def self.accept?(blob)
|
6
|
+
blob.video?
|
7
|
+
end
|
8
|
+
|
9
|
+
def preview
|
10
|
+
download_blob_to_tempfile do |input|
|
11
|
+
draw_relevant_frame_from input do |output|
|
12
|
+
yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def draw_relevant_frame_from(file, &block)
|
19
|
+
draw ffmpeg_path, "-i", file.path, "-y", "-vframes", "1", "-f", "image2", "-", &block
|
20
|
+
end
|
21
|
+
|
22
|
+
def ffmpeg_path
|
23
|
+
ActiveStorage.paths[:ffmpeg] || "ffmpeg"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
module Reflection
|
5
|
+
# Holds all the metadata about a has_one_attached attachment as it was
|
6
|
+
# specified in the Active Record class.
|
7
|
+
class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
|
8
|
+
def macro
|
9
|
+
:has_one_attached
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Holds all the metadata about a has_many_attached attachment as it was
|
14
|
+
# specified in the Active Record class.
|
15
|
+
class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
|
16
|
+
def macro
|
17
|
+
:has_many_attached
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ReflectionExtension # :nodoc:
|
22
|
+
def add_attachment_reflection(model, name, reflection)
|
23
|
+
model.attachment_reflections = model.attachment_reflections.merge(name.to_s => reflection)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def reflection_class_for(macro)
|
28
|
+
case macro
|
29
|
+
when :has_one_attached
|
30
|
+
HasOneAttachedReflection
|
31
|
+
when :has_many_attached
|
32
|
+
HasManyAttachedReflection
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module ActiveRecordExtensions
|
40
|
+
extend ActiveSupport::Concern
|
41
|
+
|
42
|
+
included do
|
43
|
+
class_attribute :attachment_reflections, instance_writer: false, default: {}
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
# Returns an array of reflection objects for all the attachments in the
|
48
|
+
# class.
|
49
|
+
def reflect_on_all_attachments
|
50
|
+
attachment_reflections.values
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the reflection object for the named +attachment+.
|
54
|
+
#
|
55
|
+
# User.reflect_on_attachment(:avatar)
|
56
|
+
# # => the avatar reflection
|
57
|
+
#
|
58
|
+
def reflect_on_attachment(attachment)
|
59
|
+
attachment_reflections[attachment.to_s]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_storage/log_subscriber"
|
4
|
+
require "action_dispatch"
|
5
|
+
require "action_dispatch/http/content_disposition"
|
6
|
+
|
7
|
+
module ActiveStorage
|
8
|
+
# Abstract class serving as an interface for concrete services.
|
9
|
+
#
|
10
|
+
# The available services are:
|
11
|
+
#
|
12
|
+
# * +Disk+, to manage attachments saved directly on the hard drive.
|
13
|
+
# * +GCS+, to manage attachments through Google Cloud Storage.
|
14
|
+
# * +S3+, to manage attachments through Amazon S3.
|
15
|
+
# * +AzureStorage+, to manage attachments through Microsoft Azure Storage.
|
16
|
+
# * +Mirror+, to be able to use several services to manage attachments.
|
17
|
+
#
|
18
|
+
# Inside a Rails application, you can set-up your services through the
|
19
|
+
# generated <tt>config/storage.yml</tt> file and reference one
|
20
|
+
# of the aforementioned constant under the +service+ key. For example:
|
21
|
+
#
|
22
|
+
# local:
|
23
|
+
# service: Disk
|
24
|
+
# root: <%= Rails.root.join("storage") %>
|
25
|
+
#
|
26
|
+
# You can checkout the service's constructor to know which keys are required.
|
27
|
+
#
|
28
|
+
# Then, in your application's configuration, you can specify the service to
|
29
|
+
# use like this:
|
30
|
+
#
|
31
|
+
# config.active_storage.service = :local
|
32
|
+
#
|
33
|
+
# If you are using Active Storage outside of a Ruby on Rails application, you
|
34
|
+
# can configure the service to use like this:
|
35
|
+
#
|
36
|
+
# ActiveStorage::Blob.service = ActiveStorage::Service.configure(
|
37
|
+
# :Disk,
|
38
|
+
# root: Pathname("/foo/bar/storage")
|
39
|
+
# )
|
40
|
+
class Service
|
41
|
+
extend ActiveSupport::Autoload
|
42
|
+
autoload :Configurator
|
43
|
+
|
44
|
+
class << self
|
45
|
+
# Configure an Active Storage service by name from a set of configurations,
|
46
|
+
# typically loaded from a YAML file. The Active Storage engine uses this
|
47
|
+
# to set the global Active Storage service when the app boots.
|
48
|
+
def configure(service_name, configurations)
|
49
|
+
Configurator.build(service_name, configurations)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Override in subclasses that stitch together multiple services and hence
|
53
|
+
# need to build additional services using the configurator.
|
54
|
+
#
|
55
|
+
# Passes the configurator and all of the service's config as keyword args.
|
56
|
+
#
|
57
|
+
# See MirrorService for an example.
|
58
|
+
def build(configurator:, service: nil, **service_config) #:nodoc:
|
59
|
+
new(**service_config)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will
|
64
|
+
# ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
|
65
|
+
def upload(key, io, checksum: nil, **options)
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
|
69
|
+
# Update metadata for the file identified by +key+ in the service.
|
70
|
+
# Override in subclasses only if the service needs to store specific
|
71
|
+
# metadata that has to be updated upon identification.
|
72
|
+
def update_metadata(key, **metadata)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return the content of the file at the +key+.
|
76
|
+
def download(key)
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return the partial content in the byte +range+ of the file at the +key+.
|
81
|
+
def download_chunk(key, range)
|
82
|
+
raise NotImplementedError
|
83
|
+
end
|
84
|
+
|
85
|
+
def open(*args, &block)
|
86
|
+
ActiveStorage::Downloader.new(self).open(*args, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Delete the file at the +key+.
|
90
|
+
def delete(key)
|
91
|
+
raise NotImplementedError
|
92
|
+
end
|
93
|
+
|
94
|
+
# Delete files at keys starting with the +prefix+.
|
95
|
+
def delete_prefixed(prefix)
|
96
|
+
raise NotImplementedError
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return +true+ if a file exists at the +key+.
|
100
|
+
def exist?(key)
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
|
105
|
+
# of seconds specified in +expires_in+. You must also provide the +disposition+ (+:inline+ or +:attachment+),
|
106
|
+
# +filename+, and +content_type+ that you wish the file to be served with on request.
|
107
|
+
def url(key, expires_in:, disposition:, filename:, content_type:)
|
108
|
+
raise NotImplementedError
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
|
112
|
+
# The URL will be valid for the amount of seconds specified in +expires_in+.
|
113
|
+
# You must also provide the +content_type+, +content_length+, and +checksum+ of the file
|
114
|
+
# that will be uploaded. All these attributes will be validated by the service upon upload.
|
115
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
116
|
+
raise NotImplementedError
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a Hash of headers for +url_for_direct_upload+ requests.
|
120
|
+
def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
|
121
|
+
{}
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
def instrument(operation, payload = {}, &block)
|
126
|
+
ActiveSupport::Notifications.instrument(
|
127
|
+
"service_#{operation}.active_storage",
|
128
|
+
payload.merge(service: service_name), &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
def service_name
|
132
|
+
# ActiveStorage::Service::DiskService => Disk
|
133
|
+
self.class.name.split("::").third.remove("Service")
|
134
|
+
end
|
135
|
+
|
136
|
+
def content_disposition_with(type: "inline", filename:)
|
137
|
+
disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline")
|
138
|
+
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/numeric/bytes"
|
4
|
+
require "azure/storage"
|
5
|
+
require "azure/storage/core/auth/shared_access_signature"
|
6
|
+
|
7
|
+
module ActiveStorage
|
8
|
+
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
|
9
|
+
# See ActiveStorage::Service for the generic API documentation that applies to all services.
|
10
|
+
class Service::AzureStorageService < Service
|
11
|
+
attr_reader :client, :blobs, :container, :signer
|
12
|
+
|
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
|
17
|
+
@container = container
|
18
|
+
end
|
19
|
+
|
20
|
+
def upload(key, io, checksum: nil, **)
|
21
|
+
instrument :upload, key: key, checksum: checksum do
|
22
|
+
handle_errors do
|
23
|
+
blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def download(key, &block)
|
29
|
+
if block_given?
|
30
|
+
instrument :streaming_download, key: key do
|
31
|
+
stream(key, &block)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
instrument :download, key: key do
|
35
|
+
handle_errors do
|
36
|
+
_, io = blobs.get_blob(container, key)
|
37
|
+
io.force_encoding(Encoding::BINARY)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def download_chunk(key, range)
|
44
|
+
instrument :download_chunk, key: key, range: range do
|
45
|
+
handle_errors do
|
46
|
+
_, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
|
47
|
+
io.force_encoding(Encoding::BINARY)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(key)
|
53
|
+
instrument :delete, key: key do
|
54
|
+
blobs.delete_blob(container, key)
|
55
|
+
rescue Azure::Core::Http::HTTPError => e
|
56
|
+
raise unless e.type == "BlobNotFound"
|
57
|
+
# Ignore files already deleted
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete_prefixed(prefix)
|
62
|
+
instrument :delete_prefixed, prefix: prefix do
|
63
|
+
marker = nil
|
64
|
+
|
65
|
+
loop do
|
66
|
+
results = blobs.list_blobs(container, prefix: prefix, marker: marker)
|
67
|
+
|
68
|
+
results.each do |blob|
|
69
|
+
blobs.delete_blob(container, blob.name)
|
70
|
+
end
|
71
|
+
|
72
|
+
break unless marker = results.continuation_token.presence
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def exist?(key)
|
78
|
+
instrument :exist, key: key do |payload|
|
79
|
+
answer = blob_for(key).present?
|
80
|
+
payload[:exist] = answer
|
81
|
+
answer
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def url(key, expires_in:, filename:, disposition:, content_type:)
|
86
|
+
instrument :url, key: key do |payload|
|
87
|
+
generated_url = signer.signed_uri(
|
88
|
+
uri_for(key), false,
|
89
|
+
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
|
+
).to_s
|
95
|
+
|
96
|
+
payload[:url] = generated_url
|
97
|
+
|
98
|
+
generated_url
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
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(
|
105
|
+
uri_for(key), false,
|
106
|
+
service: "b",
|
107
|
+
permissions: "rw",
|
108
|
+
expiry: format_expiry(expires_in)
|
109
|
+
).to_s
|
110
|
+
|
111
|
+
payload[:url] = generated_url
|
112
|
+
|
113
|
+
generated_url
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
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
|
+
|
121
|
+
private
|
122
|
+
def uri_for(key)
|
123
|
+
blobs.generate_uri("#{container}/#{key}")
|
124
|
+
end
|
125
|
+
|
126
|
+
def blob_for(key)
|
127
|
+
blobs.get_blob_properties(container, key)
|
128
|
+
rescue Azure::Core::Http::HTTPError
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def format_expiry(expires_in)
|
133
|
+
expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Reads the object for the given key in chunks, yielding each to the block.
|
137
|
+
def stream(key)
|
138
|
+
blob = blob_for(key)
|
139
|
+
|
140
|
+
chunk_size = 5.megabytes
|
141
|
+
offset = 0
|
142
|
+
|
143
|
+
raise ActiveStorage::FileNotFoundError unless blob.present?
|
144
|
+
|
145
|
+
while offset < blob.properties[:content_length]
|
146
|
+
_, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
|
147
|
+
yield chunk.force_encoding(Encoding::BINARY)
|
148
|
+
offset += chunk_size
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def handle_errors
|
153
|
+
yield
|
154
|
+
rescue Azure::Core::Http::HTTPError => e
|
155
|
+
case e.type
|
156
|
+
when "BlobNotFound"
|
157
|
+
raise ActiveStorage::FileNotFoundError
|
158
|
+
when "Md5Mismatch"
|
159
|
+
raise ActiveStorage::IntegrityError
|
160
|
+
else
|
161
|
+
raise
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
class Service::Configurator #:nodoc:
|
5
|
+
attr_reader :configurations
|
6
|
+
|
7
|
+
def self.build(service_name, configurations)
|
8
|
+
new(configurations).build(service_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(configurations)
|
12
|
+
@configurations = configurations.deep_symbolize_keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def build(service_name)
|
16
|
+
config = config_for(service_name.to_sym)
|
17
|
+
resolve(config.fetch(:service)).build(**config, configurator: self)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def config_for(name)
|
22
|
+
configurations.fetch name do
|
23
|
+
raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def resolve(class_name)
|
28
|
+
require "active_storage/service/#{class_name.to_s.underscore}_service"
|
29
|
+
ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
|
30
|
+
rescue LoadError
|
31
|
+
raise "Missing service adapter for #{class_name.inspect}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|