activestorage 0.1 → 5.2.0.beta1
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 +5 -0
- data/README.md +94 -25
- data/app/assets/javascripts/activestorage.js +1 -0
- data/app/controllers/active_storage/blobs_controller.rb +16 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
- data/app/controllers/active_storage/disk_controller.rb +51 -0
- data/app/controllers/active_storage/previews_controller.rb +12 -0
- data/app/controllers/active_storage/variants_controller.rb +16 -0
- data/app/javascript/activestorage/blob_record.js +54 -0
- data/app/javascript/activestorage/blob_upload.js +35 -0
- data/app/javascript/activestorage/direct_upload.js +42 -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 +42 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +75 -0
- data/app/jobs/active_storage/analyze_job.rb +8 -0
- data/app/jobs/active_storage/base_job.rb +5 -0
- data/app/jobs/active_storage/purge_job.rb +11 -0
- data/app/models/active_storage/attachment.rb +35 -0
- data/app/models/active_storage/blob.rb +313 -0
- data/app/models/active_storage/filename.rb +73 -0
- data/app/models/active_storage/filename/parameters.rb +36 -0
- data/app/models/active_storage/preview.rb +90 -0
- data/app/models/active_storage/variant.rb +86 -0
- data/app/models/active_storage/variation.rb +67 -0
- data/config/routes.rb +43 -0
- data/lib/active_storage.rb +37 -2
- data/lib/active_storage/analyzer.rb +33 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +36 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +79 -0
- data/lib/active_storage/attached.rb +28 -22
- data/lib/active_storage/attached/macros.rb +89 -16
- data/lib/active_storage/attached/many.rb +53 -21
- data/lib/active_storage/attached/one.rb +74 -20
- data/lib/active_storage/downloading.rb +26 -0
- data/lib/active_storage/engine.rb +72 -0
- data/lib/active_storage/gem_version.rb +17 -0
- data/lib/active_storage/log_subscriber.rb +52 -0
- data/lib/active_storage/previewer.rb +58 -0
- data/lib/active_storage/previewer/pdf_previewer.rb +17 -0
- data/lib/active_storage/previewer/video_previewer.rb +23 -0
- data/lib/active_storage/service.rb +112 -24
- data/lib/active_storage/service/azure_storage_service.rb +124 -0
- data/lib/active_storage/service/configurator.rb +32 -0
- data/lib/active_storage/service/disk_service.rb +103 -44
- data/lib/active_storage/service/gcs_service.rb +87 -29
- data/lib/active_storage/service/mirror_service.rb +38 -22
- data/lib/active_storage/service/s3_service.rb +83 -38
- data/lib/active_storage/version.rb +10 -0
- data/lib/tasks/activestorage.rake +4 -15
- metadata +64 -108
- data/.gitignore +0 -1
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -235
- data/Rakefile +0 -11
- data/activestorage.gemspec +0 -21
- data/lib/active_storage/attachment.rb +0 -30
- data/lib/active_storage/blob.rb +0 -80
- data/lib/active_storage/disk_controller.rb +0 -28
- data/lib/active_storage/download.rb +0 -90
- data/lib/active_storage/filename.rb +0 -31
- data/lib/active_storage/migration.rb +0 -28
- data/lib/active_storage/purge_job.rb +0 -10
- data/lib/active_storage/railtie.rb +0 -56
- data/lib/active_storage/storage_services.yml +0 -27
- data/lib/active_storage/verified_key_with_expiration.rb +0 -24
- data/test/attachments_test.rb +0 -95
- data/test/blob_test.rb +0 -28
- data/test/database/create_users_migration.rb +0 -7
- data/test/database/setup.rb +0 -6
- data/test/disk_controller_test.rb +0 -34
- data/test/filename_test.rb +0 -36
- data/test/service/.gitignore +0 -1
- data/test/service/configurations-example.yml +0 -11
- data/test/service/disk_service_test.rb +0 -8
- data/test/service/gcs_service_test.rb +0 -20
- data/test/service/mirror_service_test.rb +0 -50
- data/test/service/s3_service_test.rb +0 -11
- data/test/service/shared_service_tests.rb +0 -68
- data/test/test_helper.rb +0 -28
- data/test/verified_key_with_expiration_test.rb +0 -19
@@ -0,0 +1,32 @@
|
|
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}Service")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,70 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "fileutils"
|
2
4
|
require "pathname"
|
5
|
+
require "digest/md5"
|
3
6
|
require "active_support/core_ext/numeric/bytes"
|
4
7
|
|
5
|
-
|
6
|
-
|
8
|
+
module ActiveStorage
|
9
|
+
# Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
|
10
|
+
# documentation that applies to all services.
|
11
|
+
class Service::DiskService < Service
|
12
|
+
attr_reader :root
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
14
|
+
def initialize(root:)
|
15
|
+
@root = root
|
16
|
+
end
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
18
|
+
def upload(key, io, checksum: nil)
|
19
|
+
instrument :upload, key, checksum: checksum do
|
20
|
+
IO.copy_stream(io, make_path_for(key))
|
21
|
+
ensure_integrity_of(key, checksum) if checksum
|
16
22
|
end
|
17
23
|
end
|
18
24
|
|
19
|
-
|
20
|
-
|
25
|
+
def download(key)
|
26
|
+
if block_given?
|
27
|
+
instrument :streaming_download, key do
|
28
|
+
File.open(path_for(key), "rb") do |file|
|
29
|
+
while data = file.read(64.kilobytes)
|
30
|
+
yield data
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else
|
35
|
+
instrument :download, key do
|
36
|
+
File.binread path_for(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
41
|
+
def delete(key)
|
42
|
+
instrument :delete, key do
|
43
|
+
begin
|
44
|
+
File.delete path_for(key)
|
45
|
+
rescue Errno::ENOENT
|
46
|
+
# Ignore files already deleted
|
27
47
|
end
|
28
48
|
end
|
29
|
-
else
|
30
|
-
File.open path_for(key), &:read
|
31
49
|
end
|
32
|
-
end
|
33
50
|
|
34
|
-
|
35
|
-
|
36
|
-
|
51
|
+
def exist?(key)
|
52
|
+
instrument :exist, key do |payload|
|
53
|
+
answer = File.exist? path_for(key)
|
54
|
+
payload[:exist] = answer
|
55
|
+
answer
|
56
|
+
end
|
57
|
+
end
|
37
58
|
|
38
|
-
|
39
|
-
|
40
|
-
|
59
|
+
def url(key, expires_in:, filename:, disposition:, content_type:)
|
60
|
+
instrument :url, key do |payload|
|
61
|
+
verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
|
41
62
|
|
42
|
-
|
43
|
-
|
63
|
+
generated_url =
|
64
|
+
if defined?(Rails.application)
|
65
|
+
Rails.application.routes.url_helpers.rails_disk_service_path \
|
66
|
+
verified_key_with_expiration,
|
67
|
+
filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type
|
68
|
+
else
|
69
|
+
"/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}" \
|
70
|
+
"&disposition=#{content_disposition_with(type: disposition, filename: filename)}"
|
71
|
+
end
|
44
72
|
|
45
|
-
|
46
|
-
Rails.application.routes.url_helpers.rails_disk_blob_path(verified_key_with_expiration, disposition: disposition)
|
47
|
-
else
|
48
|
-
"/rails/blobs/#{verified_key_with_expiration}?disposition=#{disposition}"
|
49
|
-
end
|
50
|
-
end
|
73
|
+
payload[:url] = generated_url
|
51
74
|
|
52
|
-
|
53
|
-
|
54
|
-
File.join root, folder_for(key), key
|
75
|
+
generated_url
|
76
|
+
end
|
55
77
|
end
|
56
78
|
|
57
|
-
def
|
58
|
-
|
79
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
80
|
+
instrument :url, key do |payload|
|
81
|
+
verified_token_with_expiration = ActiveStorage.verifier.generate(
|
82
|
+
{
|
83
|
+
key: key,
|
84
|
+
content_type: content_type,
|
85
|
+
content_length: content_length,
|
86
|
+
checksum: checksum
|
87
|
+
},
|
88
|
+
{ expires_in: expires_in,
|
89
|
+
purpose: :blob_token }
|
90
|
+
)
|
91
|
+
|
92
|
+
generated_url =
|
93
|
+
if defined?(Rails.application)
|
94
|
+
Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration
|
95
|
+
else
|
96
|
+
"/rails/active_storage/disk/#{verified_token_with_expiration}"
|
97
|
+
end
|
98
|
+
|
99
|
+
payload[:url] = generated_url
|
100
|
+
|
101
|
+
generated_url
|
102
|
+
end
|
59
103
|
end
|
60
104
|
|
61
|
-
def
|
62
|
-
|
105
|
+
def headers_for_direct_upload(key, content_type:, **)
|
106
|
+
{ "Content-Type" => content_type }
|
63
107
|
end
|
64
108
|
|
65
|
-
|
66
|
-
|
67
|
-
|
109
|
+
private
|
110
|
+
def path_for(key)
|
111
|
+
File.join root, folder_for(key), key
|
68
112
|
end
|
69
|
-
|
113
|
+
|
114
|
+
def folder_for(key)
|
115
|
+
[ key[0..1], key[2..3] ].join("/")
|
116
|
+
end
|
117
|
+
|
118
|
+
def make_path_for(key)
|
119
|
+
path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def ensure_integrity_of(key, checksum)
|
123
|
+
unless Digest::MD5.file(path_for(key)).base64digest == checksum
|
124
|
+
delete key
|
125
|
+
raise ActiveStorage::IntegrityError
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
70
129
|
end
|
@@ -1,41 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "google/cloud/storage"
|
2
4
|
require "active_support/core_ext/object/to_query"
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
+
module ActiveStorage
|
7
|
+
# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
|
8
|
+
# documentation that applies to all services.
|
9
|
+
class Service::GCSService < Service
|
10
|
+
def initialize(**config)
|
11
|
+
@config = config
|
12
|
+
end
|
6
13
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
14
|
+
def upload(key, io, checksum: nil)
|
15
|
+
instrument :upload, key, checksum: checksum do
|
16
|
+
begin
|
17
|
+
bucket.create_file(io, key, md5: checksum)
|
18
|
+
rescue Google::Cloud::InvalidArgumentError
|
19
|
+
raise ActiveStorage::IntegrityError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
11
23
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
24
|
+
# FIXME: Download in chunks when given a block.
|
25
|
+
def download(key)
|
26
|
+
instrument :download, key do
|
27
|
+
io = file_for(key).download
|
28
|
+
io.rewind
|
16
29
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
30
|
+
if block_given?
|
31
|
+
yield io.read
|
32
|
+
else
|
33
|
+
io.read
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
23
37
|
|
24
|
-
|
25
|
-
|
26
|
-
|
38
|
+
def delete(key)
|
39
|
+
instrument :delete, key do
|
40
|
+
begin
|
41
|
+
file_for(key).delete
|
42
|
+
rescue Google::Cloud::NotFoundError
|
43
|
+
# Ignore files already deleted
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
27
47
|
|
28
|
-
|
29
|
-
|
30
|
-
|
48
|
+
def exist?(key)
|
49
|
+
instrument :exist, key do |payload|
|
50
|
+
answer = file_for(key).exists?
|
51
|
+
payload[:exist] = answer
|
52
|
+
answer
|
53
|
+
end
|
54
|
+
end
|
31
55
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
56
|
+
def url(key, expires_in:, filename:, content_type:, disposition:)
|
57
|
+
instrument :url, key do |payload|
|
58
|
+
generated_url = file_for(key).signed_url expires: expires_in, query: {
|
59
|
+
"response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
|
60
|
+
"response-content-type" => content_type
|
61
|
+
}
|
36
62
|
|
37
|
-
|
38
|
-
|
39
|
-
|
63
|
+
payload[:url] = generated_url
|
64
|
+
|
65
|
+
generated_url
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
70
|
+
instrument :url, key do |payload|
|
71
|
+
generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
|
72
|
+
content_type: content_type, content_md5: checksum
|
73
|
+
|
74
|
+
payload[:url] = generated_url
|
75
|
+
|
76
|
+
generated_url
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def headers_for_direct_upload(key, content_type:, checksum:, **)
|
81
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum }
|
40
82
|
end
|
83
|
+
|
84
|
+
private
|
85
|
+
attr_reader :config
|
86
|
+
|
87
|
+
def file_for(key)
|
88
|
+
bucket.file(key, skip_lookup: true)
|
89
|
+
end
|
90
|
+
|
91
|
+
def bucket
|
92
|
+
@bucket ||= client.bucket(config.fetch(:bucket))
|
93
|
+
end
|
94
|
+
|
95
|
+
def client
|
96
|
+
@client ||= Google::Cloud::Storage.new(config.except(:bucket))
|
97
|
+
end
|
98
|
+
end
|
41
99
|
end
|
@@ -1,34 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support/core_ext/module/delegation"
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
+
module ActiveStorage
|
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+.
|
9
|
+
class Service::MirrorService < Service
|
10
|
+
attr_reader :primary, :mirrors
|
5
11
|
|
6
|
-
|
12
|
+
delegate :download, :exist?, :url, to: :primary
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
14
|
+
# Stitch together from named services.
|
15
|
+
def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
|
16
|
+
new \
|
17
|
+
primary: configurator.build(primary),
|
18
|
+
mirrors: mirrors.collect { |name| configurator.build name }
|
19
|
+
end
|
11
20
|
|
12
|
-
|
13
|
-
|
14
|
-
service.upload key, io, checksum: checksum
|
15
|
-
io.rewind
|
21
|
+
def initialize(primary:, mirrors:)
|
22
|
+
@primary, @mirrors = primary, mirrors
|
16
23
|
end
|
17
|
-
end
|
18
24
|
|
19
|
-
|
20
|
-
|
21
|
-
|
25
|
+
# Upload the +io+ to the +key+ specified to all services. If a +checksum+ is provided, all services will
|
26
|
+
# ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError.
|
27
|
+
def upload(key, io, checksum: nil)
|
28
|
+
each_service.collect do |service|
|
29
|
+
service.upload key, io.tap(&:rewind), checksum: checksum
|
30
|
+
end
|
31
|
+
end
|
22
32
|
|
23
|
-
|
24
|
-
def
|
25
|
-
|
33
|
+
# Delete the file at the +key+ on all services.
|
34
|
+
def delete(key)
|
35
|
+
perform_across_services :delete, key
|
26
36
|
end
|
27
37
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
service.public_send method, *args
|
38
|
+
private
|
39
|
+
def each_service(&block)
|
40
|
+
[ primary, *mirrors ].each(&block)
|
32
41
|
end
|
33
|
-
|
42
|
+
|
43
|
+
def perform_across_services(method, *args)
|
44
|
+
# FIXME: Convert to be threaded
|
45
|
+
each_service.collect do |service|
|
46
|
+
service.public_send method, *args
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
34
50
|
end
|
@@ -1,55 +1,100 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "aws-sdk-s3"
|
2
4
|
require "active_support/core_ext/numeric/bytes"
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
+
module ActiveStorage
|
7
|
+
# Wraps the Amazon Simple Storage Service (S3) as an Active Storage service.
|
8
|
+
# See ActiveStorage::Service for the generic API documentation that applies to all services.
|
9
|
+
class Service::S3Service < Service
|
10
|
+
attr_reader :client, :bucket, :upload_options
|
6
11
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
12
|
+
def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options)
|
13
|
+
@client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options)
|
14
|
+
@bucket = @client.bucket(bucket)
|
11
15
|
|
12
|
-
|
13
|
-
|
14
|
-
object_for(key).put(body: io)
|
15
|
-
end
|
16
|
+
@upload_options = upload
|
17
|
+
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def upload(key, io, checksum: nil)
|
20
|
+
instrument :upload, key, checksum: checksum do
|
21
|
+
begin
|
22
|
+
object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
|
23
|
+
rescue Aws::S3::Errors::BadDigest
|
24
|
+
raise ActiveStorage::IntegrityError
|
25
|
+
end
|
26
|
+
end
|
22
27
|
end
|
23
|
-
end
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
def download(key, &block)
|
30
|
+
if block_given?
|
31
|
+
instrument :streaming_download, key do
|
32
|
+
stream(key, &block)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
instrument :download, key do
|
36
|
+
object_for(key).get.body.read.force_encoding(Encoding::BINARY)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
28
40
|
|
29
|
-
|
30
|
-
|
31
|
-
|
41
|
+
def delete(key)
|
42
|
+
instrument :delete, key do
|
43
|
+
object_for(key).delete
|
44
|
+
end
|
45
|
+
end
|
32
46
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
47
|
+
def exist?(key)
|
48
|
+
instrument :exist, key do |payload|
|
49
|
+
answer = object_for(key).exists?
|
50
|
+
payload[:exist] = answer
|
51
|
+
answer
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def url(key, expires_in:, filename:, disposition:, content_type:)
|
56
|
+
instrument :url, key do |payload|
|
57
|
+
generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
|
58
|
+
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
|
59
|
+
response_content_type: content_type
|
37
60
|
|
38
|
-
|
39
|
-
|
40
|
-
|
61
|
+
payload[:url] = generated_url
|
62
|
+
|
63
|
+
generated_url
|
64
|
+
end
|
41
65
|
end
|
42
66
|
|
43
|
-
|
44
|
-
|
45
|
-
|
67
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
68
|
+
instrument :url, key do |payload|
|
69
|
+
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
|
70
|
+
content_type: content_type, content_length: content_length, content_md5: checksum
|
46
71
|
|
47
|
-
|
48
|
-
offset = 0
|
72
|
+
payload[:url] = generated_url
|
49
73
|
|
50
|
-
|
51
|
-
yield object.read(options.merge(:range => "bytes=#{offset}-#{offset + chunk_size - 1}"))
|
52
|
-
offset += chunk_size
|
74
|
+
generated_url
|
53
75
|
end
|
54
76
|
end
|
77
|
+
|
78
|
+
def headers_for_direct_upload(key, content_type:, checksum:, **)
|
79
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum }
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def object_for(key)
|
84
|
+
bucket.object(key)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Reads the object for the given key in chunks, yielding each to the block.
|
88
|
+
def stream(key)
|
89
|
+
object = object_for(key)
|
90
|
+
|
91
|
+
chunk_size = 5.megabytes
|
92
|
+
offset = 0
|
93
|
+
|
94
|
+
while offset < object.content_length
|
95
|
+
yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
|
96
|
+
offset += chunk_size
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
55
100
|
end
|