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
|
@@ -3,6 +3,9 @@ require "json"
|
|
|
3
3
|
|
|
4
4
|
class Shrine
|
|
5
5
|
module Plugins
|
|
6
|
+
# *[OBSOLETE] This plugin is obsolete, you should use `upload_endpoint` or
|
|
7
|
+
# `presign_endpoint` plugins instead.*
|
|
8
|
+
#
|
|
6
9
|
# The `direct_upload` plugin provides a Rack endpoint which can be used for
|
|
7
10
|
# uploading individual files asynchronously. It requires the [Roda] gem.
|
|
8
11
|
#
|
|
@@ -283,7 +286,7 @@ class Shrine
|
|
|
283
286
|
|
|
284
287
|
# Generates a presign that points to the direct upload endpoint.
|
|
285
288
|
def generate_fake_presign(location, options)
|
|
286
|
-
url = request.url.sub(/presign
|
|
289
|
+
url = request.url.sub(/presign[^\/]*$/, "upload")
|
|
287
290
|
{url: url, fields: {key: location}}
|
|
288
291
|
end
|
|
289
292
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "roda"
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
3
5
|
|
|
4
6
|
class Shrine
|
|
5
7
|
module Plugins
|
|
@@ -10,23 +12,23 @@ class Shrine
|
|
|
10
12
|
#
|
|
11
13
|
# plugin :download_endpoint, storages: [:store], prefix: "attachments"
|
|
12
14
|
#
|
|
13
|
-
# After loading the plugin the endpoint
|
|
15
|
+
# After loading the plugin the endpoint should be mounted on the specified
|
|
16
|
+
# prefix:
|
|
14
17
|
#
|
|
15
18
|
# Rails.appliations.routes.draw do
|
|
16
|
-
# mount Shrine
|
|
19
|
+
# mount Shrine.download_endpoint => "/attachments"
|
|
17
20
|
# end
|
|
18
21
|
#
|
|
19
22
|
# Now all stored files can be downloaded through the endpoint, and the
|
|
20
23
|
# endpoint will efficiently stream the file from the storage when the
|
|
21
24
|
# storage supports it. `UploadedFile#url` will automatically return the URL
|
|
22
|
-
# to the endpoint for
|
|
23
|
-
# code:
|
|
25
|
+
# to the endpoint for files uploaded to specified storages:
|
|
24
26
|
#
|
|
25
|
-
# user.avatar.url #=> "/attachments/
|
|
27
|
+
# user.avatar.url #=> "/attachments/eyJpZCI6ImFkdzlyeTM5ODJpandoYWla"
|
|
26
28
|
#
|
|
27
29
|
# :storages
|
|
28
|
-
# : An array of storage keys which
|
|
29
|
-
#
|
|
30
|
+
# : An array of storage keys for which `UploadedFile#url` should generate
|
|
31
|
+
# download endpoint URLs.
|
|
30
32
|
#
|
|
31
33
|
# :prefix
|
|
32
34
|
# : The location where the download endpoint was mounted. If it was
|
|
@@ -42,22 +44,20 @@ class Shrine
|
|
|
42
44
|
# The default is "inline".
|
|
43
45
|
#
|
|
44
46
|
# Note that streaming the file through your app might impact the request
|
|
45
|
-
# throughput of your app,
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# body.
|
|
47
|
+
# throughput of your app, depending on which web server is used. In any
|
|
48
|
+
# case, it's recommended to use some kind of cache in front of the web
|
|
49
|
+
# server.
|
|
49
50
|
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# implements `async.callback`.
|
|
51
|
+
# If you want to authenticate the downloads, it's recommended you use the
|
|
52
|
+
# `rack_response` plugin directly. With it you can return file responses
|
|
53
|
+
# from inside your router/controller.
|
|
54
54
|
#
|
|
55
55
|
# [Roda]: https://github.com/jeremyevans/roda
|
|
56
|
-
# [Thin]: https://github.com/macournoyer/thin
|
|
57
|
-
# [Rainbows]: https://rubygems.org/gems/rainbows
|
|
58
|
-
# [Reel]: https://github.com/celluloid/reel
|
|
59
|
-
# [EventMachine]: https://github.com/eventmachine
|
|
60
56
|
module DownloadEndpoint
|
|
57
|
+
def self.load_dependencies(uploader, opts = {})
|
|
58
|
+
uploader.plugin :rack_response
|
|
59
|
+
end
|
|
60
|
+
|
|
61
61
|
def self.configure(uploader, opts = {})
|
|
62
62
|
uploader.opts[:download_endpoint_storages] = opts.fetch(:storages, uploader.opts[:download_endpoint_storages])
|
|
63
63
|
uploader.opts[:download_endpoint_prefix] = opts.fetch(:prefix, uploader.opts[:download_endpoint_prefix])
|
|
@@ -73,14 +73,28 @@ class Shrine
|
|
|
73
73
|
# Assigns the subclass a copy of the download endpoint class.
|
|
74
74
|
def inherited(subclass)
|
|
75
75
|
super
|
|
76
|
-
subclass.assign_download_endpoint(
|
|
76
|
+
subclass.assign_download_endpoint(@download_endpoint)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns the Rack application that retrieves requested files.
|
|
80
|
+
def download_endpoint
|
|
81
|
+
@download_endpoint
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
# Assigns the subclassed endpoint as the `DownloadEndpoint` constant.
|
|
80
85
|
def assign_download_endpoint(klass)
|
|
81
86
|
endpoint_class = Class.new(klass)
|
|
82
87
|
endpoint_class.opts[:shrine_class] = self
|
|
88
|
+
endpoint_class.opts[:disposition] = opts[:download_endpoint_disposition]
|
|
89
|
+
|
|
90
|
+
@download_endpoint = endpoint_class
|
|
91
|
+
|
|
83
92
|
const_set(:DownloadEndpoint, endpoint_class)
|
|
93
|
+
deprecate_constant(:DownloadEndpoint) if RUBY_VERSION > "2.3"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def download_endpoint_serializer
|
|
97
|
+
@download_endpoint_serializer ||= Serializer.new
|
|
84
98
|
end
|
|
85
99
|
end
|
|
86
100
|
|
|
@@ -90,85 +104,134 @@ class Shrine
|
|
|
90
104
|
# of storages it just returns their original URL.
|
|
91
105
|
def url(**options)
|
|
92
106
|
if shrine_class.opts[:download_endpoint_storages].include?(storage_key.to_sym)
|
|
93
|
-
|
|
94
|
-
shrine_class.opts[:download_endpoint_host],
|
|
95
|
-
*shrine_class.opts[:download_endpoint_prefix],
|
|
96
|
-
storage_key,
|
|
97
|
-
id,
|
|
98
|
-
].join("/")
|
|
107
|
+
download_url
|
|
99
108
|
else
|
|
100
109
|
super
|
|
101
110
|
end
|
|
102
111
|
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def download_url
|
|
116
|
+
[download_host, *download_prefix, download_identifier].join("/")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generates URL-safe identifier from data, filtering only a subset of
|
|
120
|
+
# metadata that the endpoint needs to prevent the URL from being too
|
|
121
|
+
# long.
|
|
122
|
+
def download_identifier
|
|
123
|
+
semantical_metadata = metadata.select { |name, _| %w[filename size mime_type].include?(name) }
|
|
124
|
+
download_serializer.dump(data.merge("metadata" => semantical_metadata))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def download_serializer
|
|
128
|
+
shrine_class.download_endpoint_serializer
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def download_host
|
|
132
|
+
shrine_class.opts[:download_endpoint_host]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def download_prefix
|
|
136
|
+
shrine_class.opts[:download_endpoint_prefix]
|
|
137
|
+
end
|
|
103
138
|
end
|
|
104
139
|
|
|
105
140
|
# Routes incoming requests. It first asserts that the storage is existent
|
|
106
141
|
# and allowed. Afterwards it proceeds with the file download using
|
|
107
142
|
# streaming.
|
|
108
143
|
class App < Roda
|
|
109
|
-
plugin :streaming
|
|
110
|
-
|
|
111
144
|
route do |r|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
# handle legacy ":storage/:id" URLs
|
|
146
|
+
r.on storage_names do |storage_name|
|
|
115
147
|
r.get /(.*)/ do |id|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
response["Content-Disposition"] = "#{disposition}; filename=\"#{filename}\""
|
|
120
|
-
response["Content-Type"] = Rack::Mime.mime_type(extname)
|
|
121
|
-
|
|
122
|
-
io = storage.open(id)
|
|
123
|
-
response["Content-Length"] = io.size.to_s if io.size
|
|
124
|
-
|
|
125
|
-
stream(callback: ->{io.close}) do |out|
|
|
126
|
-
if io.respond_to?(:each_chunk) # Down::ChunkedIO
|
|
127
|
-
io.each_chunk { |chunk| out << chunk }
|
|
128
|
-
else
|
|
129
|
-
out << io.read(16*1024) until io.eof?
|
|
130
|
-
end
|
|
131
|
-
end
|
|
148
|
+
data = { "id" => id, "storage" => storage_name, "metadata" => {} }
|
|
149
|
+
stream_file(data)
|
|
132
150
|
end
|
|
133
151
|
end
|
|
152
|
+
|
|
153
|
+
r.get /(.*)/ do |identifier|
|
|
154
|
+
data = serializer.load(identifier)
|
|
155
|
+
stream_file(data)
|
|
156
|
+
end
|
|
134
157
|
end
|
|
135
158
|
|
|
136
159
|
private
|
|
137
160
|
|
|
138
|
-
|
|
161
|
+
def stream_file(data)
|
|
162
|
+
uploaded_file = get_uploaded_file(data)
|
|
163
|
+
|
|
164
|
+
status, headers, body = uploaded_file.to_rack_response(disposition: disposition)
|
|
165
|
+
headers["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
|
|
139
166
|
|
|
140
|
-
|
|
141
|
-
allow_storage!(storage_key)
|
|
142
|
-
shrine_class.find_storage(storage_key)
|
|
167
|
+
request.halt [status, headers, body]
|
|
143
168
|
end
|
|
144
169
|
|
|
145
|
-
#
|
|
146
|
-
def
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
# Returns a Shrine::UploadedFile, or returns 404 if file doesn't exist.
|
|
171
|
+
def get_uploaded_file(data)
|
|
172
|
+
uploaded_file = shrine_class.uploaded_file(data)
|
|
173
|
+
not_found! unless uploaded_file.exists?
|
|
174
|
+
uploaded_file
|
|
175
|
+
rescue Shrine::Error
|
|
176
|
+
not_found!
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def not_found!
|
|
180
|
+
error!(404, "File Not Found")
|
|
150
181
|
end
|
|
151
182
|
|
|
152
183
|
# Halts the request with the error message.
|
|
153
184
|
def error!(status, message)
|
|
154
185
|
response.status = status
|
|
155
|
-
response["Content-Type"] = "
|
|
156
|
-
response.write(
|
|
186
|
+
response["Content-Type"] = "text/plain"
|
|
187
|
+
response.write(message)
|
|
157
188
|
request.halt
|
|
158
189
|
end
|
|
159
190
|
|
|
160
|
-
def
|
|
161
|
-
shrine_class.
|
|
191
|
+
def storage_names
|
|
192
|
+
shrine_class.storages.keys.map(&:to_s)
|
|
162
193
|
end
|
|
163
194
|
|
|
164
|
-
def
|
|
165
|
-
shrine_class.
|
|
195
|
+
def serializer
|
|
196
|
+
shrine_class.download_endpoint_serializer
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def disposition
|
|
200
|
+
opts[:disposition]
|
|
166
201
|
end
|
|
167
202
|
|
|
168
203
|
def shrine_class
|
|
169
204
|
opts[:shrine_class]
|
|
170
205
|
end
|
|
171
206
|
end
|
|
207
|
+
|
|
208
|
+
class Serializer
|
|
209
|
+
def dump(data)
|
|
210
|
+
base64_encode(json_encode(data))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def load(data)
|
|
214
|
+
json_decode(base64_decode(data))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def json_encode(data)
|
|
220
|
+
JSON.generate(data)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def base64_encode(data)
|
|
224
|
+
Base64.urlsafe_encode64(data)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def base64_decode(data)
|
|
228
|
+
Base64.urlsafe_decode64(data)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def json_decode(data)
|
|
232
|
+
JSON.parse(data)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
172
235
|
end
|
|
173
236
|
|
|
174
237
|
register_plugin(:download_endpoint, DownloadEndpoint)
|
|
@@ -16,7 +16,7 @@ class Shrine
|
|
|
16
16
|
#
|
|
17
17
|
# For example, the following will keep destroyed and replaced files:
|
|
18
18
|
#
|
|
19
|
-
# plugin :keep_files, destroyed: true,
|
|
19
|
+
# plugin :keep_files, destroyed: true, replaced: true
|
|
20
20
|
#
|
|
21
21
|
# [event store]: http://docs.geteventstore.com/introduction/event-sourcing-basics/
|
|
22
22
|
module KeepFiles
|
|
@@ -3,7 +3,7 @@ class Shrine
|
|
|
3
3
|
# The `metadata_attributes` plugin allows you to sync attachment metadata
|
|
4
4
|
# to additional record attributes.
|
|
5
5
|
#
|
|
6
|
-
# plugin :
|
|
6
|
+
# plugin :metadata_attributes
|
|
7
7
|
#
|
|
8
8
|
# It provides `Attacher.metadata_attributes` method which allows you to
|
|
9
9
|
# specify mappings between metadata fields on the attachment and attribute
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
class Shrine
|
|
6
|
+
module Plugins
|
|
7
|
+
# The `presign_endpoint` plugin provides a Rack endpoint which generates
|
|
8
|
+
# the URL, fields, and headers that can be used to upload files directly to
|
|
9
|
+
# a storage service. It can be used with client-side file upload libraries
|
|
10
|
+
# like [FineUploader], [Dropzone] or [jQuery-File-Upload] for asynchronous
|
|
11
|
+
# uploads. Storage services that support direct uploads include [Amazon
|
|
12
|
+
# S3], [Google Cloud Storage], [Microsoft Azure Storage] and more.
|
|
13
|
+
#
|
|
14
|
+
# plugin :presign_endpoint
|
|
15
|
+
#
|
|
16
|
+
# The plugin adds a `Shrine.presign_endpoint` method which accepts a
|
|
17
|
+
# storage identifier and returns a Rack application that accepts GET
|
|
18
|
+
# requests and generates a presign for the specified storage.
|
|
19
|
+
#
|
|
20
|
+
# Shrine.presign_endpoint(:cache) # rack app
|
|
21
|
+
#
|
|
22
|
+
# Asynchronous upload is typically meant to replace the caching phase in
|
|
23
|
+
# the default synchronous workflow, so we want to generate parameters for
|
|
24
|
+
# uploads to the temporary (`:cache`) storage.
|
|
25
|
+
#
|
|
26
|
+
# We can mount the returned Rack application inside our application:
|
|
27
|
+
#
|
|
28
|
+
# Rails.application.routes.draw do
|
|
29
|
+
# mount Shrine.presign_endpoint(:cache) => "/presign"
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# The above will create a `GET /presign` endpoint, which generates presign
|
|
33
|
+
# URL, fields, and headers using the specified storage, and returns it in
|
|
34
|
+
# JSON format.
|
|
35
|
+
#
|
|
36
|
+
# # GET /presign
|
|
37
|
+
# {
|
|
38
|
+
# "url": "https://my-bucket.s3-eu-west-1.amazonaws.com",
|
|
39
|
+
# "fields": {
|
|
40
|
+
# "key": "b7d575850ba61b44c8a9ff889dfdb14d88cdc25f8dd121004c8",
|
|
41
|
+
# "policy": "eyJleHBpcmF0aW9uIjoiMjAxNS0QwMToxMToyOVoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJzaHJpbmUtdGVzdGluZyJ9LHsia2V5IjoiYjdkNTc1ODUwYmE2MWI0NGU3Y2M4YTliZmY4OGU5ZGZkYjE2NTQ0ZDk4OGNkYzI1ZjhkZDEyMTAwNGM4In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlKRjU1VE1aWlk0NVVUNlEvMjAxNTEwMjQvZXUtd2VzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMDI0VDAwMTEyOVoifV19",
|
|
42
|
+
# "x-amz-credential": "AKIAIJF55TMZYT6Q/20151024/eu-west-1/s3/aws4_request",
|
|
43
|
+
# "x-amz-algorithm": "AWS4-HMAC-SHA256",
|
|
44
|
+
# "x-amz-date": "20151024T001129Z",
|
|
45
|
+
# "x-amz-signature": "c1eb634f83f96b69bd675f535b3ff15ae184b102fcba51e4db5f4959b4ae26f4"
|
|
46
|
+
# },
|
|
47
|
+
# "headers": {}
|
|
48
|
+
# }
|
|
49
|
+
#
|
|
50
|
+
# This gives the client all the information it needs to make the upload
|
|
51
|
+
# request to the selected file to the storage service. The `url` field is
|
|
52
|
+
# the request URL, `fields` are the required POST parameters, and `headers`
|
|
53
|
+
# are the required request headers.
|
|
54
|
+
#
|
|
55
|
+
# ## Location
|
|
56
|
+
#
|
|
57
|
+
# By default the generated location won't have any file extension, but you
|
|
58
|
+
# can specify one by sending the `filename` query parameter:
|
|
59
|
+
#
|
|
60
|
+
# GET /presign?filename=nature.jpg
|
|
61
|
+
#
|
|
62
|
+
# It's also possible to customize how the presign location is generated:
|
|
63
|
+
#
|
|
64
|
+
# plugin :presign_endpoint, presign_location: -> (request) do
|
|
65
|
+
# "#{SecureRandom.hex}/#{request.params["filename"]}"
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# ## Options
|
|
69
|
+
#
|
|
70
|
+
# Some storages accept additional presign options, which you can pass in via
|
|
71
|
+
# `:presign_options`:
|
|
72
|
+
#
|
|
73
|
+
# plugin :presign_endpoint, presign_options: -> (request) do
|
|
74
|
+
# filename = request.params["filename"]
|
|
75
|
+
# extension = File.extname(filename)
|
|
76
|
+
# content_type = Rack::Mime.mime_type(extension)
|
|
77
|
+
#
|
|
78
|
+
# {
|
|
79
|
+
# content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
|
|
80
|
+
# content_disposition: "attachment; filename=\"#{filename}\"", # download with original filename
|
|
81
|
+
# content_type: content_type, # set correct content type
|
|
82
|
+
# }
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# ## Presign
|
|
86
|
+
#
|
|
87
|
+
# You can also customize how the presign itself is generated via the
|
|
88
|
+
# `:presign` option:
|
|
89
|
+
#
|
|
90
|
+
# plugin :presign_endpoint, presign: -> (id, options, request) do
|
|
91
|
+
# # return an object that responds to #url, #fields, and #headers
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# ## Response
|
|
95
|
+
#
|
|
96
|
+
# The response returned by the endpoint can be customized via the
|
|
97
|
+
# `:rack_response` option:
|
|
98
|
+
#
|
|
99
|
+
# plugin :presign_endpoint, rack_response: -> (hash, request) do
|
|
100
|
+
# body = { endpoint: hash[:url], params: hash[:fields], headers: hash[:headers] }.to_json
|
|
101
|
+
# [201, { "Content-Type" => "application/json" }, [body]]
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# ## Ad-hoc options
|
|
105
|
+
#
|
|
106
|
+
# You can override any of the options above when creating the endpoint:
|
|
107
|
+
#
|
|
108
|
+
# Shrine.presign_endpoint(:cache, presign_location: "${filename}")
|
|
109
|
+
#
|
|
110
|
+
# [Amazon S3]: https://aws.amazon.com/s3/
|
|
111
|
+
# [Google Cloud Storage]: https://cloud.google.com/storage/
|
|
112
|
+
# [Microsoft Azure Storage]: https://azure.microsoft.com/en-us/services/storage/
|
|
113
|
+
module PresignEndpoint
|
|
114
|
+
def self.configure(uploader, opts = {})
|
|
115
|
+
uploader.opts[:presign_endpoint_presign_location] = opts.fetch(:presign_location, uploader.opts[:presign_endpoint_presign_location])
|
|
116
|
+
uploader.opts[:presign_endpoint_presign_options] = opts.fetch(:presign_options, uploader.opts[:presign_endpoint_presign_options])
|
|
117
|
+
uploader.opts[:presign_endpoint_presign] = opts.fetch(:presign, uploader.opts[:presign_endpoint_presign])
|
|
118
|
+
uploader.opts[:presign_endpoint_rack_response] = opts.fetch(:rack_response, uploader.opts[:presign_endpoint_rack_response])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
module ClassMethods
|
|
122
|
+
# Returns a Rack application (object that responds to `#call`) which
|
|
123
|
+
# accepts GET requests to the root URL, calls the specified storage to
|
|
124
|
+
# generate the presign, and returns that information in JSON format.
|
|
125
|
+
#
|
|
126
|
+
# The `storage_key` needs to be one of the registered Shrine storages.
|
|
127
|
+
# Additional options can be given to override the options given on
|
|
128
|
+
# plugin initialization.
|
|
129
|
+
def presign_endpoint(storage_key, **options)
|
|
130
|
+
App.new({
|
|
131
|
+
shrine_class: self,
|
|
132
|
+
storage_key: storage_key,
|
|
133
|
+
presign_location: opts[:presign_endpoint_presign_location],
|
|
134
|
+
presign_options: opts[:presign_endpoint_presign_options],
|
|
135
|
+
presign: opts[:presign_endpoint_presign],
|
|
136
|
+
rack_response: opts[:presign_endpoint_rack_response],
|
|
137
|
+
}.merge(options))
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Rack application that accepts GET request to the root URL, calls
|
|
142
|
+
# `#presign` on the specified storage, and returns that information in
|
|
143
|
+
# JSON format.
|
|
144
|
+
class App
|
|
145
|
+
# Writes given options to instance variables.
|
|
146
|
+
def initialize(options)
|
|
147
|
+
options.each do |name, value|
|
|
148
|
+
instance_variable_set("@#{name}", value)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Accepts a Rack env hash, routes GET requests to the root URL, and
|
|
153
|
+
# returns a Rack response triple.
|
|
154
|
+
#
|
|
155
|
+
# If request isn't to the root URL, a `404 Not Found` response is
|
|
156
|
+
# returned. If request verb isn't GET, a `405 Method Not Allowed`
|
|
157
|
+
# response is returned.
|
|
158
|
+
def call(env)
|
|
159
|
+
request = Rack::Request.new(env)
|
|
160
|
+
|
|
161
|
+
status, headers, body = catch(:halt) do
|
|
162
|
+
error!(404, "Not Found") unless ["", "/"].include?(request.path_info)
|
|
163
|
+
error!(405, "Method Not Allowed") unless request.get?
|
|
164
|
+
|
|
165
|
+
handle_request(request)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
headers["Content-Length"] = body.map(&:bytesize).inject(0, :+).to_s
|
|
169
|
+
|
|
170
|
+
[status, headers, body]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
# Accepts a `Rack::Request` object, generates the presign, and returns a
|
|
176
|
+
# Rack response.
|
|
177
|
+
def handle_request(request)
|
|
178
|
+
location = get_presign_location(request)
|
|
179
|
+
options = get_presign_options(request)
|
|
180
|
+
|
|
181
|
+
presign = generate_presign(location, options, request)
|
|
182
|
+
|
|
183
|
+
make_response(presign, request)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Generates the location using `Shrine#generate_uid`, and extracts the
|
|
187
|
+
# extension from the `filename` query parameter. If `:presign_location`
|
|
188
|
+
# option is given, calls that instead.
|
|
189
|
+
def get_presign_location(request)
|
|
190
|
+
if @presign_location
|
|
191
|
+
@presign_location.call(request)
|
|
192
|
+
else
|
|
193
|
+
extension = File.extname(request.params["filename"].to_s)
|
|
194
|
+
uploader.send(:generate_uid, nil) + extension
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Calls `:presign_options` option block if given.
|
|
199
|
+
def get_presign_options(request)
|
|
200
|
+
options = @presign_options
|
|
201
|
+
options = options.call(request) if options.respond_to?(:call)
|
|
202
|
+
options || {}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Calls `#presign` on the storage, and returns the `url`, `fields`, and
|
|
206
|
+
# `headers` information in a serialializable format. If `:presign`
|
|
207
|
+
# option is given, calls that instead of calling `#presign`.
|
|
208
|
+
def generate_presign(location, options, request)
|
|
209
|
+
if @presign
|
|
210
|
+
presign = @presign.call(location, options, request)
|
|
211
|
+
else
|
|
212
|
+
presign = storage.presign(location, options)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
url = presign.url
|
|
216
|
+
fields = presign.fields
|
|
217
|
+
headers = presign.headers if presign.respond_to?(:headers)
|
|
218
|
+
|
|
219
|
+
{ url: url, fields: fields.to_h, headers: headers.to_h }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Transforms the presign hash into a JSON response. It returns a Rack
|
|
223
|
+
# response triple - an array consisting of a status number, hash of
|
|
224
|
+
# headers, and a body enumerable. If `:rack_response` option is given,
|
|
225
|
+
# calls that instead.
|
|
226
|
+
def make_response(object, request)
|
|
227
|
+
if @rack_response
|
|
228
|
+
response = @rack_response.call(object, request)
|
|
229
|
+
else
|
|
230
|
+
response = [200, {"Content-Type" => "application/json"}, [object.to_json]]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# prevent browsers from caching the response
|
|
234
|
+
response[1]["Cache-Control"] = "no-store" unless response[1].key?("Cache-Control")
|
|
235
|
+
|
|
236
|
+
response
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Used for early returning an error response.
|
|
240
|
+
def error!(status, message)
|
|
241
|
+
throw :halt, [status, {"Content-Type" => "text/plain"}, [message]]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns the uploader around the specified storage.
|
|
245
|
+
def uploader
|
|
246
|
+
@shrine_class.new(@storage_key)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Returns the storage object.
|
|
250
|
+
def storage
|
|
251
|
+
@shrine_class.find_storage(@storage_key)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
register_plugin(:presign_endpoint, PresignEndpoint)
|
|
257
|
+
end
|
|
258
|
+
end
|