shrine 2.12.0 → 2.13.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +153 -41
- data/doc/advantages.md +96 -106
- data/doc/attacher.md +55 -18
- data/doc/design.md +16 -5
- data/doc/direct_s3.md +132 -113
- data/doc/metadata.md +82 -27
- data/doc/multiple_files.md +76 -33
- data/doc/processing.md +2 -11
- data/doc/testing.md +2 -2
- data/lib/shrine.rb +18 -10
- data/lib/shrine/plugins/determine_mime_type.rb +6 -1
- data/lib/shrine/plugins/download_endpoint.rb +3 -0
- data/lib/shrine/plugins/infer_extension.rb +25 -10
- data/lib/shrine/plugins/module_include.rb +1 -1
- data/lib/shrine/plugins/presign_endpoint.rb +10 -8
- data/lib/shrine/plugins/rack_file.rb +8 -0
- data/lib/shrine/plugins/store_dimensions.rb +1 -1
- data/lib/shrine/plugins/upload_endpoint.rb +8 -4
- data/lib/shrine/plugins/versions.rb +1 -2
- data/lib/shrine/storage/file_system.rb +6 -4
- data/lib/shrine/storage/s3.rb +89 -82
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +3 -2
- metadata +22 -8
@@ -179,9 +179,14 @@ class Shrine
|
|
179
179
|
|
180
180
|
raise Error, "file command failed to spawn: #{stderr.read}" if status.nil?
|
181
181
|
raise Error, "file command failed: #{stderr.read}" unless status.success?
|
182
|
+
|
182
183
|
$stderr.print(stderr.read)
|
183
184
|
|
184
|
-
stdout.read.strip
|
185
|
+
output = stdout.read.strip
|
186
|
+
|
187
|
+
raise Error, "file command failed: #{output}" if output.include?("cannot open")
|
188
|
+
|
189
|
+
output
|
185
190
|
end
|
186
191
|
rescue Errno::ENOENT
|
187
192
|
raise Error, "file command-line tool is not installed"
|
@@ -12,6 +12,9 @@ class Shrine
|
|
12
12
|
# from your storage isn't accessible over URL (e.g. database storages) or
|
13
13
|
# if you want to authenticate your downloads. It requires the [Roda] gem.
|
14
14
|
#
|
15
|
+
# # Gemfile
|
16
|
+
# gem "roda" # dependency of the download_endpoint plugin
|
17
|
+
#
|
15
18
|
# You can configure the plugin with the path prefix which the endpoint will
|
16
19
|
# be mounted on.
|
17
20
|
#
|
@@ -5,21 +5,29 @@ class Shrine
|
|
5
5
|
# The `infer_extension` plugin allows deducing the appropriate file
|
6
6
|
# extension for the upload location based on the MIME type of the file.
|
7
7
|
# This is useful when using `data_uri` and `remote_url` plugins, where the
|
8
|
-
# file extension
|
8
|
+
# file extension might not be known.
|
9
9
|
#
|
10
10
|
# plugin :infer_extension
|
11
11
|
#
|
12
|
-
#
|
13
|
-
# determined from the filename.
|
14
|
-
#
|
12
|
+
# Ordinarily, the upload location will gain the inferred extension only if
|
13
|
+
# it couldn't be determined from the filename. However, you can pass
|
14
|
+
# `force: true` to force the inferred extension to be used rather than an
|
15
|
+
# extension from the original filename. This can be used to canonicalize
|
16
|
+
# extensions (jpg, jpeg => jpeg), or replace an incorrect original
|
17
|
+
# extension.
|
18
|
+
#
|
19
|
+
# plugin :infer_extension, force: true
|
20
|
+
#
|
21
|
+
# By default `MIME::Types` will be used for inferring the extension, but
|
22
|
+
# you can also choose a different inferrer:
|
15
23
|
#
|
16
24
|
# plugin :infer_extension, inferrer: :mini_mime
|
17
25
|
#
|
18
26
|
# The following inferrers are accepted:
|
19
27
|
#
|
20
28
|
# :mime_types
|
21
|
-
# : (Default). Uses the [mime-types] gem to infer the appropriate extension
|
22
|
-
# type.
|
29
|
+
# : (Default). Uses the [mime-types] gem to infer the appropriate extension
|
30
|
+
# from MIME type.
|
23
31
|
#
|
24
32
|
# :mini_mime
|
25
33
|
# : Uses the [mini_mime] gem to infer the appropriate extension from MIME
|
@@ -45,12 +53,13 @@ class Shrine
|
|
45
53
|
# [mini_mime]: https://github.com/discourse/mini_mime
|
46
54
|
module InferExtension
|
47
55
|
def self.configure(uploader, opts = {})
|
48
|
-
uploader.opts[:
|
56
|
+
uploader.opts[:infer_extension_inferrer] = opts.fetch(:inferrer, uploader.opts.fetch(:infer_extension_inferrer, :mime_types))
|
57
|
+
uploader.opts[:infer_extension_force] = opts.fetch(:force, uploader.opts.fetch(:infer_extension_force, false))
|
49
58
|
end
|
50
59
|
|
51
60
|
module ClassMethods
|
52
61
|
def infer_extension(mime_type)
|
53
|
-
inferrer = opts[:
|
62
|
+
inferrer = opts[:infer_extension_inferrer]
|
54
63
|
inferrer = extension_inferrer(inferrer) if inferrer.is_a?(Symbol)
|
55
64
|
args = [mime_type, extension_inferrers].take(inferrer.arity.abs)
|
56
65
|
|
@@ -72,8 +81,14 @@ class Shrine
|
|
72
81
|
def generate_location(io, context = {})
|
73
82
|
mime_type = (context[:metadata] || {})["mime_type"]
|
74
83
|
|
75
|
-
location
|
76
|
-
|
84
|
+
location = super
|
85
|
+
current_extension = File.extname(location)
|
86
|
+
|
87
|
+
if current_extension.empty? || opts[:infer_extension_force]
|
88
|
+
inferred_extension = infer_extension(mime_type)
|
89
|
+
location = location.chomp(current_extension) << inferred_extension unless inferred_extension.empty?
|
90
|
+
end
|
91
|
+
|
77
92
|
location
|
78
93
|
end
|
79
94
|
|
@@ -72,17 +72,16 @@ class Shrine
|
|
72
72
|
# ## Options
|
73
73
|
#
|
74
74
|
# Some storages accept additional presign options, which you can pass in via
|
75
|
-
# `:presign_options
|
75
|
+
# `:presign_options`, here is an example for S3 storage:
|
76
76
|
#
|
77
77
|
# plugin :presign_endpoint, presign_options: -> (request) do
|
78
78
|
# filename = request.params["filename"]
|
79
|
-
#
|
80
|
-
# content_type = Rack::Mime.mime_type(extension)
|
79
|
+
# type = request.params["type"]
|
81
80
|
#
|
82
81
|
# {
|
83
|
-
# content_length_range: 0..(10*1024*1024),
|
84
|
-
# content_disposition: "
|
85
|
-
# content_type:
|
82
|
+
# content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
|
83
|
+
# content_disposition: "inline; filename=\"#{filename}\"", # download with original filename
|
84
|
+
# content_type: type, # set correct content type
|
86
85
|
# }
|
87
86
|
# end
|
88
87
|
#
|
@@ -149,6 +148,9 @@ class Shrine
|
|
149
148
|
# `#presign` on the specified storage, and returns that information in
|
150
149
|
# JSON format.
|
151
150
|
class App
|
151
|
+
CONTENT_TYPE_JSON = "application/json; charset=utf-8"
|
152
|
+
CONTENT_TYPE_TEXT = "text/plain"
|
153
|
+
|
152
154
|
# Writes given options to instance variables.
|
153
155
|
def initialize(options)
|
154
156
|
options.each do |name, value|
|
@@ -240,7 +242,7 @@ class Shrine
|
|
240
242
|
if @rack_response
|
241
243
|
response = @rack_response.call(object, request)
|
242
244
|
else
|
243
|
-
response = [200, {"Content-Type" =>
|
245
|
+
response = [200, {"Content-Type" => CONTENT_TYPE_JSON}, [object.to_json]]
|
244
246
|
end
|
245
247
|
|
246
248
|
# prevent browsers from caching the response
|
@@ -251,7 +253,7 @@ class Shrine
|
|
251
253
|
|
252
254
|
# Used for early returning an error response.
|
253
255
|
def error!(status, message)
|
254
|
-
throw :halt, [status, {"Content-Type" =>
|
256
|
+
throw :halt, [status, {"Content-Type" => CONTENT_TYPE_TEXT}, [message]]
|
255
257
|
end
|
256
258
|
|
257
259
|
# Returns the uploader around the specified storage.
|
@@ -47,6 +47,14 @@ class Shrine
|
|
47
47
|
module ClassMethods
|
48
48
|
# Accepts a Rack uploaded file hash and wraps it in an IO object.
|
49
49
|
def rack_file(hash)
|
50
|
+
if hash[:filename]
|
51
|
+
# Rack can sometimes return the filename binary encoded, so we force
|
52
|
+
# the encoding to utf-8
|
53
|
+
hash = hash.merge(
|
54
|
+
filename: hash[:filename].dup.force_encoding(Encoding::UTF_8)
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
50
58
|
UploadedFile.new(hash)
|
51
59
|
end
|
52
60
|
end
|
@@ -64,7 +64,7 @@ class Shrine
|
|
64
64
|
#
|
65
65
|
# [fastimage]: https://github.com/sdsykes/fastimage
|
66
66
|
# [mini_magick]: https://github.com/minimagick/minimagick
|
67
|
-
# [ruby-vips]: https://github.com/
|
67
|
+
# [ruby-vips]: https://github.com/libvips/ruby-vips
|
68
68
|
module StoreDimensions
|
69
69
|
def self.configure(uploader, opts = {})
|
70
70
|
uploader.opts[:dimensions_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:dimensions_analyzer, :fastimage))
|
@@ -94,7 +94,7 @@ class Shrine
|
|
94
94
|
# You can also customize the upload itself via the `:upload` option:
|
95
95
|
#
|
96
96
|
# plugin :upload_endpoint, upload: -> (io, context, request) do
|
97
|
-
#
|
97
|
+
# Shrine.new(:cache).upload(io, context)
|
98
98
|
# end
|
99
99
|
#
|
100
100
|
# ## Response
|
@@ -150,6 +150,9 @@ class Shrine
|
|
150
150
|
# calls `#upload` with the uploaded file, and returns the uploaded file
|
151
151
|
# information in JSON format.
|
152
152
|
class App
|
153
|
+
CONTENT_TYPE_JSON = "application/json; charset=utf-8"
|
154
|
+
CONTENT_TYPE_TEXT = "text/plain"
|
155
|
+
|
153
156
|
# Writes given options to instance variables.
|
154
157
|
def initialize(options)
|
155
158
|
options.each do |name, value|
|
@@ -196,7 +199,8 @@ class Shrine
|
|
196
199
|
def get_io(request)
|
197
200
|
file = request.params["file"]
|
198
201
|
|
199
|
-
error!(400, "Upload Not Found")
|
202
|
+
error!(400, "Upload Not Found") if file.nil?
|
203
|
+
error!(400, "Upload Not Valid") unless file.is_a?(Hash) && file[:tempfile]
|
200
204
|
error!(413, "Upload Too Large") if @max_size && file[:tempfile].size > @max_size
|
201
205
|
|
202
206
|
verify_checksum!(file[:tempfile], request.env["HTTP_CONTENT_MD5"]) if request.env["HTTP_CONTENT_MD5"]
|
@@ -232,7 +236,7 @@ class Shrine
|
|
232
236
|
if @rack_response
|
233
237
|
@rack_response.call(object, request)
|
234
238
|
else
|
235
|
-
[200, {"Content-Type" =>
|
239
|
+
[200, {"Content-Type" => CONTENT_TYPE_JSON}, [object.to_json]]
|
236
240
|
end
|
237
241
|
end
|
238
242
|
|
@@ -246,7 +250,7 @@ class Shrine
|
|
246
250
|
|
247
251
|
# Used for early returning an error response.
|
248
252
|
def error!(status, message)
|
249
|
-
throw :halt, [status, {"Content-Type" =>
|
253
|
+
throw :halt, [status, {"Content-Type" => CONTENT_TYPE_TEXT}, [message]]
|
250
254
|
end
|
251
255
|
|
252
256
|
# Returns the uploader around the specified storage.
|
@@ -234,8 +234,7 @@ class Shrine
|
|
234
234
|
end
|
235
235
|
end
|
236
236
|
|
237
|
-
# Deletes each file individually
|
238
|
-
# capabilities.
|
237
|
+
# Deletes each file individually
|
239
238
|
def _delete(uploaded_file, context)
|
240
239
|
if (hash = uploaded_file).is_a?(Hash)
|
241
240
|
hash.each do |name, value|
|
@@ -165,8 +165,9 @@ class Shrine
|
|
165
165
|
# Delets the file, and by default deletes the containing directory if
|
166
166
|
# it's empty.
|
167
167
|
def delete(id)
|
168
|
-
path(id)
|
169
|
-
|
168
|
+
path = path(id)
|
169
|
+
path.delete
|
170
|
+
clean(path) if clean?
|
170
171
|
rescue Errno::ENOENT
|
171
172
|
end
|
172
173
|
|
@@ -241,8 +242,9 @@ class Shrine
|
|
241
242
|
|
242
243
|
# Creates all intermediate directories for that location.
|
243
244
|
def path!(id)
|
244
|
-
|
245
|
-
path
|
245
|
+
path = path(id)
|
246
|
+
FileUtils.mkdir_p(path.dirname, mode: directory_permissions)
|
247
|
+
path
|
246
248
|
end
|
247
249
|
|
248
250
|
def relative_path(id)
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -37,7 +37,7 @@ class Shrine
|
|
37
37
|
# region: "eu-west-1",
|
38
38
|
# )
|
39
39
|
#
|
40
|
-
# The core features of this storage
|
40
|
+
# The core features of this storage require the following AWS permissions:
|
41
41
|
# `s3:ListBucket`, `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`.
|
42
42
|
# If you have additional upload options configured such as setting object
|
43
43
|
# ACLs, then additional permissions may be required.
|
@@ -54,6 +54,18 @@ class Shrine
|
|
54
54
|
#
|
55
55
|
# s3.object("key") #=> #<Aws::S3::Object>
|
56
56
|
#
|
57
|
+
# ## Public uploads
|
58
|
+
#
|
59
|
+
# By default, uploaded S3 objects will have private visibility, meaning
|
60
|
+
# they can only be accessed via signed expiring URLs generated using your
|
61
|
+
# private S3 credentials. If you would like to generate public URLs, you
|
62
|
+
# can tell S3 storage to make uploads public:
|
63
|
+
#
|
64
|
+
# s3 = Shrine::Storage::S3.new(public: true, **s3_options)
|
65
|
+
#
|
66
|
+
# s3.upload(io, "key") # uploads with "public-read" ACL
|
67
|
+
# s3.url("key") # returns public (unsigned) object URL
|
68
|
+
#
|
57
69
|
# ## Prefix
|
58
70
|
#
|
59
71
|
# The `:prefix` option can be specified for uploading all files inside
|
@@ -103,22 +115,44 @@ class Shrine
|
|
103
115
|
#
|
104
116
|
# All other options are forwarded to the aws-sdk-s3 gem:
|
105
117
|
#
|
106
|
-
# s3.url(expires_in: 15)
|
107
|
-
# s3.url(virtual_host: true)
|
118
|
+
# s3.url(expires_in: 15, response_content_disposition: "...")
|
108
119
|
#
|
109
|
-
# ##
|
120
|
+
# ## URL Host
|
110
121
|
#
|
111
|
-
# If you
|
112
|
-
# the `:host` option to `#url`:
|
122
|
+
# If you want your S3 object URLs to be generated with a different URL host
|
123
|
+
# (e.g. a CDN), you can specify the `:host` option to `#url`:
|
113
124
|
#
|
114
125
|
# s3.url("image.jpg", host: "http://abc123.cloudfront.net")
|
115
126
|
# #=> "http://abc123.cloudfront.net/image.jpg"
|
116
127
|
#
|
117
|
-
#
|
128
|
+
# The host URL can include a path prefix, but it needs to end with a slash:
|
129
|
+
#
|
130
|
+
# s3.url("image.jpg", host: "https://your-s3-host.com/prefix/") # needs to end with a slash
|
131
|
+
# #=> "http://your-s3-host.com/prefix/image.jpg"
|
132
|
+
#
|
133
|
+
# To have the `:host` option passed automatically for every URL, use the
|
118
134
|
# `default_url_options` plugin.
|
119
135
|
#
|
120
136
|
# plugin :default_url_options, store: { host: "http://abc123.cloudfront.net" }
|
121
137
|
#
|
138
|
+
# If you would like to [serve private content via CloudFront], you need to
|
139
|
+
# sign the object URLs with a special signer, such as
|
140
|
+
# [`Aws::CloudFront::UrlSigner`] provided by the `aws-sdk-cloudfront` gem.
|
141
|
+
# The S3 storage initializer accepts a `:signer` block, which you can use
|
142
|
+
# to call your signer:
|
143
|
+
#
|
144
|
+
#
|
145
|
+
# require "aws-sdk-cloudfront"
|
146
|
+
#
|
147
|
+
# signer = Aws::CloudFront::UrlSigner.new(
|
148
|
+
# key_pair_id: "cf-keypair-id",
|
149
|
+
# private_key_path: "./cf_private_key.pem"
|
150
|
+
# )
|
151
|
+
#
|
152
|
+
# Shrine::Storage::S3.new(signer: signer.method(:signed_url))
|
153
|
+
# # or
|
154
|
+
# Shrine::Storage::S3.new(signer: -> (url, **options) { signer.signed_url(url, **options) })
|
155
|
+
#
|
122
156
|
# ## Accelerate endpoint
|
123
157
|
#
|
124
158
|
# To use Amazon S3's [Transfer Acceleration] feature, you can change the
|
@@ -177,10 +211,12 @@ class Shrine
|
|
177
211
|
# [aws-sdk-s3]: https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-s3
|
178
212
|
# [Transfer Acceleration]: http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
|
179
213
|
# [object lifecycle]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
214
|
+
# [serve private content via CloudFront]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html
|
215
|
+
# [`Aws::CloudFront::UrlSigner`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFront/UrlSigner.html
|
180
216
|
class S3
|
181
217
|
MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB
|
182
218
|
|
183
|
-
attr_reader :client, :bucket, :prefix, :host, :upload_options
|
219
|
+
attr_reader :client, :bucket, :prefix, :host, :upload_options, :signer, :public
|
184
220
|
|
185
221
|
# Initializes a storage for uploading to S3. All options are forwarded to
|
186
222
|
# [`Aws::S3::Client#initialize`], except the following:
|
@@ -213,7 +249,7 @@ class Shrine
|
|
213
249
|
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
214
250
|
# [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method
|
215
251
|
# [configuring AWS SDK]: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html
|
216
|
-
def initialize(bucket:, prefix: nil, host: nil, upload_options: {}, multipart_threshold: {}, **s3_options)
|
252
|
+
def initialize(bucket:, prefix: nil, host: nil, upload_options: {}, multipart_threshold: {}, signer: nil, public: nil, **s3_options)
|
217
253
|
Shrine.deprecation("The :host option to Shrine::Storage::S3#initialize is deprecated and will be removed in Shrine 3. Pass :host to S3#url instead, you can also use default_url_options plugin.") if host
|
218
254
|
resource = Aws::S3::Resource.new(**s3_options)
|
219
255
|
|
@@ -229,6 +265,8 @@ class Shrine
|
|
229
265
|
@host = host
|
230
266
|
@upload_options = upload_options
|
231
267
|
@multipart_threshold = multipart_threshold
|
268
|
+
@signer = signer
|
269
|
+
@public = public
|
232
270
|
end
|
233
271
|
|
234
272
|
# Returns an `Aws::S3::Resource` object.
|
@@ -250,9 +288,10 @@ class Shrine
|
|
250
288
|
options = {}
|
251
289
|
options[:content_type] = content_type if content_type
|
252
290
|
options[:content_disposition] = "inline; filename=\"#{filename}\"" if filename
|
291
|
+
options[:acl] = "public-read" if public
|
253
292
|
|
254
|
-
options.
|
255
|
-
options.
|
293
|
+
options.merge!(@upload_options)
|
294
|
+
options.merge!(upload_options)
|
256
295
|
|
257
296
|
options[:content_disposition] = encode_content_disposition(options[:content_disposition]) if options[:content_disposition]
|
258
297
|
|
@@ -306,11 +345,6 @@ class Shrine
|
|
306
345
|
|
307
346
|
# Returns the presigned URL to the file.
|
308
347
|
#
|
309
|
-
# :public
|
310
|
-
# : Controls whether the URL is signed (`false`) or unsigned (`true`).
|
311
|
-
# Note that for unsigned URLs the S3 bucket need to be modified to allow
|
312
|
-
# public URLs. Defaults to `false`.
|
313
|
-
#
|
314
348
|
# :host
|
315
349
|
# : This option replaces the host part of the returned URL, and is
|
316
350
|
# typically useful for setting CDN hosts (e.g.
|
@@ -326,11 +360,11 @@ class Shrine
|
|
326
360
|
#
|
327
361
|
# [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_url-instance_method
|
328
362
|
# [`Aws::S3::Object#public_url`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#public_url-instance_method
|
329
|
-
def url(id, download: nil, public:
|
363
|
+
def url(id, download: nil, public: self.public, host: self.host, **options)
|
330
364
|
options[:response_content_disposition] ||= "attachment" if download
|
331
365
|
options[:response_content_disposition] = encode_content_disposition(options[:response_content_disposition]) if options[:response_content_disposition]
|
332
366
|
|
333
|
-
if public
|
367
|
+
if public || signer
|
334
368
|
url = object(id).public_url(**options)
|
335
369
|
else
|
336
370
|
url = object(id).presigned_url(:get, **options)
|
@@ -338,8 +372,12 @@ class Shrine
|
|
338
372
|
|
339
373
|
if host
|
340
374
|
uri = URI.parse(url)
|
341
|
-
uri.path = uri.path.match(/^\/#{bucket.name}/).post_match unless uri.host.include?(bucket.name)
|
342
|
-
url = URI.join(host, uri.request_uri).to_s
|
375
|
+
uri.path = uri.path.match(/^\/#{bucket.name}/).post_match unless uri.host.include?(bucket.name)
|
376
|
+
url = URI.join(host, uri.request_uri[1..-1]).to_s
|
377
|
+
end
|
378
|
+
|
379
|
+
if signer
|
380
|
+
url = signer.call(url, **options)
|
343
381
|
end
|
344
382
|
|
345
383
|
url
|
@@ -353,8 +391,13 @@ class Shrine
|
|
353
391
|
#
|
354
392
|
# [`Aws::S3::Object#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
355
393
|
# [`Aws::S3::Object#presigned_url`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_url-instance_method
|
356
|
-
def presign(id, method: :post, **
|
357
|
-
options =
|
394
|
+
def presign(id, method: :post, **presign_options)
|
395
|
+
options = {}
|
396
|
+
options[:acl] = "public-read" if public
|
397
|
+
|
398
|
+
options.merge!(@upload_options)
|
399
|
+
options.merge!(presign_options)
|
400
|
+
|
358
401
|
options[:content_disposition] = encode_content_disposition(options[:content_disposition]) if options[:content_disposition]
|
359
402
|
|
360
403
|
if method == :post
|
@@ -427,33 +470,43 @@ class Shrine
|
|
427
470
|
|
428
471
|
# Uploads the file to S3. Uses multipart upload for large files.
|
429
472
|
def put(io, id, **options)
|
430
|
-
|
431
|
-
path = io.path
|
432
|
-
elsif io.is_a?(UploadedFile) && defined?(Storage::FileSystem) && io.storage.is_a?(Storage::FileSystem)
|
433
|
-
path = io.storage.path(io.id).to_s
|
434
|
-
end
|
473
|
+
bytes_uploaded = nil
|
435
474
|
|
436
|
-
if path
|
475
|
+
if (path = extract_path(io))
|
437
476
|
# use `upload_file` for files because it can do multipart upload
|
438
477
|
options = { multipart_threshold: @multipart_threshold[:upload] }.merge!(options)
|
439
478
|
object(id).upload_file(path, **options)
|
440
|
-
File.size(path)
|
479
|
+
bytes_uploaded = File.size(path)
|
441
480
|
else
|
442
481
|
io.to_io if io.is_a?(UploadedFile) # open if not already opened
|
443
482
|
|
444
|
-
if io.respond_to?(:size) && io.size
|
483
|
+
if io.respond_to?(:size) && io.size && (io.size <= @multipart_threshold[:upload] || !object(id).respond_to?(:upload_stream))
|
445
484
|
object(id).put(body: io, **options)
|
446
|
-
io.size
|
485
|
+
bytes_uploaded = io.size
|
486
|
+
elsif object(id).respond_to?(:upload_stream)
|
487
|
+
# `upload_stream` uses multipart upload
|
488
|
+
object(id).upload_stream(tempfile: true, **options) do |write_stream|
|
489
|
+
bytes_uploaded = IO.copy_stream(io, write_stream)
|
490
|
+
end
|
447
491
|
else
|
448
|
-
|
449
|
-
|
492
|
+
Shrine.deprecation "Uploading a file of unknown size with aws-sdk-s3 older than 1.14 is deprecated and will be removed in Shrine 3. Update to aws-sdk-s3 1.14 or higher."
|
493
|
+
|
494
|
+
Tempfile.create("shrine-s3", binmode: true) do |file|
|
495
|
+
bytes_uploaded = IO.copy_stream(io, file.path)
|
496
|
+
object(id).upload_file(file.path, **options)
|
497
|
+
end
|
450
498
|
end
|
451
499
|
end
|
500
|
+
|
501
|
+
bytes_uploaded
|
452
502
|
end
|
453
503
|
|
454
|
-
|
455
|
-
|
456
|
-
|
504
|
+
def extract_path(io)
|
505
|
+
if io.respond_to?(:path)
|
506
|
+
io.path
|
507
|
+
elsif io.is_a?(UploadedFile) && defined?(Storage::FileSystem) && io.storage.is_a?(Storage::FileSystem)
|
508
|
+
io.storage.path(io.id).to_s
|
509
|
+
end
|
457
510
|
end
|
458
511
|
|
459
512
|
# The file is copyable if it's on S3 and on the same Amazon account.
|
@@ -480,52 +533,6 @@ class Shrine
|
|
480
533
|
CGI.escape(filename).gsub("+", " ")
|
481
534
|
end
|
482
535
|
end
|
483
|
-
|
484
|
-
# Uploads IO objects of unknown size using the multipart API.
|
485
|
-
class MultipartUploader
|
486
|
-
def initialize(object)
|
487
|
-
@object = object
|
488
|
-
end
|
489
|
-
|
490
|
-
# Initiates multipart upload, uploads IO content into multiple parts,
|
491
|
-
# and completes the multipart upload. If an exception is raised, the
|
492
|
-
# multipart upload is automatically aborted.
|
493
|
-
def upload(io, **options)
|
494
|
-
multipart_upload = @object.initiate_multipart_upload(**options)
|
495
|
-
|
496
|
-
parts = upload_parts(multipart_upload, io)
|
497
|
-
bytes_uploaded = parts.inject(0) { |size, part| size + part.delete(:size) }
|
498
|
-
|
499
|
-
multipart_upload.complete(multipart_upload: { parts: parts })
|
500
|
-
|
501
|
-
bytes_uploaded
|
502
|
-
rescue
|
503
|
-
multipart_upload.abort if multipart_upload
|
504
|
-
raise
|
505
|
-
end
|
506
|
-
|
507
|
-
# Uploads parts until the IO object has reached EOF.
|
508
|
-
def upload_parts(multipart_upload, io)
|
509
|
-
1.step.inject([]) do |parts, part_number|
|
510
|
-
parts << upload_part(multipart_upload, io, part_number)
|
511
|
-
break parts if io.eof?
|
512
|
-
parts
|
513
|
-
end
|
514
|
-
end
|
515
|
-
|
516
|
-
# Uploads at most 5MB of IO content into a single multipart part.
|
517
|
-
def upload_part(multipart_upload, io, part_number)
|
518
|
-
Tempfile.create("shrine-s3-part-#{part_number}", binmode: true) do |body|
|
519
|
-
IO.copy_stream(io, body, MIN_PART_SIZE)
|
520
|
-
body.rewind
|
521
|
-
|
522
|
-
multipart_part = multipart_upload.part(part_number)
|
523
|
-
response = multipart_part.upload(body: body)
|
524
|
-
|
525
|
-
{ part_number: part_number, size: body.size, etag: response.etag }
|
526
|
-
end
|
527
|
-
end
|
528
|
-
end
|
529
536
|
end
|
530
537
|
end
|
531
538
|
end
|