shrine 1.0.0 → 1.1.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -149
  3. data/doc/carrierwave.md +12 -16
  4. data/doc/changing_location.md +50 -0
  5. data/doc/creating_plugins.md +2 -2
  6. data/doc/creating_storages.md +70 -9
  7. data/doc/direct_s3.md +132 -61
  8. data/doc/migrating_storage.md +12 -10
  9. data/doc/paperclip.md +12 -17
  10. data/doc/refile.md +338 -0
  11. data/doc/regenerating_versions.md +75 -11
  12. data/doc/securing_uploads.md +172 -0
  13. data/lib/shrine.rb +21 -16
  14. data/lib/shrine/plugins/activerecord.rb +2 -2
  15. data/lib/shrine/plugins/background_helpers.rb +2 -148
  16. data/lib/shrine/plugins/backgrounding.rb +148 -0
  17. data/lib/shrine/plugins/backup.rb +88 -0
  18. data/lib/shrine/plugins/data_uri.rb +25 -4
  19. data/lib/shrine/plugins/default_url.rb +37 -0
  20. data/lib/shrine/plugins/delete_uploaded.rb +40 -0
  21. data/lib/shrine/plugins/determine_mime_type.rb +4 -2
  22. data/lib/shrine/plugins/direct_upload.rb +107 -62
  23. data/lib/shrine/plugins/download_endpoint.rb +157 -0
  24. data/lib/shrine/plugins/hooks.rb +19 -5
  25. data/lib/shrine/plugins/keep_location.rb +43 -0
  26. data/lib/shrine/plugins/moving.rb +11 -10
  27. data/lib/shrine/plugins/parallelize.rb +1 -5
  28. data/lib/shrine/plugins/parsed_json.rb +7 -1
  29. data/lib/shrine/plugins/pretty_location.rb +6 -0
  30. data/lib/shrine/plugins/rack_file.rb +7 -1
  31. data/lib/shrine/plugins/remove_invalid.rb +22 -0
  32. data/lib/shrine/plugins/sequel.rb +2 -2
  33. data/lib/shrine/plugins/upload_options.rb +41 -0
  34. data/lib/shrine/plugins/versions.rb +9 -7
  35. data/lib/shrine/storage/file_system.rb +46 -30
  36. data/lib/shrine/storage/linter.rb +48 -25
  37. data/lib/shrine/storage/s3.rb +89 -22
  38. data/lib/shrine/version.rb +1 -1
  39. data/shrine.gemspec +3 -3
  40. metadata +16 -5
@@ -22,8 +22,8 @@ class Shrine
22
22
  # end
23
23
  # end
24
24
  #
25
- # Now when you access the attachment through the model, a hash of uploaded
26
- # files will be returned:
25
+ # Now when you access the stored attachment through the model, a hash of
26
+ # uploaded files will be returned:
27
27
  #
28
28
  # JSON.parse(user.avatar_data) #=>
29
29
  # # {
@@ -44,6 +44,9 @@ class Shrine
44
44
  # user.avatar[:medium].width #=> 500
45
45
  # user.avatar[:small].width #=> 300
46
46
  #
47
+ # You probably want to load the `delete_uploaded` plugin to automatically
48
+ # delete processed files after they have been uploaded.
49
+ #
47
50
  # The plugin also extends the `avatar_url` method to accept versions:
48
51
  #
49
52
  # user.avatar_url(:medium)
@@ -55,7 +58,7 @@ class Shrine
55
58
  # user.avatar #=> #<Shrine::UploadedFile>
56
59
  # user.avatar_url(:medium) #=> "http://example.com/original.jpg"
57
60
  #
58
- # # the versions have finished generating
61
+ # # the background job has finished generating versions
59
62
  #
60
63
  # user.avatar_url(:medium) #=> "http://example.com/medium.jpg"
61
64
  #
@@ -67,10 +70,8 @@ class Shrine
67
70
  # You can also easily generate default URLs for specific versions, since
68
71
  # the `context` will include the version name:
69
72
  #
70
- # class ImageUploader
71
- # def default_url(io, context)
72
- # "/images/defaults/#{context[:version]}.jpg"
73
- # end
73
+ # plugin :default_url do |context|
74
+ # "/images/defaults/#{context[:version]}.jpg"
74
75
  # end
75
76
  #
76
77
  # When deleting versions, any multi delete capabilities will be leveraged,
@@ -139,6 +140,7 @@ class Shrine
139
140
  # version.
140
141
  def _store(io, context)
141
142
  if (hash = io).is_a?(Hash)
143
+ raise Error, ":location is not applicable to versions" if context.key?(:location)
142
144
  self.class.versions!(hash).inject({}) do |result, (name, version)|
143
145
  result.update(name => _store(version, version: name, **context))
144
146
  end
@@ -6,14 +6,14 @@ require "pathname"
6
6
  class Shrine
7
7
  module Storage
8
8
  # The FileSystem storage handles uploads to the filesystem, and it is
9
- # most commonly initialized with a "base" folder and a "subdirectory":
9
+ # most commonly initialized with a "base" folder and a "prefix":
10
10
  #
11
- # storage = Shrine::Storage::FileSystem.new("public", subdirectory: "uploads")
11
+ # storage = Shrine::Storage::FileSystem.new("public", prefix: "uploads")
12
12
  # storage.url("image.jpg") #=> "/uploads/image.jpg"
13
13
  #
14
14
  # This storage will upload all files to "public/uploads", and the URLs
15
15
  # of the uploaded files will start with "/uploads/*". This way you can
16
- # use FileSystem for both cache and store, one having subdirectory
16
+ # use FileSystem for both cache and store, one having the prefix
17
17
  # "uploads/cache" and other "uploads/store".
18
18
  #
19
19
  # You can also initialize the storage just with the "base" directory, and
@@ -22,20 +22,23 @@ class Shrine
22
22
  # storage = Shrine::Storage::FileSystem.new(Dir.tmpdir)
23
23
  # storage.url("image.jpg") #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/image.jpg"
24
24
  #
25
- # ## CDN
25
+ # ## Host
26
26
  #
27
27
  # It's generally a good idea to serve your files via a CDN, so an
28
28
  # additional `:host` option can be provided:
29
29
  #
30
30
  # storage = Shrine::Storage::FileSystem.new("public",
31
- # subdirectory: "uploads", host: "//abc123.cloudfront.net")
32
- # storage.url("image.jpg") #=> "//abc123.cloudfront.net/uploads/image.jpg"
31
+ # prefix: "uploads", host: "http://abc123.cloudfront.net")
32
+ # storage.url("image.jpg") #=> "http://abc123.cloudfront.net/uploads/image.jpg"
33
33
  #
34
- # The `:host` option can also be used wihout `:subdirectory`, and is
34
+ # If you're not using a CDN, it's recommended that you still set `:host` to
35
+ # your application's domain (at least in production).
36
+ #
37
+ # The `:host` option can also be used wihout `:prefix`, and is
35
38
  # useful if you for example have files located on another server:
36
39
  #
37
- # storage = Shrine::Storage::FileSystem.new("files", host: "943.23.43.1")
38
- # storage.url("image.jpg") #=> "943.23.43.1/files/image.jpg"
40
+ # storage = Shrine::Storage::FileSystem.new("files", host: "http://943.23.43.1")
41
+ # storage.url("image.jpg") #=> "http://943.23.43.1/files/image.jpg"
39
42
  #
40
43
  # ## Clearing cache
41
44
  #
@@ -52,17 +55,32 @@ class Shrine
52
55
  # pass the `:permissions` option:
53
56
  #
54
57
  # Shrine::Storage::FileSystem.new("directory", permissions: 0755)
58
+ #
59
+ # ## Heroku
60
+ #
61
+ # Note that Heroku has a read-only filesystem, and doesn't allow you to
62
+ # upload your files to the "public" directory, you can however upload to
63
+ # "tmp" directory:
64
+ #
65
+ # Shrine::Storage::FileSystem.new("tmp/uploads")
66
+ #
67
+ # Note that this approach has a couple of downsides. For example, you can
68
+ # only use it for cache, since Heroku wipes this directory between app
69
+ # restarts. This also means that deploying the app can cancel someone's
70
+ # uploading if you're using backgrounding. Also, by default you cannot
71
+ # generate URLs to files in the "tmp" directory, but you can with the
72
+ # download_endpoint plugin.
55
73
  class FileSystem
56
- attr_reader :directory, :subdirectory, :host, :permissions
74
+ attr_reader :directory, :prefix, :host, :permissions
57
75
 
58
76
  # Initializes a storage for uploading to the filesystem.
59
77
  #
60
- # :subdirectory
78
+ # :prefix
61
79
  # : The directory relative to `directory` to which files will be stored,
62
80
  # and it is included in the URL.
63
81
  #
64
82
  # :host
65
- # : URLs will by default be relative if `:subdirectory` is set, and you
83
+ # : URLs will by default be relative if `:prefix` is set, and you
66
84
  # can use this option to set a CDN host (e.g. `//abc123.cloudfront.net`).
67
85
  #
68
86
  # :permissions
@@ -73,10 +91,11 @@ class Shrine
73
91
  # : By default empty folders inside the directory are automatically
74
92
  # deleted, but if it happens that it causes too much load on the
75
93
  # filesystem, you can set this option to `false`.
76
- def initialize(directory, subdirectory: nil, host: nil, clean: true, permissions: nil)
77
- if subdirectory
78
- @subdirectory = Pathname(relative(subdirectory))
79
- @directory = Pathname(directory).join(@subdirectory)
94
+ def initialize(directory, prefix: nil, host: nil, clean: true, permissions: nil, subdirectory: nil)
95
+ if prefix || subdirectory
96
+ warn "The :subdirectory option is deprecated and will be removed in Shrine 2. You should use :prefix instead." if subdirectory
97
+ @prefix = Pathname(relative(prefix || subdirectory))
98
+ @directory = Pathname(directory).join(@prefix)
80
99
  else
81
100
  @directory = Pathname(directory)
82
101
  end
@@ -91,13 +110,13 @@ class Shrine
91
110
 
92
111
  # Copies the file into the given location.
93
112
  def upload(io, id, metadata = {})
94
- IO.copy_stream(io, path!(id)); io.rewind
113
+ IO.copy_stream(io, path!(id))
95
114
  path(id).chmod(permissions) if permissions
96
115
  end
97
116
 
98
117
  # Downloads the file from the given location, and returns a `Tempfile`.
99
118
  def download(id)
100
- Down.copy_to_tempfile(id, open(id))
119
+ open(id) { |file| Down.copy_to_tempfile(id, file) }
101
120
  end
102
121
 
103
122
  # Moves the file to the given location. This gets called by the "moving"
@@ -120,8 +139,8 @@ class Shrine
120
139
  end
121
140
 
122
141
  # Opens the file on the given location in read mode.
123
- def open(id)
124
- path(id).open("rb")
142
+ def open(id, &block)
143
+ path(id).open("rb", &block)
125
144
  end
126
145
 
127
146
  # Returns the contents of the file as a String.
@@ -141,19 +160,12 @@ class Shrine
141
160
  clean(id) if clean?
142
161
  end
143
162
 
144
- # If #subdirectory is present, returns the path relative to #directory,
163
+ # If #prefix is present, returns the path relative to #directory,
145
164
  # with an optional #host in front. Otherwise returns the full path to the
146
165
  # file (also with an optional #host).
147
166
  def url(id, **options)
148
- if subdirectory
149
- File.join(host || "", subdirectory, id)
150
- else
151
- if host
152
- File.join(host, path(id))
153
- else
154
- path(id).to_s
155
- end
156
- end
167
+ path = (prefix ? relative_path(id) : path(id)).to_s
168
+ host ? host + path : path
157
169
  end
158
170
 
159
171
  # Without any options it deletes all files from the #directory (and this
@@ -202,6 +214,10 @@ class Shrine
202
214
  path(id)
203
215
  end
204
216
 
217
+ def relative_path(id)
218
+ "/" + prefix.join(id.gsub("/", File::SEPARATOR)).to_s
219
+ end
220
+
205
221
  def relative(path)
206
222
  path.sub(%r{^/}, "")
207
223
  end
@@ -17,62 +17,84 @@ class Shrine
17
17
 
18
18
  module Storage
19
19
  # Checks if the storage conforms to Shrine's specification. If the check
20
- # fails a LintError is raised.
20
+ # fails, by default it raises a LintError, but you can also specify
21
+ # `action: :warn`.
21
22
  class Linter
22
- def self.call(storage)
23
- new(storage).call
23
+ def self.call(*args)
24
+ new(*args).call
24
25
  end
25
26
 
26
- def initialize(storage)
27
+ def initialize(storage, action: :error)
27
28
  @storage = storage
28
- @errors = []
29
+ @action = action
30
+ @errors = []
29
31
  end
30
32
 
31
- def call
32
- fakeio = FakeIO.new("image")
33
+ def call(io_factory = ->{FakeIO.new("image")})
34
+ storage.upload(io_factory.call, id = "foo.jpg", {"mime_type" => "image/jpeg"})
33
35
 
34
- storage.upload(fakeio, "foo.jpg", {"mime_type" => "image/jpeg"})
35
- error! "#upload doesn't rewind the file" if !(fakeio.read == "image")
36
-
37
- file = storage.download("foo.jpg")
36
+ file = storage.download(id)
38
37
  error! "#download doesn't return a Tempfile" if !file.is_a?(Tempfile)
39
- error! "#download doesn't return the uploaded file" if !(file.read == "image")
38
+ error! "#download returns an empty file" if file.read.empty?
39
+
40
+ error! "#open doesn't return a valid IO object" if !io?(storage.open(id))
41
+ error! "#read returns an empty string" if storage.read(id).empty?
42
+ error! "#exists? returns false for a file that was uploaded" if !storage.exists?(id)
43
+ error! "#url doesn't return a string" if !storage.url(id, {}).is_a?(String)
44
+
45
+ if storage.respond_to?(:stream)
46
+ content = ""
47
+ storage.stream(id) { |chunk| content << chunk }
48
+ error! "#stream does not yield any chunks" if content.empty?
49
+ end
50
+
51
+ storage.delete(id)
52
+ error! "#exists? returns true for a file that was deleted" if storage.exists?(id)
40
53
 
41
54
  if storage.respond_to?(:move)
42
55
  if storage.respond_to?(:movable?)
43
56
  error! "#movable? doesn't accept 2 arguments" if !(storage.method(:movable?).arity == 2)
44
57
  error! "#move doesn't accept 3 arguments" if !(storage.method(:move).arity == -3)
58
+
59
+ uploaded_file = uploader.upload(io_factory.call, location: "bar.jpg")
60
+
61
+ if storage.movable?(uploaded_file, "quux.jpg")
62
+ storage.move(uploaded_file, id = "quux.jpg")
63
+ error! "#exists? returns false for destination after #move" if !storage.exists?(id)
64
+ error! "#exists? returns true for source after #move" if storage.exists?(uploaded_file.id)
65
+ end
45
66
  else
46
- error! "doesn't respond to #movable?" if !storage.respond_to?(:movable?)
67
+ error! "responds to #move but doesn't respond to #movable?" if !storage.respond_to?(:movable?)
47
68
  end
48
69
  end
49
70
 
50
- if !io?(storage.open("foo.jpg"))
51
- error! "#open doesn't return a valid IO object"
71
+ if storage.respond_to?(:multi_delete)
72
+ storage.upload(io_factory.call, id = "foo.jpg")
73
+ storage.multi_delete([id])
74
+ error! "#exists? returns true for a file that was multi-deleted" if storage.exists?(id)
52
75
  end
53
76
 
54
- error! "#read doesn't return content of the uploaded file" if !(storage.read("foo.jpg") == "image")
55
- error! "#exists? returns false for a file that was uploaded" if !storage.exists?("foo.jpg")
56
- error! "#url doesn't return a string" if !storage.url("foo.jpg", {}).is_a?(String)
57
-
58
- storage.delete("foo.jpg")
59
- error! "#exists? returns true for a file that was deleted" if storage.exists?("foo.jpg")
60
-
61
77
  begin
62
78
  storage.clear!
63
79
  error! "#clear! should raise Shrine::Confirm unless :confirm is passed in"
64
80
  rescue Shrine::Confirm
65
81
  end
66
82
 
67
- storage.upload(FakeIO.new("image"), "foo.jpg", {"mime_type" => "image/jpeg"})
83
+ storage.upload(io_factory.call, id = "foo.jpg")
68
84
  storage.clear!(:confirm)
69
- error! "a file still #exists? after #clear! was called" if storage.exists?("foo.jpg")
85
+ error! "file still #exists? after #clear! was called" if storage.exists?(id)
70
86
 
71
- raise LintError.new(@errors) if @errors.any?
87
+ raise LintError.new(@errors) if @errors.any? && @action == :error
72
88
  end
73
89
 
74
90
  private
75
91
 
92
+ def uploader
93
+ shrine = Class.new(Shrine)
94
+ shrine.storages[:storage] = storage
95
+ shrine.new(:storage)
96
+ end
97
+
76
98
  def io?(object)
77
99
  missing_methods = IO_METHODS.reject do |m, a|
78
100
  object.respond_to?(m) && [a.count, -1].include?(object.method(m).arity)
@@ -82,6 +104,7 @@ class Shrine
82
104
 
83
105
  def error!(message)
84
106
  @errors << message
107
+ warn(message) if @action.to_s.start_with?("warn")
85
108
  end
86
109
 
87
110
  attr_reader :storage
@@ -4,8 +4,12 @@ require "uri"
4
4
 
5
5
  class Shrine
6
6
  module Storage
7
- # The S3 storage handles uploads to Amazon S3 service, and it is
8
- # initialized with the following 4 required options:
7
+ # The S3 storage handles uploads to Amazon S3 service, it depends on the
8
+ # [aws-sdk] gem:
9
+ #
10
+ # gem "aws-sdk", "~> 2.1"
11
+ #
12
+ # It is initialized with the following 4 required options:
9
13
  #
10
14
  # Shrine::Storage::S3.new(
11
15
  # access_key_id: "xyz",
@@ -23,12 +27,38 @@ class Shrine
23
27
  # Shrine::Storage::S3.new(prefix: "cache", **s3_options)
24
28
  # Shrine::Storage::S3.new(prefix: "store", **s3_options)
25
29
  #
30
+ # ## Upload options
31
+ #
32
+ # Sometimes you'll want to add additional upload options to all S3 uploads.
33
+ # You can do that by passing the `:upload` option:
34
+ #
35
+ # Shrine::Storage::S3.new(
36
+ # upload_options: {acl: "public-read", cache_control: "public, max-age=3600"},
37
+ # **s3_options
38
+ # )
39
+ #
40
+ # These options will be passed to aws-sdk's methods for [uploading],
41
+ # [copying] and [presigning].
42
+ #
43
+ # You can also forward additional upload options per upload with the
44
+ # `upload_options` plugin:
45
+ #
46
+ # class MyUploader < Shrine
47
+ # plugin :upload_options, store: ->(io, context) do
48
+ # if context[:version] == :thumb
49
+ # {acl: "public-read"}
50
+ # else
51
+ # {acl: "private"}
52
+ # end
53
+ # end
54
+ # end
55
+ #
26
56
  # ## CDN
27
57
  #
28
58
  # If you're using a CDN with S3 like Amazon CloudFront, you can specify
29
59
  # the `:host` option to have all your URLs use the CDN host:
30
60
  #
31
- # Shrine::Storage::S3.new(host: "//abc123.cloudfront.net", **s3_options)
61
+ # Shrine::Storage::S3.new(host: "http://abc123.cloudfront.net", **s3_options)
32
62
  #
33
63
  # ## Clearing cache
34
64
  #
@@ -36,8 +66,13 @@ class Shrine
36
66
  # delete old files which aren't used anymore. S3 has a built-in way to do
37
67
  # this, read [this article](http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html)
38
68
  # for instructions.
69
+ #
70
+ # [uploading]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#put-instance_method
71
+ # [copying]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#copy_from-instance_method
72
+ # [presigning]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_post-instance_method
73
+ # [aws-sdk]: https://github.com/aws/aws-sdk-ruby
39
74
  class S3
40
- attr_reader :prefix, :bucket, :s3, :host
75
+ attr_reader :prefix, :bucket, :s3, :host, :upload_options
41
76
 
42
77
  # Initializes a storage for uploading to S3.
43
78
  #
@@ -51,14 +86,26 @@ class Shrine
51
86
  # : "Folder" name inside the bucket to store files into.
52
87
  #
53
88
  # :host
54
- # : This option is used for setting CDNs, e.g. it can be set to `//abc123.cloudfront.net`.
89
+ # : This option is used for setting CDNs, e.g. it can be set to
90
+ # `//abc123.cloudfront.net`.
55
91
  #
56
- # All other options are forwarded to [`Aws::S3::Client#initialize`](http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#initialize-instance_method).
57
- def initialize(bucket:, prefix: nil, host: nil, **s3_options)
92
+ # :upload_options
93
+ # : Additional options that will be used for uploading files, they will
94
+ # be passed to [`Aws::S3::Object#put`], [`Aws::S3::Object#copy_from`]
95
+ # and [`Aws::S3::Bucket#presigned_post`].
96
+ #
97
+ # All other options are forwarded to [`Aws::S3::Client#initialize`].
98
+ #
99
+ # [`Aws::S3::Object#put`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#put-instance_method
100
+ # [`Aws::S3::Object#copy_from`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#copy_from-instance_method
101
+ # [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_post-instance_method
102
+ # [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#initialize-instance_method
103
+ def initialize(bucket:, prefix: nil, host: nil, upload_options: {}, **s3_options)
58
104
  @prefix = prefix
59
105
  @s3 = Aws::S3::Resource.new(**s3_options)
60
106
  @bucket = @s3.bucket(bucket)
61
107
  @host = host
108
+ @upload_options = upload_options
62
109
  end
63
110
 
64
111
  # If the file is an UploadedFile from S3, issues a COPY command, otherwise
@@ -68,12 +115,13 @@ class Shrine
68
115
  # by default S3 sets everything to "application/octet-stream".
69
116
  def upload(io, id, metadata = {})
70
117
  options = {content_type: metadata["mime_type"]}
118
+ options.update(upload_options)
119
+ options.update(metadata.delete("s3") || {})
71
120
 
72
121
  if copyable?(io)
73
- object(id).copy_from(io.storage.object(io.id), **options)
122
+ copy(io, id, **options)
74
123
  else
75
- object(id).put(body: io, **options)
76
- io.rewind
124
+ put(io, id, **options)
77
125
  end
78
126
  end
79
127
 
@@ -82,6 +130,11 @@ class Shrine
82
130
  Down.download(url(id))
83
131
  end
84
132
 
133
+ # Streams the object from S3, yielding downloaded chunks.
134
+ def stream(id)
135
+ object(id).get { |chunk| yield chunk }
136
+ end
137
+
85
138
  # Alias for #download.
86
139
  def open(id)
87
140
  download(id)
@@ -89,7 +142,7 @@ class Shrine
89
142
 
90
143
  # Returns the contents of the file as a String.
91
144
  def read(id)
92
- object(id).get.body.read
145
+ object(id).get.body.string
93
146
  end
94
147
 
95
148
  # Returns true file exists on S3.
@@ -102,11 +155,14 @@ class Shrine
102
155
  object(id).delete
103
156
  end
104
157
 
105
- # This is called when multiple files are being deleted at once. Issues
106
- # a single MULTI DELETE command.
158
+ # This is called when multiple files are being deleted at once. Issues a
159
+ # single MULTI DELETE command for each 1000 objects (S3 delete limit).
107
160
  def multi_delete(ids)
108
- objects = ids.map { |id| {key: object(id).key} }
161
+ objects = ids.take(1000).map { |id| {key: object(id).key} }
109
162
  bucket.delete_objects(delete: {objects: objects})
163
+
164
+ rest = Array(ids[1000..-1])
165
+ multi_delete(rest) unless rest.empty?
110
166
  end
111
167
 
112
168
  # Returns the presigned URL to the file.
@@ -120,13 +176,7 @@ class Shrine
120
176
  # : Creates an unsigned version of the URL (requires setting appropriate
121
177
  # permissions on the S3 bucket).
122
178
  #
123
- # options
124
- # : All other optione are forwarded to
125
- # If `download: true` is passed,
126
- # returns a forced download link. If `public: true` is passed, it returns
127
- # an unsigned S3 URL. All other options are forwarded to
128
- # [`Aws::S3::Object#presigned_url`], so take a look there for the
129
- # complete list of additional options.
179
+ # All other options are forwarded to [`Aws::S3::Object#presigned_url`].
130
180
  #
131
181
  # [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_url-instance_method
132
182
  def url(id, download: nil, public: nil, **options)
@@ -154,7 +204,8 @@ class Shrine
154
204
  #
155
205
  # [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Bucket.html#presigned_post-instance_method
156
206
  def presign(id, **options)
157
- @bucket.presigned_post(key: object(id).key, **options)
207
+ options = upload_options.merge(options)
208
+ object(id).presigned_post(options)
158
209
  end
159
210
 
160
211
  protected
@@ -171,12 +222,28 @@ class Shrine
171
222
 
172
223
  private
173
224
 
225
+ # Copies an existing S3 object to a new location.
226
+ def copy(io, id, **options)
227
+ options.update(multipart_copy: true) if large?(io)
228
+ object(id).copy_from(io.storage.object(io.id), **options)
229
+ end
230
+
231
+ # Uploads the file to S3.
232
+ def put(io, id, **options)
233
+ object(id).put(body: io, **options)
234
+ end
235
+
174
236
  # The file is copyable if it's on S3 and on the same Amazon account.
175
237
  def copyable?(io)
176
238
  io.respond_to?(:storage) &&
177
239
  io.storage.is_a?(Storage::S3) &&
178
240
  io.storage.access_key_id == access_key_id
179
241
  end
242
+
243
+ # Amazon requires multipart copy from S3 objects larger than 5 GB.
244
+ def large?(io)
245
+ io.size >= 5*1024*1024*1024 # 5GB
246
+ end
180
247
  end
181
248
  end
182
249
  end