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,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "pathname"
|
5
|
+
require "digest/md5"
|
6
|
+
require "active_support/core_ext/numeric/bytes"
|
7
|
+
|
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
|
13
|
+
|
14
|
+
def initialize(root:)
|
15
|
+
@root = root
|
16
|
+
end
|
17
|
+
|
18
|
+
def upload(key, io, checksum: nil, **)
|
19
|
+
instrument :upload, key: key, checksum: checksum do
|
20
|
+
IO.copy_stream(io, make_path_for(key))
|
21
|
+
ensure_integrity_of(key, checksum) if checksum
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def download(key, &block)
|
26
|
+
if block_given?
|
27
|
+
instrument :streaming_download, key: key do
|
28
|
+
stream key, &block
|
29
|
+
end
|
30
|
+
else
|
31
|
+
instrument :download, key: key do
|
32
|
+
File.binread path_for(key)
|
33
|
+
rescue Errno::ENOENT
|
34
|
+
raise ActiveStorage::FileNotFoundError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def download_chunk(key, range)
|
40
|
+
instrument :download_chunk, key: key, range: range do
|
41
|
+
File.open(path_for(key), "rb") do |file|
|
42
|
+
file.seek range.begin
|
43
|
+
file.read range.size
|
44
|
+
end
|
45
|
+
rescue Errno::ENOENT
|
46
|
+
raise ActiveStorage::FileNotFoundError
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(key)
|
51
|
+
instrument :delete, key: key do
|
52
|
+
File.delete path_for(key)
|
53
|
+
rescue Errno::ENOENT
|
54
|
+
# Ignore files already deleted
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete_prefixed(prefix)
|
59
|
+
instrument :delete_prefixed, prefix: prefix do
|
60
|
+
Dir.glob(path_for("#{prefix}*")).each do |path|
|
61
|
+
FileUtils.rm_rf(path)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def exist?(key)
|
67
|
+
instrument :exist, key: key do |payload|
|
68
|
+
answer = File.exist? path_for(key)
|
69
|
+
payload[:exist] = answer
|
70
|
+
answer
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
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
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
104
|
+
instrument :url, key: key do |payload|
|
105
|
+
verified_token_with_expiration = ActiveStorage.verifier.generate(
|
106
|
+
{
|
107
|
+
key: key,
|
108
|
+
content_type: content_type,
|
109
|
+
content_length: content_length,
|
110
|
+
checksum: checksum
|
111
|
+
},
|
112
|
+
{ expires_in: expires_in,
|
113
|
+
purpose: :blob_token }
|
114
|
+
)
|
115
|
+
|
116
|
+
generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
|
117
|
+
|
118
|
+
payload[:url] = generated_url
|
119
|
+
|
120
|
+
generated_url
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def headers_for_direct_upload(key, content_type:, **)
|
125
|
+
{ "Content-Type" => content_type }
|
126
|
+
end
|
127
|
+
|
128
|
+
def path_for(key) #:nodoc:
|
129
|
+
File.join root, folder_for(key), key
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
def stream(key)
|
134
|
+
File.open(path_for(key), "rb") do |file|
|
135
|
+
while data = file.read(5.megabytes)
|
136
|
+
yield data
|
137
|
+
end
|
138
|
+
end
|
139
|
+
rescue Errno::ENOENT
|
140
|
+
raise ActiveStorage::FileNotFoundError
|
141
|
+
end
|
142
|
+
|
143
|
+
def folder_for(key)
|
144
|
+
[ key[0..1], key[2..3] ].join("/")
|
145
|
+
end
|
146
|
+
|
147
|
+
def make_path_for(key)
|
148
|
+
path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) }
|
149
|
+
end
|
150
|
+
|
151
|
+
def ensure_integrity_of(key, checksum)
|
152
|
+
unless Digest::MD5.file(path_for(key)).base64digest == checksum
|
153
|
+
delete key
|
154
|
+
raise ActiveStorage::IntegrityError
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def url_helpers
|
159
|
+
@url_helpers ||= Rails.application.routes.url_helpers
|
160
|
+
end
|
161
|
+
|
162
|
+
def current_host
|
163
|
+
ActiveStorage::Current.host
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "google-cloud-storage", "~> 1.11"
|
4
|
+
require "google/cloud/storage"
|
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
|
13
|
+
|
14
|
+
def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
|
15
|
+
instrument :upload, key: key, checksum: checksum do
|
16
|
+
# GCS's signed URLs don't include params such as response-content-type response-content_disposition
|
17
|
+
# in the signature, which means an attacker can modify them and bypass our effort to force these to
|
18
|
+
# binary and attachment when the file's content type requires it. The only way to force them is to
|
19
|
+
# store them as object's metadata.
|
20
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
21
|
+
bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
|
22
|
+
rescue Google::Cloud::InvalidArgumentError
|
23
|
+
raise ActiveStorage::IntegrityError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def download(key, &block)
|
28
|
+
if block_given?
|
29
|
+
instrument :streaming_download, key: key do
|
30
|
+
stream(key, &block)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
instrument :download, key: key do
|
34
|
+
file_for(key).download.string
|
35
|
+
rescue Google::Cloud::NotFoundError
|
36
|
+
raise ActiveStorage::FileNotFoundError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_metadata(key, content_type:, disposition: nil, filename: nil)
|
42
|
+
instrument :update_metadata, key: key, content_type: content_type, disposition: disposition do
|
43
|
+
file_for(key).update do |file|
|
44
|
+
file.content_type = content_type
|
45
|
+
file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def download_chunk(key, range)
|
51
|
+
instrument :download_chunk, key: key, range: range do
|
52
|
+
file_for(key).download(range: range).string
|
53
|
+
rescue Google::Cloud::NotFoundError
|
54
|
+
raise ActiveStorage::FileNotFoundError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete(key)
|
59
|
+
instrument :delete, key: key do
|
60
|
+
file_for(key).delete
|
61
|
+
rescue Google::Cloud::NotFoundError
|
62
|
+
# Ignore files already deleted
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def delete_prefixed(prefix)
|
67
|
+
instrument :delete_prefixed, prefix: prefix do
|
68
|
+
bucket.files(prefix: prefix).all do |file|
|
69
|
+
file.delete
|
70
|
+
rescue Google::Cloud::NotFoundError
|
71
|
+
# Ignore concurrently-deleted files
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def exist?(key)
|
77
|
+
instrument :exist, key: key do |payload|
|
78
|
+
answer = file_for(key).exists?
|
79
|
+
payload[:exist] = answer
|
80
|
+
answer
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
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
|
+
def url_for_direct_upload(key, expires_in:, checksum:, **)
|
98
|
+
instrument :url, key: key do |payload|
|
99
|
+
generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
|
100
|
+
|
101
|
+
payload[:url] = generated_url
|
102
|
+
|
103
|
+
generated_url
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def headers_for_direct_upload(key, checksum:, **)
|
108
|
+
{ "Content-MD5" => checksum }
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
attr_reader :config
|
113
|
+
|
114
|
+
def file_for(key, skip_lookup: true)
|
115
|
+
bucket.file(key, skip_lookup: skip_lookup)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Reads the file for the given key in chunks, yielding each to the block.
|
119
|
+
def stream(key)
|
120
|
+
file = file_for(key, skip_lookup: false)
|
121
|
+
|
122
|
+
chunk_size = 5.megabytes
|
123
|
+
offset = 0
|
124
|
+
|
125
|
+
raise ActiveStorage::FileNotFoundError unless file.present?
|
126
|
+
|
127
|
+
while offset < file.size
|
128
|
+
yield file.download(range: offset..(offset + chunk_size - 1)).string
|
129
|
+
offset += chunk_size
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def bucket
|
134
|
+
@bucket ||= client.bucket(config.fetch(:bucket), skip_lookup: true)
|
135
|
+
end
|
136
|
+
|
137
|
+
def client
|
138
|
+
@client ||= Google::Cloud::Storage.new(config.except(:bucket))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/delegation"
|
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
|
11
|
+
|
12
|
+
delegate :download, :download_chunk, :exist?, :url, :path_for, to: :primary
|
13
|
+
|
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
|
20
|
+
|
21
|
+
def initialize(primary:, mirrors:)
|
22
|
+
@primary, @mirrors = primary, mirrors
|
23
|
+
end
|
24
|
+
|
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, **options)
|
28
|
+
each_service.collect do |service|
|
29
|
+
service.upload key, io.tap(&:rewind), checksum: checksum, **options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Delete the file at the +key+ on all services.
|
34
|
+
def delete(key)
|
35
|
+
perform_across_services :delete, key
|
36
|
+
end
|
37
|
+
|
38
|
+
# Delete files at keys starting with the +prefix+ on all services.
|
39
|
+
def delete_prefixed(prefix)
|
40
|
+
perform_across_services :delete_prefixed, prefix
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def each_service(&block)
|
45
|
+
[ primary, *mirrors ].each(&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform_across_services(method, *args)
|
49
|
+
# FIXME: Convert to be threaded
|
50
|
+
each_service.collect do |service|
|
51
|
+
service.public_send method, *args
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "aws-sdk-s3"
|
4
|
+
require "active_support/core_ext/numeric/bytes"
|
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
|
11
|
+
|
12
|
+
def initialize(bucket:, upload: {}, **options)
|
13
|
+
@client = Aws::S3::Resource.new(**options)
|
14
|
+
@bucket = @client.bucket(bucket)
|
15
|
+
|
16
|
+
@upload_options = upload
|
17
|
+
end
|
18
|
+
|
19
|
+
def upload(key, io, checksum: nil, content_type: nil, **)
|
20
|
+
instrument :upload, key: key, checksum: checksum do
|
21
|
+
object_for(key).put(upload_options.merge(body: io, content_md5: checksum, content_type: content_type))
|
22
|
+
rescue Aws::S3::Errors::BadDigest
|
23
|
+
raise ActiveStorage::IntegrityError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def download(key, &block)
|
28
|
+
if block_given?
|
29
|
+
instrument :streaming_download, key: key do
|
30
|
+
stream(key, &block)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
instrument :download, key: key do
|
34
|
+
object_for(key).get.body.string.force_encoding(Encoding::BINARY)
|
35
|
+
rescue Aws::S3::Errors::NoSuchKey
|
36
|
+
raise ActiveStorage::FileNotFoundError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def download_chunk(key, range)
|
42
|
+
instrument :download_chunk, key: key, range: range do
|
43
|
+
object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
|
44
|
+
rescue Aws::S3::Errors::NoSuchKey
|
45
|
+
raise ActiveStorage::FileNotFoundError
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete(key)
|
50
|
+
instrument :delete, key: key do
|
51
|
+
object_for(key).delete
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_prefixed(prefix)
|
56
|
+
instrument :delete_prefixed, prefix: prefix do
|
57
|
+
bucket.objects(prefix: prefix).batch_delete!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def exist?(key)
|
62
|
+
instrument :exist, key: key do |payload|
|
63
|
+
answer = object_for(key).exists?
|
64
|
+
payload[:exist] = answer
|
65
|
+
answer
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def url(key, expires_in:, filename:, disposition:, content_type:)
|
70
|
+
instrument :url, key: key do |payload|
|
71
|
+
generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
|
72
|
+
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
|
73
|
+
response_content_type: content_type
|
74
|
+
|
75
|
+
payload[:url] = generated_url
|
76
|
+
|
77
|
+
generated_url
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
82
|
+
instrument :url, key: key do |payload|
|
83
|
+
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
|
84
|
+
content_type: content_type, content_length: content_length, content_md5: checksum
|
85
|
+
|
86
|
+
payload[:url] = generated_url
|
87
|
+
|
88
|
+
generated_url
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def headers_for_direct_upload(key, content_type:, checksum:, **)
|
93
|
+
{ "Content-Type" => content_type, "Content-MD5" => checksum }
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def object_for(key)
|
98
|
+
bucket.object(key)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Reads the object for the given key in chunks, yielding each to the block.
|
102
|
+
def stream(key)
|
103
|
+
object = object_for(key)
|
104
|
+
|
105
|
+
chunk_size = 5.megabytes
|
106
|
+
offset = 0
|
107
|
+
|
108
|
+
raise ActiveStorage::FileNotFoundError unless object.exists?
|
109
|
+
|
110
|
+
while offset < object.content_length
|
111
|
+
yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
|
112
|
+
offset += chunk_size
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|