activestorage 6.0.6.1 → 6.1.7.3
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 +4 -4
- data/CHANGELOG.md +215 -167
- data/MIT-LICENSE +1 -1
- data/README.md +35 -3
- data/app/controllers/active_storage/base_controller.rb +11 -0
- data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
- data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
- data/app/controllers/active_storage/direct_uploads_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +8 -20
- data/app/controllers/active_storage/representations/base_controller.rb +14 -0
- data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
- data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -4
- data/app/controllers/concerns/active_storage/file_server.rb +18 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +2 -2
- data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
- data/app/jobs/active_storage/mirror_job.rb +15 -0
- data/app/models/active_storage/attachment.rb +19 -11
- data/app/models/active_storage/blob/analyzable.rb +6 -2
- data/app/models/active_storage/blob/identifiable.rb +7 -6
- data/app/models/active_storage/blob/representable.rb +34 -4
- data/app/models/active_storage/blob.rb +122 -57
- data/app/models/active_storage/preview.rb +31 -10
- data/app/models/active_storage/record.rb +7 -0
- data/app/models/active_storage/variant.rb +31 -44
- data/app/models/active_storage/variant_record.rb +8 -0
- data/app/models/active_storage/variant_with_record.rb +54 -0
- data/app/models/active_storage/variation.rb +26 -21
- data/config/routes.rb +58 -8
- data/db/migrate/20170806125915_create_active_storage_tables.rb +30 -9
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +21 -0
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +26 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
- data/lib/active_storage/analyzer.rb +6 -0
- data/lib/active_storage/attached/changes/create_many.rb +1 -0
- data/lib/active_storage/attached/changes/create_one.rb +17 -4
- data/lib/active_storage/attached/many.rb +4 -3
- data/lib/active_storage/attached/model.rb +67 -14
- data/lib/active_storage/attached/one.rb +4 -3
- data/lib/active_storage/engine.rb +41 -43
- data/lib/active_storage/errors.rb +3 -0
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/log_subscriber.rb +6 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
- data/lib/active_storage/previewer/video_previewer.rb +5 -3
- data/lib/active_storage/previewer.rb +13 -3
- data/lib/active_storage/service/azure_storage_service.rb +40 -35
- data/lib/active_storage/service/configurator.rb +3 -1
- data/lib/active_storage/service/disk_service.rb +36 -31
- data/lib/active_storage/service/gcs_service.rb +18 -16
- data/lib/active_storage/service/mirror_service.rb +31 -7
- data/lib/active_storage/service/registry.rb +32 -0
- data/lib/active_storage/service/s3_service.rb +51 -23
- data/lib/active_storage/service.rb +35 -7
- data/lib/active_storage/transformers/image_processing_transformer.rb +21 -308
- data/lib/active_storage/transformers/transformer.rb +0 -3
- data/lib/active_storage.rb +301 -7
- data/lib/tasks/activestorage.rake +5 -1
- metadata +53 -16
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
- data/lib/active_storage/downloading.rb +0 -47
- data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
@@ -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
|
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
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "shellwords"
|
4
|
+
|
3
5
|
module ActiveStorage
|
4
6
|
class Previewer::VideoPreviewer < Previewer
|
5
7
|
class << self
|
@@ -18,17 +20,17 @@ module ActiveStorage
|
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
def preview
|
23
|
+
def preview(**options)
|
22
24
|
download_blob_to_tempfile do |input|
|
23
25
|
draw_relevant_frame_from input do |output|
|
24
|
-
yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg"
|
26
|
+
yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg", **options
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
31
|
private
|
30
32
|
def draw_relevant_frame_from(file, &block)
|
31
|
-
draw self.class.ffmpeg_path, "-i", file.path,
|
33
|
+
draw self.class.ffmpeg_path, "-i", file.path, *Shellwords.split(ActiveStorage.video_preview_arguments), "-", &block
|
32
34
|
end
|
33
35
|
end
|
34
36
|
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
|
-
|
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
|
|
@@ -69,7 +70,16 @@ module ActiveStorage
|
|
69
70
|
|
70
71
|
def capture(*argv, to:)
|
71
72
|
to.binmode
|
72
|
-
|
73
|
+
|
74
|
+
open_tempfile do |err|
|
75
|
+
IO.popen(argv, err: err) { |out| IO.copy_stream(out, to) }
|
76
|
+
err.rewind
|
77
|
+
|
78
|
+
unless $?.success?
|
79
|
+
raise PreviewError, "#{argv.first} failed (status #{$?.exitstatus}): #{err.read.to_s.chomp}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
73
83
|
to.rewind
|
74
84
|
end
|
75
85
|
|
@@ -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, :
|
13
|
+
attr_reader :client, :container, :signer
|
12
14
|
|
13
|
-
def initialize(storage_account_name:, storage_access_key:, container:, **options)
|
14
|
-
@client = Azure::Storage::
|
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
|
-
|
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 =
|
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 =
|
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
|
-
|
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 =
|
70
|
+
results = client.list_blobs(container, prefix: prefix, marker: marker)
|
67
71
|
|
68
72
|
results.each do |blob|
|
69
|
-
|
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
|
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: "
|
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
|
103
|
-
|
104
|
-
|
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: "
|
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
|
-
|
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
|
-
|
128
|
+
client.generate_uri("#{container}/#{key}")
|
124
129
|
end
|
125
130
|
|
126
131
|
def blob_for(key)
|
127
|
-
|
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 =
|
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(
|
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
|
-
|
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
|
8
|
-
#
|
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,
|
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 { |
|
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
|
-
|
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
|
@@ -9,20 +9,29 @@ module ActiveStorage
|
|
9
9
|
# Wraps the Amazon Simple Storage Service (S3) as an Active Storage service.
|
10
10
|
# See ActiveStorage::Service for the generic API documentation that applies to all services.
|
11
11
|
class Service::S3Service < Service
|
12
|
-
attr_reader :client, :bucket
|
12
|
+
attr_reader :client, :bucket
|
13
|
+
attr_reader :multipart_upload_threshold, :upload_options
|
13
14
|
|
14
|
-
def initialize(bucket:, upload: {}, **options)
|
15
|
+
def initialize(bucket:, upload: {}, public: false, **options)
|
15
16
|
@client = Aws::S3::Resource.new(**options)
|
16
17
|
@bucket = @client.bucket(bucket)
|
17
18
|
|
19
|
+
@multipart_upload_threshold = upload.delete(:multipart_threshold) || 100.megabytes
|
20
|
+
@public = public
|
21
|
+
|
18
22
|
@upload_options = upload
|
23
|
+
@upload_options[:acl] = "public-read" if public?
|
19
24
|
end
|
20
25
|
|
21
|
-
def upload(key, io, checksum: nil, content_type: nil, **)
|
26
|
+
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
|
22
27
|
instrument :upload, key: key, checksum: checksum do
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
|
29
|
+
|
30
|
+
if io.size < multipart_upload_threshold
|
31
|
+
upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition
|
32
|
+
else
|
33
|
+
upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition
|
34
|
+
end
|
26
35
|
end
|
27
36
|
end
|
28
37
|
|
@@ -42,7 +51,7 @@ module ActiveStorage
|
|
42
51
|
|
43
52
|
def download_chunk(key, range)
|
44
53
|
instrument :download_chunk, key: key, range: range do
|
45
|
-
object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.
|
54
|
+
object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.string.force_encoding(Encoding::BINARY)
|
46
55
|
rescue Aws::S3::Errors::NoSuchKey
|
47
56
|
raise ActiveStorage::FileNotFoundError
|
48
57
|
end
|
@@ -68,23 +77,11 @@ module ActiveStorage
|
|
68
77
|
end
|
69
78
|
end
|
70
79
|
|
71
|
-
def url(key, expires_in:, filename:, disposition:, content_type:)
|
72
|
-
instrument :url, key: key do |payload|
|
73
|
-
generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
|
74
|
-
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
|
75
|
-
response_content_type: content_type
|
76
|
-
|
77
|
-
payload[:url] = generated_url
|
78
|
-
|
79
|
-
generated_url
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
80
|
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
84
81
|
instrument :url, key: key do |payload|
|
85
82
|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
|
86
83
|
content_type: content_type, content_length: content_length, content_md5: checksum,
|
87
|
-
whitelist_headers: [
|
84
|
+
whitelist_headers: ["content-length"], **upload_options
|
88
85
|
|
89
86
|
payload[:url] = generated_url
|
90
87
|
|
@@ -92,11 +89,42 @@ module ActiveStorage
|
|
92
89
|
end
|
93
90
|
end
|
94
91
|
|
95
|
-
def headers_for_direct_upload(key, content_type:, checksum:, **)
|
96
|
-
|
92
|
+
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
|
93
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
|
94
|
+
|
95
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
|
97
96
|
end
|
98
97
|
|
99
98
|
private
|
99
|
+
def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
|
100
|
+
object_for(key).presigned_url :get, expires_in: expires_in.to_i,
|
101
|
+
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
|
102
|
+
response_content_type: content_type
|
103
|
+
end
|
104
|
+
|
105
|
+
def public_url(key, **)
|
106
|
+
object_for(key).public_url
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
MAXIMUM_UPLOAD_PARTS_COUNT = 10000
|
111
|
+
MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
|
112
|
+
|
113
|
+
def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil)
|
114
|
+
object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, **upload_options)
|
115
|
+
rescue Aws::S3::Errors::BadDigest
|
116
|
+
raise ActiveStorage::IntegrityError
|
117
|
+
end
|
118
|
+
|
119
|
+
def upload_with_multipart(key, io, content_type: nil, content_disposition: nil)
|
120
|
+
part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
|
121
|
+
|
122
|
+
object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, **upload_options) do |out|
|
123
|
+
IO.copy_stream(io, out)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
100
128
|
def object_for(key)
|
101
129
|
bucket.object(key)
|
102
130
|
end
|
@@ -111,7 +139,7 @@ module ActiveStorage
|
|
111
139
|
raise ActiveStorage::FileNotFoundError unless object.exists?
|
112
140
|
|
113
141
|
while offset < object.content_length
|
114
|
-
yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.
|
142
|
+
yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.string.force_encoding(Encoding::BINARY)
|
115
143
|
offset += chunk_size
|
116
144
|
end
|
117
145
|
end
|