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.

Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +20 -16
  4. data/doc/creating_storages.md +0 -21
  5. data/doc/design.md +1 -0
  6. data/doc/direct_s3.md +26 -15
  7. data/doc/metadata.md +67 -22
  8. data/doc/multiple_files.md +3 -3
  9. data/doc/processing.md +1 -1
  10. data/doc/retrieving_uploads.md +184 -0
  11. data/lib/shrine.rb +268 -900
  12. data/lib/shrine/attacher.rb +271 -0
  13. data/lib/shrine/attachment.rb +97 -0
  14. data/lib/shrine/plugins.rb +29 -0
  15. data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
  16. data/lib/shrine/plugins/activerecord.rb +16 -14
  17. data/lib/shrine/plugins/add_metadata.rb +58 -24
  18. data/lib/shrine/plugins/backgrounding.rb +6 -1
  19. data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
  20. data/lib/shrine/plugins/copy.rb +12 -8
  21. data/lib/shrine/plugins/data_uri.rb +23 -20
  22. data/lib/shrine/plugins/default_url_options.rb +5 -4
  23. data/lib/shrine/plugins/determine_mime_type.rb +24 -23
  24. data/lib/shrine/plugins/download_endpoint.rb +61 -73
  25. data/lib/shrine/plugins/migration_helpers.rb +17 -17
  26. data/lib/shrine/plugins/module_include.rb +9 -8
  27. data/lib/shrine/plugins/presign_endpoint.rb +13 -7
  28. data/lib/shrine/plugins/processing.rb +1 -1
  29. data/lib/shrine/plugins/rack_response.rb +128 -36
  30. data/lib/shrine/plugins/refresh_metadata.rb +20 -5
  31. data/lib/shrine/plugins/remote_url.rb +8 -8
  32. data/lib/shrine/plugins/remove_attachment.rb +9 -9
  33. data/lib/shrine/plugins/sequel.rb +21 -18
  34. data/lib/shrine/plugins/tempfile.rb +68 -0
  35. data/lib/shrine/plugins/upload_endpoint.rb +3 -2
  36. data/lib/shrine/plugins/upload_options.rb +7 -6
  37. data/lib/shrine/plugins/validation_helpers.rb +2 -1
  38. data/lib/shrine/storage/file_system.rb +20 -17
  39. data/lib/shrine/storage/linter.rb +0 -7
  40. data/lib/shrine/storage/s3.rb +159 -50
  41. data/lib/shrine/uploaded_file.rb +258 -0
  42. data/lib/shrine/version.rb +1 -1
  43. data/shrine.gemspec +7 -19
  44. metadata +41 -21
@@ -70,33 +70,36 @@ class Shrine
70
70
 
71
71
  return unless model < ::Sequel::Model
72
72
 
73
- opts = shrine_class.opts
73
+ name = attachment_name
74
74
 
75
- module_eval <<-RUBY, __FILE__, __LINE__ + 1 if opts[:sequel_validations]
76
- def validate
77
- super
78
- #{@name}_attacher.errors.each do |message|
79
- errors.add(:#{@name}, *message)
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
- RUBY
82
+ end
83
83
 
84
- module_eval <<-RUBY, __FILE__, __LINE__ + 1 if opts[:sequel_callbacks]
85
- def before_save
86
- super
87
- #{@name}_attacher.save if #{@name}_attacher.changed?
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
- def after_save
91
- super
92
- db.after_commit{#{@name}_attacher.finalize} if #{@name}_attacher.changed?
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
- def after_destroy
96
- super
97
- db.after_commit{#{@name}_attacher.destroy} if #{@name}_attacher.read
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
- RUBY
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
- }.merge(options))
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] = (uploader.opts[:upload_options] || {}).merge(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] = (uploader.opts[:validation_default_messages] || {}).merge(opts[: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, *args, &block)
157
- path(id).open("rb", *args, &block)
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
- directory.find do |path|
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
- if name == :download
210
- begin
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)
@@ -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
- # ## Accelerate endpoint
160
+ # ## Presigns
157
161
  #
158
- # To use Amazon S3's [Transfer Acceleration] feature, you can change the
159
- # `:endpoint` of the underlying client to the accelerate endpoint, and this
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
- # Shrine::Storage::S3.new(endpoint: "https://s3-accelerate.amazonaws.com")
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
- # ## Presigns
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
- @bucket = resource.bucket(bucket) or fail(ArgumentError, "the :bucket option was nil")
263
- @client = resource.client
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] = "inline; filename=\"#{filename}\"" if filename
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
- io = Down::ChunkedIO.new(
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 headers for direct uploads. By default it
387
- # generates data for a POST request, calling [`Aws::S3::Object#presigned_post`].
388
- # You can also specify `method: :put` to generate data for a PUT request,
389
- # using [`Aws::S3::Object#presigned_url`]. Any additional options are
390
- # forwarded to the underlying AWS SDK method.
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` method.
451
- def method_missing(name, *args)
452
- if name == :stream
453
- Shrine.deprecation("Shrine::Storage::S3#stream is deprecated over calling #each_chunk on S3#open.")
454
- object = object(*args)
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
- CGI.escape(filename).gsub("+", " ")
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