shrine 2.13.0 → 2.14.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 +72 -0
- data/README.md +20 -16
- data/doc/creating_storages.md +0 -21
- data/doc/design.md +1 -0
- data/doc/direct_s3.md +26 -15
- data/doc/metadata.md +67 -22
- data/doc/multiple_files.md +3 -3
- data/doc/processing.md +1 -1
- data/doc/retrieving_uploads.md +184 -0
- data/lib/shrine.rb +268 -900
- data/lib/shrine/attacher.rb +271 -0
- data/lib/shrine/attachment.rb +97 -0
- data/lib/shrine/plugins.rb +29 -0
- data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
- data/lib/shrine/plugins/activerecord.rb +16 -14
- data/lib/shrine/plugins/add_metadata.rb +58 -24
- data/lib/shrine/plugins/backgrounding.rb +6 -1
- data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
- data/lib/shrine/plugins/copy.rb +12 -8
- data/lib/shrine/plugins/data_uri.rb +23 -20
- data/lib/shrine/plugins/default_url_options.rb +5 -4
- data/lib/shrine/plugins/determine_mime_type.rb +24 -23
- data/lib/shrine/plugins/download_endpoint.rb +61 -73
- data/lib/shrine/plugins/migration_helpers.rb +17 -17
- data/lib/shrine/plugins/module_include.rb +9 -8
- data/lib/shrine/plugins/presign_endpoint.rb +13 -7
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +128 -36
- data/lib/shrine/plugins/refresh_metadata.rb +20 -5
- data/lib/shrine/plugins/remote_url.rb +8 -8
- data/lib/shrine/plugins/remove_attachment.rb +9 -9
- data/lib/shrine/plugins/sequel.rb +21 -18
- data/lib/shrine/plugins/tempfile.rb +68 -0
- data/lib/shrine/plugins/upload_endpoint.rb +3 -2
- data/lib/shrine/plugins/upload_options.rb +7 -6
- data/lib/shrine/plugins/validation_helpers.rb +2 -1
- data/lib/shrine/storage/file_system.rb +20 -17
- data/lib/shrine/storage/linter.rb +0 -7
- data/lib/shrine/storage/s3.rb +159 -50
- data/lib/shrine/uploaded_file.rb +258 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +7 -19
- metadata +41 -21
@@ -70,33 +70,36 @@ class Shrine
|
|
70
70
|
|
71
71
|
return unless model < ::Sequel::Model
|
72
72
|
|
73
|
-
|
73
|
+
name = attachment_name
|
74
74
|
|
75
|
-
|
76
|
-
|
77
|
-
super
|
78
|
-
#{
|
79
|
-
errors.add(
|
75
|
+
if shrine_class.opts[:sequel_validations]
|
76
|
+
define_method :validate do
|
77
|
+
super()
|
78
|
+
send(:"#{name}_attacher").errors.each do |message|
|
79
|
+
errors.add(name, *message)
|
80
80
|
end
|
81
81
|
end
|
82
|
-
|
82
|
+
end
|
83
83
|
|
84
|
-
|
85
|
-
|
86
|
-
super
|
87
|
-
|
84
|
+
if shrine_class.opts[:sequel_callbacks]
|
85
|
+
define_method :before_save do
|
86
|
+
super()
|
87
|
+
attacher = send(:"#{name}_attacher")
|
88
|
+
attacher.save if attacher.changed?
|
88
89
|
end
|
89
90
|
|
90
|
-
|
91
|
-
super
|
92
|
-
|
91
|
+
define_method :after_save do
|
92
|
+
super()
|
93
|
+
attacher = send(:"#{name}_attacher")
|
94
|
+
db.after_commit { attacher.finalize } if attacher.changed?
|
93
95
|
end
|
94
96
|
|
95
|
-
|
96
|
-
super
|
97
|
-
|
97
|
+
define_method :after_destroy do
|
98
|
+
super()
|
99
|
+
attacher = send(:"#{name}_attacher")
|
100
|
+
db.after_commit { attacher.destroy } if attacher.read
|
98
101
|
end
|
99
|
-
|
102
|
+
end
|
100
103
|
end
|
101
104
|
end
|
102
105
|
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The `tempfile` plugin makes it easier to reuse a single copy of an
|
4
|
+
# uploaded file on disk.
|
5
|
+
#
|
6
|
+
# Shrine.plugin :tempfile
|
7
|
+
#
|
8
|
+
# The plugin provides the `UploadedFile#tempfile` method, which when called
|
9
|
+
# on an open uploaded file will return a copy of its content on disk. The
|
10
|
+
# first time the method is called the file content will cached into a
|
11
|
+
# temporary file and returned. On any subsequent method calls the cached
|
12
|
+
# temporary file will be returned directly. The temporary file is deleted
|
13
|
+
# when the uploaded file is closed.
|
14
|
+
#
|
15
|
+
# uploaded_file.open do
|
16
|
+
# # ...
|
17
|
+
# uploaded_file.tempfile #=> #<Tempfile:...> (file is cached)
|
18
|
+
# # ...
|
19
|
+
# uploaded_file.tempfile #=> #<Tempfile:...> (cache is returned)
|
20
|
+
# # ...
|
21
|
+
# end # tempfile is deleted
|
22
|
+
#
|
23
|
+
# # OR
|
24
|
+
#
|
25
|
+
# uploaded_file.open
|
26
|
+
# # ...
|
27
|
+
# uploaded_file.tempfile #=> #<Tempfile:...> (file is cached)
|
28
|
+
# # ...
|
29
|
+
# uploaded_file.tempfile #=> #<Tempfile:...> (cache is returned)
|
30
|
+
# # ...
|
31
|
+
# uploaded_file.close # tempfile is deleted
|
32
|
+
#
|
33
|
+
# This plugin also modifies `Shrine.with_file` to call
|
34
|
+
# `UploadedFile#tempfile` when the given IO object is an open
|
35
|
+
# `UploadedFile`. Since `Shrine.with_file` is typically called on the
|
36
|
+
# `Shrine` class directly, it's recommended to load this plugin globally.
|
37
|
+
module Tempfile
|
38
|
+
module ClassMethods
|
39
|
+
def with_file(io)
|
40
|
+
if io.is_a?(UploadedFile) && io.opened?
|
41
|
+
yield io.tempfile
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module FileMethods
|
49
|
+
def tempfile
|
50
|
+
raise Error, "uploaded file must be opened" unless @io
|
51
|
+
|
52
|
+
@tempfile ||= download
|
53
|
+
@tempfile.rewind
|
54
|
+
@tempfile
|
55
|
+
end
|
56
|
+
|
57
|
+
def close
|
58
|
+
super
|
59
|
+
|
60
|
+
@tempfile.close! if @tempfile
|
61
|
+
@tempfile = nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
register_plugin(:tempfile, Tempfile)
|
67
|
+
end
|
68
|
+
end
|
@@ -135,14 +135,15 @@ class Shrine
|
|
135
135
|
# Additional options can be given to override the options given on
|
136
136
|
# plugin initialization.
|
137
137
|
def upload_endpoint(storage_key, **options)
|
138
|
-
App.new(
|
138
|
+
App.new(
|
139
139
|
shrine_class: self,
|
140
140
|
storage_key: storage_key,
|
141
141
|
max_size: opts[:upload_endpoint_max_size],
|
142
142
|
upload_context: opts[:upload_endpoint_upload_context],
|
143
143
|
upload: opts[:upload_endpoint_upload],
|
144
144
|
rack_response: opts[:upload_endpoint_rack_response],
|
145
|
-
|
145
|
+
**options
|
146
|
+
)
|
146
147
|
end
|
147
148
|
end
|
148
149
|
|
@@ -5,32 +5,33 @@ class Shrine
|
|
5
5
|
# The `upload_options` plugin allows you to automatically pass additional
|
6
6
|
# upload options to storage on every upload:
|
7
7
|
#
|
8
|
-
# plugin :upload_options, cache: {acl: "private"}
|
8
|
+
# plugin :upload_options, cache: { acl: "private" }
|
9
9
|
#
|
10
10
|
# Keys are names of the registered storages, and values are either hashes
|
11
11
|
# or blocks.
|
12
12
|
#
|
13
13
|
# plugin :upload_options, store: ->(io, context) do
|
14
14
|
# if [:original, :thumb].include?(context[:version])
|
15
|
-
# {acl: "public-read"}
|
15
|
+
# { acl: "public-read" }
|
16
16
|
# else
|
17
|
-
# {acl: "private"}
|
17
|
+
# { acl: "private" }
|
18
18
|
# end
|
19
19
|
# end
|
20
20
|
#
|
21
21
|
# If you're uploading the file directly, you can also pass `:upload_options`
|
22
22
|
# to the uploader.
|
23
23
|
#
|
24
|
-
# uploader.upload(file, upload_options: {acl: "public-read"})
|
24
|
+
# uploader.upload(file, upload_options: { acl: "public-read" })
|
25
25
|
module UploadOptions
|
26
26
|
def self.configure(uploader, options = {})
|
27
|
-
uploader.opts[:upload_options]
|
27
|
+
uploader.opts[:upload_options] ||= {}
|
28
|
+
uploader.opts[:upload_options].merge!(options)
|
28
29
|
end
|
29
30
|
|
30
31
|
module InstanceMethods
|
31
32
|
def put(io, context)
|
32
33
|
upload_options = get_upload_options(io, context)
|
33
|
-
context = {upload_options: upload_options}.merge(context)
|
34
|
+
context = { upload_options: upload_options }.merge(context)
|
34
35
|
super
|
35
36
|
end
|
36
37
|
|
@@ -40,7 +40,8 @@ class Shrine
|
|
40
40
|
# For a complete list of all validation helpers, see AttacherMethods.
|
41
41
|
module ValidationHelpers
|
42
42
|
def self.configure(uploader, opts = {})
|
43
|
-
uploader.opts[:validation_default_messages]
|
43
|
+
uploader.opts[:validation_default_messages] ||= {}
|
44
|
+
uploader.opts[:validation_default_messages].merge!(opts[:default_messages] || {})
|
44
45
|
end
|
45
46
|
|
46
47
|
DEFAULT_MESSAGES = {
|
@@ -8,6 +8,8 @@ class Shrine
|
|
8
8
|
# The FileSystem storage handles uploads to the filesystem, and it is
|
9
9
|
# most commonly initialized with a "base" folder and a "prefix":
|
10
10
|
#
|
11
|
+
# require "shrine/storage/file_system"
|
12
|
+
#
|
11
13
|
# storage = Shrine::Storage::FileSystem.new("public", prefix: "uploads")
|
12
14
|
# storage.url("image.jpg") #=> "/uploads/image.jpg"
|
13
15
|
#
|
@@ -153,8 +155,8 @@ class Shrine
|
|
153
155
|
|
154
156
|
# Opens the file on the given location in read mode. Accepts additional
|
155
157
|
# `File.open` arguments.
|
156
|
-
def open(id,
|
157
|
-
path(id).open(
|
158
|
+
def open(id, **options, &block)
|
159
|
+
path(id).open(binmode: true, **options, &block)
|
158
160
|
end
|
159
161
|
|
160
162
|
# Returns true if the file exists on the filesystem.
|
@@ -186,16 +188,15 @@ class Shrine
|
|
186
188
|
# time.
|
187
189
|
def clear!(older_than: nil)
|
188
190
|
if older_than
|
189
|
-
|
191
|
+
# add trailing slash to make it work with symlinks
|
192
|
+
Pathname("#{directory}/").find do |path|
|
190
193
|
if path.file? && path.mtime < older_than
|
191
194
|
path.delete
|
192
195
|
clean(path) if clean?
|
193
196
|
end
|
194
197
|
end
|
195
198
|
else
|
196
|
-
directory.rmtree
|
197
|
-
directory.mkpath
|
198
|
-
directory.chmod(directory_permissions) if directory_permissions
|
199
|
+
directory.children.each(&:rmtree)
|
199
200
|
end
|
200
201
|
end
|
201
202
|
|
@@ -205,17 +206,9 @@ class Shrine
|
|
205
206
|
end
|
206
207
|
|
207
208
|
# Catches the deprecated `#download` method.
|
208
|
-
def method_missing(name, *args)
|
209
|
-
|
210
|
-
|
211
|
-
Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
|
212
|
-
tempfile = Tempfile.new(["shrine-filesystem", File.extname(args[0])], binmode: true)
|
213
|
-
open(*args) { |file| IO.copy_stream(file, tempfile) }
|
214
|
-
tempfile.tap(&:open)
|
215
|
-
rescue
|
216
|
-
tempfile.close! if tempfile
|
217
|
-
raise
|
218
|
-
end
|
209
|
+
def method_missing(name, *args, &block)
|
210
|
+
case name
|
211
|
+
when :download then deprecated_download(*args, &block)
|
219
212
|
else
|
220
213
|
super
|
221
214
|
end
|
@@ -254,6 +247,16 @@ class Shrine
|
|
254
247
|
def relative(path)
|
255
248
|
path.sub(%r{^/}, "")
|
256
249
|
end
|
250
|
+
|
251
|
+
def deprecated_download(id, **options)
|
252
|
+
Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
|
253
|
+
tempfile = Tempfile.new(["shrine-filesystem", File.extname(id)], binmode: true)
|
254
|
+
open(id, **options) { |file| IO.copy_stream(file, tempfile) }
|
255
|
+
tempfile.tap(&:open)
|
256
|
+
rescue
|
257
|
+
tempfile.close! if tempfile
|
258
|
+
raise
|
259
|
+
end
|
257
260
|
end
|
258
261
|
end
|
259
262
|
end
|
@@ -38,7 +38,6 @@ class Shrine
|
|
38
38
|
def call(io_factory = default_io_factory)
|
39
39
|
storage.upload(io_factory.call, id = "foo".dup, {})
|
40
40
|
|
41
|
-
lint_download(id) if storage.respond_to?(:download)
|
42
41
|
lint_open(id)
|
43
42
|
lint_exists(id)
|
44
43
|
lint_url(id)
|
@@ -59,12 +58,6 @@ class Shrine
|
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
62
|
-
def lint_download(id)
|
63
|
-
downloaded = storage.download(id)
|
64
|
-
error :download, "doesn't return a Tempfile" if !downloaded.is_a?(Tempfile)
|
65
|
-
error :download, "returns an empty IO object" if downloaded.read.empty?
|
66
|
-
end
|
67
|
-
|
68
61
|
def lint_open(id)
|
69
62
|
opened = storage.open(id)
|
70
63
|
error :open, "doesn't return a valid IO object" if !io?(opened)
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -17,6 +17,8 @@ rescue LoadError => exception
|
|
17
17
|
end
|
18
18
|
|
19
19
|
require "down/chunked_io"
|
20
|
+
require "content_disposition"
|
21
|
+
|
20
22
|
require "uri"
|
21
23
|
require "cgi"
|
22
24
|
require "tempfile"
|
@@ -30,6 +32,8 @@ class Shrine
|
|
30
32
|
#
|
31
33
|
# It can be initialized by providing the bucket name and credentials:
|
32
34
|
#
|
35
|
+
# require "shrine/storage/s3"
|
36
|
+
#
|
33
37
|
# s3 = Shrine::Storage::S3.new(
|
34
38
|
# bucket: "my-app", # required
|
35
39
|
# access_key_id: "abc",
|
@@ -153,24 +157,24 @@ class Shrine
|
|
153
157
|
# # or
|
154
158
|
# Shrine::Storage::S3.new(signer: -> (url, **options) { signer.signed_url(url, **options) })
|
155
159
|
#
|
156
|
-
# ##
|
160
|
+
# ## Presigns
|
157
161
|
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
# will be applied both to regular and presigned uploads, as well as
|
161
|
-
# download URLs.
|
162
|
+
# The `#presign` method can be used for generating paramters for direct
|
163
|
+
# uploads to Amazon S3:
|
162
164
|
#
|
163
|
-
#
|
165
|
+
# s3.presign("/path/to/file") #=>
|
166
|
+
# # {
|
167
|
+
# # url: "https://my-bucket.s3.amazonaws.com/...",
|
168
|
+
# # fields: { ... }, # blank for PUT presigns
|
169
|
+
# # headers: { ... }, # blank for POST presigns
|
170
|
+
# # method: "post",
|
171
|
+
# # }
|
164
172
|
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
# This storage can generate presigns for direct uploads to Amazon S3, and
|
168
|
-
# it accepts additional options which are passed to aws-sdk-s3. There are
|
169
|
-
# three places in which you can specify presign options:
|
173
|
+
# Additional presign options can be given in three places:
|
170
174
|
#
|
175
|
+
# * in `Storage::S3#presign` by forwarding options
|
171
176
|
# * in `:upload_options` option on this storage
|
172
177
|
# * in `presign_endpoint` plugin through `:presign_options`
|
173
|
-
# * in `Storage::S3#presign` by forwarding options
|
174
178
|
#
|
175
179
|
# ## Large files
|
176
180
|
#
|
@@ -194,6 +198,55 @@ class Shrine
|
|
194
198
|
# { thread_count: 5 }
|
195
199
|
# end
|
196
200
|
#
|
201
|
+
# ## Encryption
|
202
|
+
#
|
203
|
+
# The easiest way to use server-side encryption for uploaded S3 objects is
|
204
|
+
# to configure default encryption for your S3 bucket. Alternatively, you
|
205
|
+
# can pass server-side encryption parameters to the API calls.
|
206
|
+
#
|
207
|
+
# The `#upload` method accepts `:sse_*` options:
|
208
|
+
#
|
209
|
+
# s3.upload(io, "key", sse_customer_algorithm: "AES256",
|
210
|
+
# sse_customer_key: "secret_key",
|
211
|
+
# sse_customer_key_md5: "secret_key_md5",
|
212
|
+
# ssekms_key_id: "key_id")
|
213
|
+
#
|
214
|
+
# The `#presign` method accepts `:server_side_encryption_*` options for
|
215
|
+
# POST presigns, and the same `:sse_*` options as above for PUT presigns.
|
216
|
+
#
|
217
|
+
# s3.presign("key", server_side_encryption_customer_algorithm: "AES256",
|
218
|
+
# server_side_encryption_customer_key: "secret_key",
|
219
|
+
# server_side_encryption_aws_kms_key_id: "key_id")
|
220
|
+
#
|
221
|
+
# When downloading encrypted S3 objects, the same server-side encryption
|
222
|
+
# parameters need to be passed in.
|
223
|
+
#
|
224
|
+
# s3.download("key", sse_customer_algorithm: "AES256",
|
225
|
+
# sse_customer_key: "secret_key",
|
226
|
+
# sse_customer_key_md5: "secret_key_md5")
|
227
|
+
#
|
228
|
+
# s3.open("key", sse_customer_algorithm: "AES256",
|
229
|
+
# sse_customer_key: "secret_key",
|
230
|
+
# sse_customer_key_md5: "secret_key_md5")
|
231
|
+
#
|
232
|
+
# If you want to use client-side encryption instead, you can instantiate
|
233
|
+
# the storage with an `Aws::S3::Encryption::Client` instance.
|
234
|
+
#
|
235
|
+
# client = Aws::S3::Encryption::Client.new(
|
236
|
+
# kms_key_id: "alias/my-key"
|
237
|
+
# )
|
238
|
+
#
|
239
|
+
# Shrine::Storage::S3(client: client, bucket: "my-bucket")
|
240
|
+
#
|
241
|
+
# ## Accelerate endpoint
|
242
|
+
#
|
243
|
+
# To use Amazon S3's [Transfer Acceleration] feature, you can change the
|
244
|
+
# `:endpoint` of the underlying client to the accelerate endpoint, and this
|
245
|
+
# will be applied both to regular and presigned uploads, as well as
|
246
|
+
# download URLs.
|
247
|
+
#
|
248
|
+
# Shrine::Storage::S3.new(endpoint: "https://s3-accelerate.amazonaws.com")
|
249
|
+
#
|
197
250
|
# ## Clearing cache
|
198
251
|
#
|
199
252
|
# If you're using S3 as a cache, you will probably want to periodically
|
@@ -205,6 +258,14 @@ class Shrine
|
|
205
258
|
# # deletes all objects that were uploaded more than 7 days ago
|
206
259
|
# s3.clear! { |object| object.last_modified < Time.now - 7*24*60*60 }
|
207
260
|
#
|
261
|
+
# ## Request Rate and Performance Guidelines
|
262
|
+
#
|
263
|
+
# Amazon S3 automatically scales to high request rates. For example, your
|
264
|
+
# application can achieve at least 3,500 PUT/POST/DELETE and 5,500 GET
|
265
|
+
# requests per second per prefix in a bucket (a prefix is a top-level
|
266
|
+
# "directory" in the bucket). If your app needs to support higher request
|
267
|
+
# rates to S3 than that, you can scale exponentially by using more prefixes.
|
268
|
+
#
|
208
269
|
# [uploading]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
209
270
|
# [copying]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#copy_from-instance_method
|
210
271
|
# [presigning]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
@@ -214,8 +275,6 @@ class Shrine
|
|
214
275
|
# [serve private content via CloudFront]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html
|
215
276
|
# [`Aws::CloudFront::UrlSigner`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CloudFront/UrlSigner.html
|
216
277
|
class S3
|
217
|
-
MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB
|
218
|
-
|
219
278
|
attr_reader :client, :bucket, :prefix, :host, :upload_options, :signer, :public
|
220
279
|
|
221
280
|
# Initializes a storage for uploading to S3. All options are forwarded to
|
@@ -224,6 +283,12 @@ class Shrine
|
|
224
283
|
# :bucket
|
225
284
|
# : (Required). Name of the S3 bucket.
|
226
285
|
#
|
286
|
+
# :client
|
287
|
+
# : By default an `Aws::S3::Client` instance is created internally from
|
288
|
+
# additional options, but you can use this option to provide your own
|
289
|
+
# client. This can be an `Aws::S3::Client` or an
|
290
|
+
# `Aws::S3::Encryption::Client` object.
|
291
|
+
#
|
227
292
|
# :prefix
|
228
293
|
# : "Directory" inside the bucket to store files into.
|
229
294
|
#
|
@@ -249,9 +314,10 @@ class Shrine
|
|
249
314
|
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
250
315
|
# [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method
|
251
316
|
# [configuring AWS SDK]: https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html
|
252
|
-
def initialize(bucket:, prefix: nil, host: nil, upload_options: {}, multipart_threshold: {}, signer: nil, public: nil, **s3_options)
|
317
|
+
def initialize(bucket:, client: nil, prefix: nil, host: nil, upload_options: {}, multipart_threshold: {}, signer: nil, public: nil, **s3_options)
|
318
|
+
raise ArgumentError, "the :bucket option is nil" unless bucket
|
319
|
+
|
253
320
|
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
|
254
|
-
resource = Aws::S3::Resource.new(**s3_options)
|
255
321
|
|
256
322
|
if multipart_threshold.is_a?(Integer)
|
257
323
|
Shrine.deprecation("Accepting the :multipart_threshold S3 option as an integer is deprecated, use a hash with :upload and :copy keys instead, e.g. {upload: 15*1024*1024, copy: 150*1024*1024}")
|
@@ -259,8 +325,8 @@ class Shrine
|
|
259
325
|
end
|
260
326
|
multipart_threshold = { upload: 15*1024*1024, copy: 100*1024*1024 }.merge(multipart_threshold)
|
261
327
|
|
262
|
-
@
|
263
|
-
@
|
328
|
+
@client = client || Aws::S3::Client.new(**s3_options)
|
329
|
+
@bucket = Aws::S3::Bucket.new(name: bucket, client: @client)
|
264
330
|
@prefix = prefix
|
265
331
|
@host = host
|
266
332
|
@upload_options = upload_options
|
@@ -287,7 +353,7 @@ class Shrine
|
|
287
353
|
|
288
354
|
options = {}
|
289
355
|
options[:content_type] = content_type if content_type
|
290
|
-
options[:content_disposition] =
|
356
|
+
options[:content_disposition] = ContentDisposition.inline(filename) if filename
|
291
357
|
options[:acl] = "public-read" if public
|
292
358
|
|
293
359
|
options.merge!(@upload_options)
|
@@ -303,22 +369,6 @@ class Shrine
|
|
303
369
|
end
|
304
370
|
end
|
305
371
|
|
306
|
-
# Downloads the file from S3 and returns a `Tempfile`. The download will
|
307
|
-
# be automatically retried up to 3 times. Any additional options are
|
308
|
-
# forwarded to [`Aws::S3::Object#get`].
|
309
|
-
#
|
310
|
-
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
311
|
-
def download(id, **options)
|
312
|
-
tempfile = Tempfile.new(["shrine-s3", File.extname(id)], binmode: true)
|
313
|
-
(object = object(id)).get(response_target: tempfile, **options)
|
314
|
-
tempfile.singleton_class.instance_eval { attr_accessor :content_type }
|
315
|
-
tempfile.content_type = object.content_type
|
316
|
-
tempfile.tap(&:open)
|
317
|
-
rescue
|
318
|
-
tempfile.close! if tempfile
|
319
|
-
raise
|
320
|
-
end
|
321
|
-
|
322
372
|
# Returns a `Down::ChunkedIO` object that downloads S3 object content
|
323
373
|
# on-demand. By default, read content will be cached onto disk so that
|
324
374
|
# it can be rewinded, but if you don't need that you can pass
|
@@ -329,13 +379,15 @@ class Shrine
|
|
329
379
|
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
330
380
|
def open(id, rewindable: true, **options)
|
331
381
|
object = object(id)
|
332
|
-
|
382
|
+
|
383
|
+
load_data(object, **options)
|
384
|
+
|
385
|
+
Down::ChunkedIO.new(
|
333
386
|
chunks: object.enum_for(:get, **options),
|
334
387
|
rewindable: rewindable,
|
388
|
+
size: object.content_length,
|
335
389
|
data: { object: object },
|
336
390
|
)
|
337
|
-
io.size = object.content_length
|
338
|
-
io
|
339
391
|
end
|
340
392
|
|
341
393
|
# Returns true file exists on S3.
|
@@ -383,11 +435,24 @@ class Shrine
|
|
383
435
|
url
|
384
436
|
end
|
385
437
|
|
386
|
-
# Returns URL, params and
|
387
|
-
#
|
388
|
-
#
|
389
|
-
#
|
390
|
-
#
|
438
|
+
# Returns URL, params, headers, and verb for direct uploads.
|
439
|
+
#
|
440
|
+
# s3.presign("key") #=>
|
441
|
+
# # {
|
442
|
+
# # url: "https://my-bucket.s3.amazonaws.com/...",
|
443
|
+
# # fields: { ... }, # blank for PUT presigns
|
444
|
+
# # headers: { ... }, # blank for POST presigns
|
445
|
+
# # method: "post",
|
446
|
+
# # }
|
447
|
+
#
|
448
|
+
# By default it calls [`Aws::S3::Object#presigned_post`] which generates
|
449
|
+
# data for a POST request, but you can also specify `method: :put` for
|
450
|
+
# PUT uploads which calls [`Aws::S3::Object#presigned_url`].
|
451
|
+
#
|
452
|
+
# s3.presign("key", method: :post) # for POST upload (default)
|
453
|
+
# s3.presign("key", method: :put) # for PUT upload
|
454
|
+
#
|
455
|
+
# Any additional options are forwarded to the underlying AWS SDK method.
|
391
456
|
#
|
392
457
|
# [`Aws::S3::Object#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
393
458
|
# [`Aws::S3::Object#presigned_url`]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_url-instance_method
|
@@ -447,12 +512,11 @@ class Shrine
|
|
447
512
|
bucket.object([*prefix, id].join("/"))
|
448
513
|
end
|
449
514
|
|
450
|
-
# Catches the deprecated `#stream`
|
451
|
-
def method_missing(name, *args)
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
object.get { |chunk| yield chunk, object.content_length }
|
515
|
+
# Catches the deprecated `#download` and `#stream` methods.
|
516
|
+
def method_missing(name, *args, &block)
|
517
|
+
case name
|
518
|
+
when :stream then deprecated_stream(*args, &block)
|
519
|
+
when :download then deprecated_download(*args, &block)
|
456
520
|
else
|
457
521
|
super
|
458
522
|
end
|
@@ -501,6 +565,23 @@ class Shrine
|
|
501
565
|
bytes_uploaded
|
502
566
|
end
|
503
567
|
|
568
|
+
# Aws::S3::Object#load doesn't support passing options to #head_object,
|
569
|
+
# so we call #head_object ourselves and assign the response data
|
570
|
+
def load_data(object, **options)
|
571
|
+
# filter out #get_object options that are not valid #head_object options
|
572
|
+
options = options.select do |key, value|
|
573
|
+
client.config.api.operation(:head_object).input.shape.member?(key)
|
574
|
+
end
|
575
|
+
|
576
|
+
response = client.head_object(
|
577
|
+
bucket: bucket.name,
|
578
|
+
key: object.key,
|
579
|
+
**options
|
580
|
+
)
|
581
|
+
|
582
|
+
object.instance_variable_set(:@data, response.data)
|
583
|
+
end
|
584
|
+
|
504
585
|
def extract_path(io)
|
505
586
|
if io.respond_to?(:path)
|
506
587
|
io.path
|
@@ -530,9 +611,37 @@ class Shrine
|
|
530
611
|
# should automatically URI-decode filenames when downloading.
|
531
612
|
def encode_content_disposition(content_disposition)
|
532
613
|
content_disposition.sub(/(?<=filename=").+(?=")/) do |filename|
|
533
|
-
|
614
|
+
if filename =~ /[^[:ascii:]]/
|
615
|
+
Shrine.deprecation("Shrine::Storage::S3 will not escape characters in the filename for Content-Disposition header in Shrine 3. Use the content_disposition gem, for example `ContentDisposition.format(disposition: 'inline', filename: '...')`.")
|
616
|
+
CGI.escape(filename).gsub("+", " ")
|
617
|
+
else
|
618
|
+
filename
|
619
|
+
end
|
534
620
|
end
|
535
621
|
end
|
622
|
+
|
623
|
+
def deprecated_stream(id)
|
624
|
+
Shrine.deprecation("Shrine::Storage::S3#stream is deprecated over calling #each_chunk on S3#open.")
|
625
|
+
object = object(id)
|
626
|
+
object.get { |chunk| yield chunk, object.content_length }
|
627
|
+
end
|
628
|
+
|
629
|
+
def deprecated_download(id, **options)
|
630
|
+
Shrine.deprecation("Shrine::Storage::S3#download is deprecated over S3#open.")
|
631
|
+
|
632
|
+
tempfile = Tempfile.new(["shrine-s3", File.extname(id)], binmode: true)
|
633
|
+
data = object(id).get(response_target: tempfile, **options)
|
634
|
+
tempfile.content_type = data.content_type
|
635
|
+
tempfile.tap(&:open)
|
636
|
+
rescue
|
637
|
+
tempfile.close! if tempfile
|
638
|
+
raise
|
639
|
+
end
|
640
|
+
|
641
|
+
# Tempfile with #content_type accessor which represents downloaded files.
|
642
|
+
class Tempfile < ::Tempfile
|
643
|
+
attr_accessor :content_type
|
644
|
+
end
|
536
645
|
end
|
537
646
|
end
|
538
647
|
end
|