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.
- checksums.yaml +4 -4
- data/README.md +101 -149
- data/doc/carrierwave.md +12 -16
- data/doc/changing_location.md +50 -0
- data/doc/creating_plugins.md +2 -2
- data/doc/creating_storages.md +70 -9
- data/doc/direct_s3.md +132 -61
- data/doc/migrating_storage.md +12 -10
- data/doc/paperclip.md +12 -17
- data/doc/refile.md +338 -0
- data/doc/regenerating_versions.md +75 -11
- data/doc/securing_uploads.md +172 -0
- data/lib/shrine.rb +21 -16
- data/lib/shrine/plugins/activerecord.rb +2 -2
- data/lib/shrine/plugins/background_helpers.rb +2 -148
- data/lib/shrine/plugins/backgrounding.rb +148 -0
- data/lib/shrine/plugins/backup.rb +88 -0
- data/lib/shrine/plugins/data_uri.rb +25 -4
- data/lib/shrine/plugins/default_url.rb +37 -0
- data/lib/shrine/plugins/delete_uploaded.rb +40 -0
- data/lib/shrine/plugins/determine_mime_type.rb +4 -2
- data/lib/shrine/plugins/direct_upload.rb +107 -62
- data/lib/shrine/plugins/download_endpoint.rb +157 -0
- data/lib/shrine/plugins/hooks.rb +19 -5
- data/lib/shrine/plugins/keep_location.rb +43 -0
- data/lib/shrine/plugins/moving.rb +11 -10
- data/lib/shrine/plugins/parallelize.rb +1 -5
- data/lib/shrine/plugins/parsed_json.rb +7 -1
- data/lib/shrine/plugins/pretty_location.rb +6 -0
- data/lib/shrine/plugins/rack_file.rb +7 -1
- data/lib/shrine/plugins/remove_invalid.rb +22 -0
- data/lib/shrine/plugins/sequel.rb +2 -2
- data/lib/shrine/plugins/upload_options.rb +41 -0
- data/lib/shrine/plugins/versions.rb +9 -7
- data/lib/shrine/storage/file_system.rb +46 -30
- data/lib/shrine/storage/linter.rb +48 -25
- data/lib/shrine/storage/s3.rb +89 -22
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +3 -3
- 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
|
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
|
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
|
-
#
|
71
|
-
#
|
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 "
|
9
|
+
# most commonly initialized with a "base" folder and a "prefix":
|
10
10
|
#
|
11
|
-
# storage = Shrine::Storage::FileSystem.new("public",
|
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
|
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
|
-
# ##
|
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
|
-
#
|
32
|
-
# storage.url("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
|
-
#
|
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, :
|
74
|
+
attr_reader :directory, :prefix, :host, :permissions
|
57
75
|
|
58
76
|
# Initializes a storage for uploading to the filesystem.
|
59
77
|
#
|
60
|
-
# :
|
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 `:
|
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,
|
77
|
-
if subdirectory
|
78
|
-
|
79
|
-
@
|
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))
|
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,
|
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 #
|
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
|
-
|
149
|
-
|
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
|
20
|
+
# fails, by default it raises a LintError, but you can also specify
|
21
|
+
# `action: :warn`.
|
21
22
|
class Linter
|
22
|
-
def self.call(
|
23
|
-
new(
|
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
|
-
@
|
29
|
+
@action = action
|
30
|
+
@errors = []
|
29
31
|
end
|
30
32
|
|
31
|
-
def call
|
32
|
-
|
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.
|
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
|
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
|
51
|
-
|
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(
|
83
|
+
storage.upload(io_factory.call, id = "foo.jpg")
|
68
84
|
storage.clear!(:confirm)
|
69
|
-
error! "
|
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
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -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,
|
8
|
-
#
|
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: "
|
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
|
89
|
+
# : This option is used for setting CDNs, e.g. it can be set to
|
90
|
+
# `//abc123.cloudfront.net`.
|
55
91
|
#
|
56
|
-
#
|
57
|
-
|
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
|
-
|
122
|
+
copy(io, id, **options)
|
74
123
|
else
|
75
|
-
|
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.
|
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
|
-
#
|
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
|
-
|
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
|