shrine 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +66 -27
- data/doc/attacher.md +8 -0
- data/doc/carrierwave.md +2 -2
- data/doc/creating_storages.md +3 -2
- data/doc/direct_s3.md +71 -56
- data/doc/multiple_files.md +3 -2
- data/doc/refile.md +18 -15
- data/doc/regenerating_versions.md +1 -1
- data/doc/securing_uploads.md +8 -8
- data/doc/testing.md +27 -21
- data/lib/shrine.rb +35 -24
- data/lib/shrine/plugins/activerecord.rb +22 -3
- data/lib/shrine/plugins/copy.rb +1 -1
- data/lib/shrine/plugins/data_uri.rb +3 -3
- data/lib/shrine/plugins/determine_mime_type.rb +24 -10
- data/lib/shrine/plugins/direct_upload.rb +4 -1
- data/lib/shrine/plugins/download_endpoint.rb +126 -63
- data/lib/shrine/plugins/keep_files.rb +1 -1
- data/lib/shrine/plugins/logging.rb +1 -0
- data/lib/shrine/plugins/metadata_attributes.rb +1 -1
- data/lib/shrine/plugins/presign_endpoint.rb +258 -0
- data/lib/shrine/plugins/rack_file.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +85 -0
- data/lib/shrine/plugins/remote_url.rb +5 -7
- data/lib/shrine/plugins/sequel.rb +1 -1
- data/lib/shrine/plugins/signature.rb +1 -1
- data/lib/shrine/plugins/upload_endpoint.rb +238 -0
- data/lib/shrine/storage/file_system.rb +3 -2
- data/lib/shrine/storage/s3.rb +62 -54
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +3 -4
- metadata +22 -33
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
|
|
3
|
+
class Shrine
|
|
4
|
+
module Plugins
|
|
5
|
+
# The `rack_response` plugin allows you to convert an `UploadedFile` object
|
|
6
|
+
# into a triple consisting of status, headers, and body, suitable for
|
|
7
|
+
# returning as a response in a Rack-based application.
|
|
8
|
+
#
|
|
9
|
+
# plugin :rack_response
|
|
10
|
+
#
|
|
11
|
+
# To convert a `Shrine::UploadedFile` into a Rack response, simply call
|
|
12
|
+
# `#to_rack_response`:
|
|
13
|
+
#
|
|
14
|
+
# status, headers, body = uploaded_file.to_rack_response
|
|
15
|
+
# status #=> 200
|
|
16
|
+
# headers #=> {"Content-Length" => "100", "Content-Type" => "text/plain", "Content-Disposition" => "inline; filename=\"file.txt\""}
|
|
17
|
+
# body # object that responds to #each and #close
|
|
18
|
+
#
|
|
19
|
+
# An example how this can be used in a Rails controller:
|
|
20
|
+
#
|
|
21
|
+
# class FilesController < ActionController::Base
|
|
22
|
+
# def download
|
|
23
|
+
# # ...
|
|
24
|
+
# file_response = record.attachment.to_rack_response
|
|
25
|
+
#
|
|
26
|
+
# response.status = file_response[0]
|
|
27
|
+
# response.headers.merge!(file_response[1])
|
|
28
|
+
# self.response_body = file_response[2]
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# By default the "Content-Disposition" header will use the `inline`
|
|
33
|
+
# disposition, but you can change it to `attachment` if you don't want the
|
|
34
|
+
# file to be rendered inside the browser:
|
|
35
|
+
#
|
|
36
|
+
# status, headers, body = uploaded_file.to_rack_response(disposition: "attachment")
|
|
37
|
+
# headers["Content-Disposition"] #=> "attachment; filename=\"file.txt\""
|
|
38
|
+
module RackResponse
|
|
39
|
+
module FileMethods
|
|
40
|
+
# Returns a Rack response triple for the uploaded file.
|
|
41
|
+
def to_rack_response(disposition: "inline")
|
|
42
|
+
status = 200
|
|
43
|
+
headers = rack_headers(disposition: disposition)
|
|
44
|
+
body = rack_body
|
|
45
|
+
|
|
46
|
+
[status, headers, body]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Returns a hash of "Content-Length", "Content-Type", and
|
|
52
|
+
# "Content-Disposition" headers, whose values are extracted from
|
|
53
|
+
# metadata.
|
|
54
|
+
def rack_headers(disposition:)
|
|
55
|
+
length = size || io.size
|
|
56
|
+
type = mime_type || Rack::Mime.mime_type(".#{extension}")
|
|
57
|
+
filename = original_filename || id.split("/").last
|
|
58
|
+
|
|
59
|
+
headers = {}
|
|
60
|
+
headers["Content-Length"] = length.to_s if length
|
|
61
|
+
headers["Content-Type"] = type
|
|
62
|
+
headers["Content-Disposition"] = "#{disposition}; filename=\"#{filename}\""
|
|
63
|
+
|
|
64
|
+
headers
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns an object that responds to #each and #close, which yields
|
|
68
|
+
# contents of the file.
|
|
69
|
+
def rack_body
|
|
70
|
+
chunks = Enumerator.new do |yielder|
|
|
71
|
+
if io.respond_to?(:each_chunk) # Down::ChunkedIO
|
|
72
|
+
io.each_chunk { |chunk| yielder << chunk }
|
|
73
|
+
else
|
|
74
|
+
yielder << io.read(16*1024) until io.eof?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Rack::BodyProxy.new(chunks) { io.close }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
register_plugin(:rack_response, RackResponse)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "down"
|
|
2
|
+
|
|
1
3
|
class Shrine
|
|
2
4
|
module Plugins
|
|
3
5
|
# The `remote_url` plugin allows you to attach files from a remote location.
|
|
@@ -42,15 +44,12 @@ class Shrine
|
|
|
42
44
|
#
|
|
43
45
|
# If you want to customize how the file is downloaded, you can override the
|
|
44
46
|
# `:downloader` parameter and provide your own implementation. For example,
|
|
45
|
-
# you can
|
|
46
|
-
# passing it to Down:
|
|
47
|
+
# you can use the HTTP.rb Down backend for downloading:
|
|
47
48
|
#
|
|
48
|
-
# require "down"
|
|
49
|
-
# require "addressable/uri"
|
|
49
|
+
# require "down/http"
|
|
50
50
|
#
|
|
51
51
|
# plugin :remote_url, max_size: 20*1024*1024, downloader: ->(url, max_size:) do
|
|
52
|
-
#
|
|
53
|
-
# Down.download(url, max_size: max_size, max_redirects: 4, read_timeout: 3)
|
|
52
|
+
# Down::Http.download(url, max_size: max_size, follow: { max_hops: 4 }, timeout: { read: 3 })
|
|
54
53
|
# end
|
|
55
54
|
#
|
|
56
55
|
# ## Errors
|
|
@@ -138,7 +137,6 @@ class Shrine
|
|
|
138
137
|
# We silence any download errors, because for the user's point of view
|
|
139
138
|
# the download simply failed.
|
|
140
139
|
def download_with_open_uri(url, max_size:)
|
|
141
|
-
require "down"
|
|
142
140
|
Down.download(url, max_size: max_size)
|
|
143
141
|
end
|
|
144
142
|
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
class Shrine
|
|
6
|
+
module Plugins
|
|
7
|
+
# The `upload_endpoint` plugin provides a Rack endpoint which accepts file
|
|
8
|
+
# uploads and forwards them to specified storage. It can be used with
|
|
9
|
+
# client-side file upload libraries like [FineUploader], [Dropzone] or
|
|
10
|
+
# [jQuery-File-Upload] for asynchronous uploads.
|
|
11
|
+
#
|
|
12
|
+
# plugin :upload_endpoint
|
|
13
|
+
#
|
|
14
|
+
# The plugin adds a `Shrine.upload_endpoint` method which accepts a storage
|
|
15
|
+
# identifier and returns a Rack application that accepts multipart POST
|
|
16
|
+
# requests, and uploads received files to the specified storage.
|
|
17
|
+
#
|
|
18
|
+
# Shrine.upload_endpoint(:cache) # rack app
|
|
19
|
+
#
|
|
20
|
+
# Asynchronous upload is typically meant to replace the caching phase in
|
|
21
|
+
# the default synchronous workflow, so we want the uploads to go to
|
|
22
|
+
# temporary (`:cache`) storage.
|
|
23
|
+
#
|
|
24
|
+
# When we want to mount the Rack application to our app, it's recommended
|
|
25
|
+
# to generate endpoints for specific uploaders. This is because different
|
|
26
|
+
# uploaders may have different uploading logic, and this also allows
|
|
27
|
+
# customizing the upload endpoint per uploader.
|
|
28
|
+
#
|
|
29
|
+
# Rails.application.routes.draw do
|
|
30
|
+
# mount ImageUploader.upload_endpoint(:cache) => "/images/upload"
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# The above will create a `POST /images/upload` endpoint, which uploads the
|
|
34
|
+
# file received in the `file` param using `ImageUploader`, and returns a
|
|
35
|
+
# JSON representation of the uploaded file.
|
|
36
|
+
#
|
|
37
|
+
# # POST /images/upload
|
|
38
|
+
# {
|
|
39
|
+
# "id": "43kewit94.jpg",
|
|
40
|
+
# "storage": "cache",
|
|
41
|
+
# "metadata": {
|
|
42
|
+
# "size": 384393,
|
|
43
|
+
# "filename": "nature.jpg",
|
|
44
|
+
# "mime_type": "image/jpeg"
|
|
45
|
+
# }
|
|
46
|
+
# }
|
|
47
|
+
#
|
|
48
|
+
# This JSON string can now be assigned to an attachment attribute instead
|
|
49
|
+
# of a raw file. In a form it can be written to a hidden attachment field,
|
|
50
|
+
# and then it can be assigned as the attachment.
|
|
51
|
+
#
|
|
52
|
+
# ## Limiting filesize
|
|
53
|
+
#
|
|
54
|
+
# It's good practice to limit the accepted filesize of uploaded files. You
|
|
55
|
+
# can do that with the `:max_size` option:
|
|
56
|
+
#
|
|
57
|
+
# plugin :upload_endpoint, max_size: 20*1024*1024 # 20 MB
|
|
58
|
+
#
|
|
59
|
+
# If the uploaded file is larger than the specified value, a `413 Payload
|
|
60
|
+
# Too Large` response will be returned.
|
|
61
|
+
#
|
|
62
|
+
# ## Context
|
|
63
|
+
#
|
|
64
|
+
# The upload context will *not* contain `:record` and `:name` values, as
|
|
65
|
+
# the upload happens independently of a database record. The endpoint will
|
|
66
|
+
# sent the following upload context:
|
|
67
|
+
#
|
|
68
|
+
# * `:action` - holds the value `:upload`
|
|
69
|
+
# * `:request` - holds an instance of `Rack::Request`
|
|
70
|
+
#
|
|
71
|
+
# You can update the upload context via `:upload_context`:
|
|
72
|
+
#
|
|
73
|
+
# plugin :upload_endpoint, upload_context: -> (request) do
|
|
74
|
+
# { location: "my-location" }
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# ## Upload
|
|
78
|
+
#
|
|
79
|
+
# You can also customize the upload itself via the `:upload` option:
|
|
80
|
+
#
|
|
81
|
+
# plugin :upload_endpoint, upload: -> (io, context, request) do
|
|
82
|
+
# # perform uploading and return the Shrine::UploadedFile
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# ## Response
|
|
86
|
+
#
|
|
87
|
+
# The response returned by the endpoint can be customized via the
|
|
88
|
+
# `:rack_response` option:
|
|
89
|
+
#
|
|
90
|
+
# plugin :upload_endpoint, rack_response: -> (uploaded_file, request) do
|
|
91
|
+
# body = { data: uploaded_file.data, url: uploaded_file.url }.to_json
|
|
92
|
+
# [201, { "Content-Type" => "application/json" }, [body]]
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# ## Ad-hoc options
|
|
96
|
+
#
|
|
97
|
+
# You can override any of the options above when creating the endpoint:
|
|
98
|
+
#
|
|
99
|
+
# Shrine.upload_endpoint(:cache, max_size: 20*1024*1024)
|
|
100
|
+
#
|
|
101
|
+
# [FineUploader]: https://github.com/FineUploader/fine-uploader
|
|
102
|
+
# [Dropzone]: https://github.com/enyo/dropzone
|
|
103
|
+
# [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
|
|
104
|
+
module UploadEndpoint
|
|
105
|
+
def self.load_dependencies(uploader, opts = {})
|
|
106
|
+
uploader.plugin :rack_file
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.configure(uploader, opts = {})
|
|
110
|
+
uploader.opts[:upload_endpoint_max_size] = opts.fetch(:max_size, uploader.opts[:upload_endpoint_max_size])
|
|
111
|
+
uploader.opts[:upload_endpoint_upload_context] = opts.fetch(:upload_context, uploader.opts[:upload_endpoint_upload_context])
|
|
112
|
+
uploader.opts[:upload_endpoint_upload] = opts.fetch(:upload, uploader.opts[:upload_endpoint_upload])
|
|
113
|
+
uploader.opts[:upload_endpoint_rack_response] = opts.fetch(:rack_response, uploader.opts[:upload_endpoint_rack_response])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
module ClassMethods
|
|
117
|
+
# Returns a Rack application (object that responds to `#call`) which
|
|
118
|
+
# accepts multipart POST requests to the root URL, uploads given file
|
|
119
|
+
# to the specified storage, and returns that information in JSON format.
|
|
120
|
+
#
|
|
121
|
+
# The `storage_key` needs to be one of the registered Shrine storages.
|
|
122
|
+
# Additional options can be given to override the options given on
|
|
123
|
+
# plugin initialization.
|
|
124
|
+
def upload_endpoint(storage_key, **options)
|
|
125
|
+
App.new({
|
|
126
|
+
shrine_class: self,
|
|
127
|
+
storage_key: storage_key,
|
|
128
|
+
max_size: opts[:upload_endpoint_max_size],
|
|
129
|
+
upload_context: opts[:upload_endpoint_upload_context],
|
|
130
|
+
upload: opts[:upload_endpoint_upload],
|
|
131
|
+
rack_response: opts[:upload_endpoint_rack_response],
|
|
132
|
+
}.merge(options))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Rack application that accepts multipart POSt request to the root URL,
|
|
137
|
+
# calls `#upload` with the uploaded file, and returns the uploaded file
|
|
138
|
+
# information in JSON format.
|
|
139
|
+
class App
|
|
140
|
+
# Writes given options to instance variables.
|
|
141
|
+
def initialize(options)
|
|
142
|
+
options.each do |name, value|
|
|
143
|
+
instance_variable_set("@#{name}", value)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Accepts a Rack env hash, routes POST requests to the root URL, and
|
|
148
|
+
# returns a Rack response triple.
|
|
149
|
+
#
|
|
150
|
+
# If request isn't to the root URL, a `404 Not Found` response is
|
|
151
|
+
# returned. If request verb isn't GET, a `405 Method Not Allowed`
|
|
152
|
+
# response is returned.
|
|
153
|
+
def call(env)
|
|
154
|
+
request = Rack::Request.new(env)
|
|
155
|
+
|
|
156
|
+
status, headers, body = catch(:halt) do
|
|
157
|
+
error!(404, "Not Found") unless ["", "/"].include?(request.path_info)
|
|
158
|
+
error!(405, "Method Not Allowed") unless request.post?
|
|
159
|
+
|
|
160
|
+
handle_request(request)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
headers["Content-Length"] = body.map(&:bytesize).inject(0, :+).to_s
|
|
164
|
+
|
|
165
|
+
[status, headers, body]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# Accepts a `Rack::Request` object, uploads the file, and returns a Rack
|
|
171
|
+
# response.
|
|
172
|
+
def handle_request(request)
|
|
173
|
+
io = get_io(request)
|
|
174
|
+
context = get_context(request)
|
|
175
|
+
|
|
176
|
+
uploaded_file = upload(io, context, request)
|
|
177
|
+
|
|
178
|
+
make_response(uploaded_file, request)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Retrieves the "file" multipart request parameter, and returns an
|
|
182
|
+
# IO-like object that can be passed to `Shrine#upload`.
|
|
183
|
+
def get_io(request)
|
|
184
|
+
file = request.params["file"]
|
|
185
|
+
|
|
186
|
+
error!(400, "Upload Not Found") unless file.is_a?(Hash) && file[:tempfile]
|
|
187
|
+
error!(413, "Upload Too Large") if @max_size && file[:tempfile].size > @max_size
|
|
188
|
+
|
|
189
|
+
@shrine_class.rack_file(file)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Returns a hash of information containing `:action` and `:request`
|
|
193
|
+
# keys, which is to be passed to `Shrine#upload`. Calls
|
|
194
|
+
# `:upload_context` option if given.
|
|
195
|
+
def get_context(request)
|
|
196
|
+
context = { action: :upload, phase: :upload, request: request }
|
|
197
|
+
context.merge! @upload_context.call(request) if @upload_context
|
|
198
|
+
context
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Calls `Shrine#upload` with the given IO and context, and returns a
|
|
202
|
+
# `Shrine::UploadedFile` object. If `:upload` option is given, calls
|
|
203
|
+
# that instead.
|
|
204
|
+
def upload(io, context, request)
|
|
205
|
+
if @upload
|
|
206
|
+
@upload.call(io, context, request)
|
|
207
|
+
else
|
|
208
|
+
uploader.upload(io, context)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Transforms the uploaded file object into a JSON response. It returns
|
|
213
|
+
# a Rack response triple - an array consisting of a status number, hash
|
|
214
|
+
# of headers, and a body enumerable. If a `:rack_response` option is
|
|
215
|
+
# given, calls that instead.
|
|
216
|
+
def make_response(object, request)
|
|
217
|
+
if @rack_response
|
|
218
|
+
@rack_response.call(object, request)
|
|
219
|
+
else
|
|
220
|
+
[200, {"Content-Type" => "application/json"}, [object.to_json]]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Used for early returning an error response.
|
|
225
|
+
def error!(status, message)
|
|
226
|
+
throw :halt, [status, {"Content-Type" => "text/plain"}, [message]]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Returns the uploader around the specified storage.
|
|
230
|
+
def uploader
|
|
231
|
+
@shrine_class.new(@storage_key)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
register_plugin(:upload_endpoint, UploadEndpoint)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -202,8 +202,9 @@ class Shrine
|
|
|
202
202
|
def method_missing(name, *args)
|
|
203
203
|
if name == :download
|
|
204
204
|
Shrine.deprecation("Shrine::Storage::FileSystem#download is deprecated and will be removed in Shrine 3.")
|
|
205
|
-
|
|
206
|
-
open(*args) { |file|
|
|
205
|
+
tempfile = Tempfile.new(["shrine-filesystem", File.extname(args[0])], binmode: true)
|
|
206
|
+
open(*args) { |file| IO.copy_stream(file, tempfile) }
|
|
207
|
+
tempfile.tap(&:open)
|
|
207
208
|
else
|
|
208
209
|
super
|
|
209
210
|
end
|
data/lib/shrine/storage/s3.rb
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
require "
|
|
1
|
+
begin
|
|
2
|
+
require "aws-sdk-s3"
|
|
3
|
+
if Gem::Version.new(Aws::S3::GEM_VERSION) < Gem::Version.new("1.2.0")
|
|
4
|
+
raise "Shrine::Storage::S3 requires aws-sdk-s3 version 1.2.0 or above"
|
|
5
|
+
end
|
|
6
|
+
rescue LoadError
|
|
7
|
+
require "aws-sdk"
|
|
8
|
+
Aws.eager_autoload!(services: ["S3"])
|
|
9
|
+
end
|
|
10
|
+
require "down/chunked_io"
|
|
3
11
|
require "uri"
|
|
4
|
-
require "cgi
|
|
5
|
-
|
|
6
|
-
Aws.eager_autoload!(services: ["S3"])
|
|
12
|
+
require "cgi"
|
|
7
13
|
|
|
8
14
|
class Shrine
|
|
9
15
|
module Storage
|
|
10
|
-
# The S3 storage handles uploads to Amazon S3 service, using the
|
|
11
|
-
# gem:
|
|
16
|
+
# The S3 storage handles uploads to Amazon S3 service, using the
|
|
17
|
+
# [aws-sdk-s3] gem:
|
|
12
18
|
#
|
|
13
|
-
# gem "aws-sdk", "~> 2
|
|
19
|
+
# gem "aws-sdk-s3", "~> 1.2"
|
|
14
20
|
#
|
|
15
21
|
# It is initialized with the following 4 required options:
|
|
16
22
|
#
|
|
@@ -49,7 +55,7 @@ class Shrine
|
|
|
49
55
|
#
|
|
50
56
|
# Shrine::Storage::S3.new(upload_options: {acl: "private"}, **s3_options)
|
|
51
57
|
#
|
|
52
|
-
# These options will be passed to aws-sdk's methods for [uploading],
|
|
58
|
+
# These options will be passed to aws-sdk-s3's methods for [uploading],
|
|
53
59
|
# [copying] and [presigning].
|
|
54
60
|
#
|
|
55
61
|
# You can also generate upload options per upload with the `upload_options`
|
|
@@ -81,7 +87,7 @@ class Shrine
|
|
|
81
87
|
# uploaded_file.url(public: true) # public URL without signed parameters
|
|
82
88
|
# uploaded_file.url(download: true) # forced download URL
|
|
83
89
|
#
|
|
84
|
-
# All other options are forwarded to the
|
|
90
|
+
# All other options are forwarded to the aws-sdk-s3 gem:
|
|
85
91
|
#
|
|
86
92
|
# uploaded_file.url(expires_in: 15)
|
|
87
93
|
# uploaded_file.url(virtual_host: true)
|
|
@@ -106,16 +112,16 @@ class Shrine
|
|
|
106
112
|
# ## Presigns
|
|
107
113
|
#
|
|
108
114
|
# This storage can generate presigns for direct uploads to Amazon S3, and
|
|
109
|
-
# it accepts additional options which are passed to
|
|
115
|
+
# it accepts additional options which are passed to aws-sdk-s3. There are
|
|
110
116
|
# three places in which you can specify presign options:
|
|
111
117
|
#
|
|
112
118
|
# * in `:upload_options` option on this storage
|
|
113
|
-
# * in `
|
|
119
|
+
# * in `presign_endpoint` plugin through `:presign_options`
|
|
114
120
|
# * in `Storage::S3#presign` by forwarding options
|
|
115
121
|
#
|
|
116
122
|
# ## Large files
|
|
117
123
|
#
|
|
118
|
-
# The
|
|
124
|
+
# The aws-sdk-s3 gem has the ability to automatically use multipart
|
|
119
125
|
# upload/copy for larger files, splitting the file into multiple chunks
|
|
120
126
|
# and uploading/copying them in parallel.
|
|
121
127
|
#
|
|
@@ -127,7 +133,7 @@ class Shrine
|
|
|
127
133
|
# thresholds = {upload: 30*1024*1024, copy: 200*1024*1024}
|
|
128
134
|
# Shrine::Storage::S3.new(multipart_threshold: thresholds, **s3_options)
|
|
129
135
|
#
|
|
130
|
-
# If you want to change how many threads
|
|
136
|
+
# If you want to change how many threads aws-sdk-s3 will use for multipart
|
|
131
137
|
# upload/copy, you can use the `upload_options` plugin to specify
|
|
132
138
|
# `:thread_count`.
|
|
133
139
|
#
|
|
@@ -139,14 +145,14 @@ class Shrine
|
|
|
139
145
|
#
|
|
140
146
|
# If you're using S3 as a cache, you will probably want to periodically
|
|
141
147
|
# delete old files which aren't used anymore. S3 has a built-in way to do
|
|
142
|
-
# this, read [this article]
|
|
143
|
-
# for instructions.
|
|
148
|
+
# this, read [this article][object lifecycle] for instructions.
|
|
144
149
|
#
|
|
145
|
-
# [uploading]: http://docs.aws.amazon.com/
|
|
146
|
-
# [copying]: http://docs.aws.amazon.com/
|
|
147
|
-
# [presigning]: http://docs.aws.amazon.com/
|
|
148
|
-
# [aws-sdk]: https://github.com/aws/aws-sdk-ruby
|
|
150
|
+
# [uploading]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
|
151
|
+
# [copying]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#copy_from-instance_method
|
|
152
|
+
# [presigning]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
|
153
|
+
# [aws-sdk-s3]: https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-sdk-s3
|
|
149
154
|
# [Transfer Acceleration]: http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
|
|
155
|
+
# [object lifecycle]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
|
150
156
|
class S3
|
|
151
157
|
attr_reader :client, :bucket, :prefix, :host, :upload_options
|
|
152
158
|
|
|
@@ -156,7 +162,7 @@ class Shrine
|
|
|
156
162
|
# :secret_access_key
|
|
157
163
|
# :region
|
|
158
164
|
# :bucket
|
|
159
|
-
# : Credentials required by the `aws-sdk` gem.
|
|
165
|
+
# : Credentials required by the `aws-sdk-s3` gem.
|
|
160
166
|
#
|
|
161
167
|
# :prefix
|
|
162
168
|
# : "Folder" name inside the bucket to store files into.
|
|
@@ -174,22 +180,21 @@ class Shrine
|
|
|
174
180
|
#
|
|
175
181
|
# All other options are forwarded to [`Aws::S3::Client#initialize`].
|
|
176
182
|
#
|
|
177
|
-
# [`Aws::S3::Object#put`]: http://docs.aws.amazon.com/
|
|
178
|
-
# [`Aws::S3::Object#copy_from`]: http://docs.aws.amazon.com/
|
|
179
|
-
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/
|
|
180
|
-
# [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/
|
|
183
|
+
# [`Aws::S3::Object#put`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
|
184
|
+
# [`Aws::S3::Object#copy_from`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#copy_from-instance_method
|
|
185
|
+
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_post-instance_method
|
|
186
|
+
# [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#initialize-instance_method
|
|
181
187
|
def initialize(bucket:, prefix: nil, host: nil, upload_options: {}, multipart_threshold: {}, **s3_options)
|
|
182
188
|
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
|
|
183
189
|
resource = Aws::S3::Resource.new(**s3_options)
|
|
184
190
|
|
|
185
191
|
if multipart_threshold.is_a?(Integer)
|
|
186
192
|
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}")
|
|
187
|
-
multipart_threshold = {upload: multipart_threshold}
|
|
193
|
+
multipart_threshold = { upload: multipart_threshold }
|
|
188
194
|
end
|
|
189
|
-
multipart_threshold
|
|
190
|
-
multipart_threshold[:copy] ||= 100*1024*1024
|
|
195
|
+
multipart_threshold = { upload: 15*1024*1024, copy: 100*1024*1024 }.merge(multipart_threshold)
|
|
191
196
|
|
|
192
|
-
@bucket = resource.bucket(bucket)
|
|
197
|
+
@bucket = resource.bucket(bucket) or fail(ArgumentError, "the :bucket option was nil")
|
|
193
198
|
@client = resource.client
|
|
194
199
|
@prefix = prefix
|
|
195
200
|
@host = host
|
|
@@ -240,7 +245,10 @@ class Shrine
|
|
|
240
245
|
|
|
241
246
|
# Returns a `Down::ChunkedIO` object representing the S3 object.
|
|
242
247
|
def open(id)
|
|
243
|
-
|
|
248
|
+
object = object(id)
|
|
249
|
+
io = Down::ChunkedIO.new(chunks: object.enum_for(:get), data: {object: object})
|
|
250
|
+
io.size = object.content_length
|
|
251
|
+
io
|
|
244
252
|
end
|
|
245
253
|
|
|
246
254
|
# Returns true file exists on S3.
|
|
@@ -248,20 +256,6 @@ class Shrine
|
|
|
248
256
|
object(id).exists?
|
|
249
257
|
end
|
|
250
258
|
|
|
251
|
-
# Deletes the file from S3.
|
|
252
|
-
def delete(id)
|
|
253
|
-
object(id).delete
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# This is called when multiple files are being deleted at once. Issues a
|
|
257
|
-
# single MULTI DELETE command for each 1000 objects (S3 delete limit).
|
|
258
|
-
def multi_delete(ids)
|
|
259
|
-
ids.each_slice(1000) do |ids_batch|
|
|
260
|
-
delete_params = {objects: ids_batch.map { |id| {key: object(id).key} }}
|
|
261
|
-
bucket.delete_objects(delete: delete_params)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
259
|
# Returns the presigned URL to the file.
|
|
266
260
|
#
|
|
267
261
|
# :public
|
|
@@ -282,8 +276,8 @@ class Shrine
|
|
|
282
276
|
# All other options are forwarded to [`Aws::S3::Object#presigned_url`] or
|
|
283
277
|
# [`Aws::S3::Object#public_url`].
|
|
284
278
|
#
|
|
285
|
-
# [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/
|
|
286
|
-
# [`Aws::S3::Object#public_url`]: http://docs.aws.amazon.com/
|
|
279
|
+
# [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#presigned_url-instance_method
|
|
280
|
+
# [`Aws::S3::Object#public_url`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#public_url-instance_method
|
|
287
281
|
def url(id, download: nil, public: nil, host: self.host, **options)
|
|
288
282
|
options[:response_content_disposition] ||= "attachment" if download
|
|
289
283
|
options[:response_content_disposition] = encode_content_disposition(options[:response_content_disposition]) if options[:response_content_disposition]
|
|
@@ -303,24 +297,38 @@ class Shrine
|
|
|
303
297
|
url
|
|
304
298
|
end
|
|
305
299
|
|
|
306
|
-
# Deletes all files from the storage.
|
|
307
|
-
def clear!
|
|
308
|
-
objects = bucket.object_versions(prefix: prefix)
|
|
309
|
-
objects.respond_to?(:batch_delete!) ? objects.batch_delete! : objects.delete
|
|
310
|
-
end
|
|
311
|
-
|
|
312
300
|
# Returns a signature for direct uploads. Internally it calls
|
|
313
301
|
# [`Aws::S3::Bucket#presigned_post`], and forwards any additional options
|
|
314
302
|
# to it.
|
|
315
303
|
#
|
|
316
|
-
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/
|
|
304
|
+
# [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Bucket.html#presigned_post-instance_method
|
|
317
305
|
def presign(id, **options)
|
|
318
|
-
options = upload_options.merge(options)
|
|
306
|
+
options = @upload_options.merge(options)
|
|
319
307
|
options[:content_disposition] = encode_content_disposition(options[:content_disposition]) if options[:content_disposition]
|
|
320
308
|
|
|
321
309
|
object(id).presigned_post(options)
|
|
322
310
|
end
|
|
323
311
|
|
|
312
|
+
# Deletes the file from S3.
|
|
313
|
+
def delete(id)
|
|
314
|
+
object(id).delete
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# This is called when multiple files are being deleted at once. Issues a
|
|
318
|
+
# single MULTI DELETE command for each 1000 objects (S3 delete limit).
|
|
319
|
+
def multi_delete(ids)
|
|
320
|
+
ids.each_slice(1000) do |ids_batch|
|
|
321
|
+
delete_params = {objects: ids_batch.map { |id| {key: object(id).key} }}
|
|
322
|
+
bucket.delete_objects(delete: delete_params)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Deletes all files from the storage.
|
|
327
|
+
def clear!
|
|
328
|
+
objects = bucket.object_versions(prefix: prefix)
|
|
329
|
+
objects.respond_to?(:batch_delete!) ? objects.batch_delete! : objects.delete
|
|
330
|
+
end
|
|
331
|
+
|
|
324
332
|
# Returns an `Aws::S3::Object` for the given id.
|
|
325
333
|
def object(id)
|
|
326
334
|
bucket.object([*prefix, id].join("/"))
|