shrine 2.16.0 → 2.17.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.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/doc/plugins/data_uri.md +9 -0
- data/doc/plugins/default_url.md +12 -0
- data/doc/plugins/derivation_endpoint.md +22 -5
- data/doc/plugins/determine_mime_type.md +34 -5
- data/doc/plugins/download_endpoint.md +9 -7
- data/doc/plugins/{metadata_attribues.md → metadata_attributes.md} +0 -0
- data/doc/plugins/remote_url.md +7 -1
- data/doc/release_notes/2.17.0.md +129 -0
- data/doc/storage/s3.md +3 -4
- data/lib/shrine/plugins/data_uri.rb +7 -2
- data/lib/shrine/plugins/default_url.rb +17 -5
- data/lib/shrine/plugins/derivation_endpoint.rb +43 -28
- data/lib/shrine/plugins/determine_mime_type.rb +20 -13
- data/lib/shrine/plugins/download_endpoint.rb +76 -84
- data/lib/shrine/plugins/metadata_attributes.rb +1 -1
- data/lib/shrine/plugins/parsed_json.rb +3 -2
- data/lib/shrine/plugins/presign_endpoint.rb +122 -118
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +46 -15
- data/lib/shrine/plugins/remote_url.rb +2 -2
- data/lib/shrine/plugins/upload_endpoint.rb +109 -105
- data/lib/shrine/plugins/versions.rb +2 -1
- data/lib/shrine/storage/file_system.rb +1 -1
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +1 -2
- metadata +6 -19
@@ -308,13 +308,25 @@ class Shrine
|
|
308
308
|
# serializes the source uploaded file into an URL-safe format
|
309
309
|
source_component = source.urlsafe_dump(metadata: metadata)
|
310
310
|
|
311
|
+
# generate plain URL
|
312
|
+
url = plain_url(name, *args, source_component, params)
|
313
|
+
|
311
314
|
# generate signed URL
|
312
|
-
signed_url(
|
315
|
+
signed_url(url)
|
316
|
+
end
|
317
|
+
|
318
|
+
def plain_url(*components, params)
|
319
|
+
# When using Rack < 2, Rack::Utils#escape_path will escape '/'.
|
320
|
+
# Escape each component and then join them together.
|
321
|
+
path = components.map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
|
322
|
+
query = Rack::Utils.build_query(params)
|
323
|
+
|
324
|
+
"#{path}?#{query}"
|
313
325
|
end
|
314
326
|
|
315
|
-
def signed_url(
|
327
|
+
def signed_url(url)
|
316
328
|
signer = UrlSigner.new(secret_key)
|
317
|
-
signer.
|
329
|
+
signer.sign_url(url)
|
318
330
|
end
|
319
331
|
end
|
320
332
|
|
@@ -383,6 +395,11 @@ class Shrine
|
|
383
395
|
[status, headers, body]
|
384
396
|
end
|
385
397
|
|
398
|
+
def inspect
|
399
|
+
"#<#{@shrine_class}::DerivationEndpoint>"
|
400
|
+
end
|
401
|
+
alias to_s inspect
|
402
|
+
|
386
403
|
private
|
387
404
|
|
388
405
|
# Return an error response if the signature is invalid.
|
@@ -449,19 +466,13 @@ class Shrine
|
|
449
466
|
|
450
467
|
status = response[0]
|
451
468
|
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
headers = {}
|
460
|
-
headers["Content-Type"] = content_type if content_type
|
461
|
-
headers["Content-Disposition"] = content_disposition(filename)
|
462
|
-
headers["Content-Length"] = content_length
|
463
|
-
headers["Content-Range"] = content_range if content_range
|
464
|
-
headers["Accept-Ranges"] = "bytes"
|
469
|
+
headers = {
|
470
|
+
"Content-Type" => type || response[1]["Content-Type"],
|
471
|
+
"Content-Length" => response[1]["Content-Length"],
|
472
|
+
"Content-Disposition" => content_disposition(file),
|
473
|
+
"Content-Range" => response[1]["Content-Range"],
|
474
|
+
"Accept-Ranges" => "bytes",
|
475
|
+
}.compact
|
465
476
|
|
466
477
|
body = Rack::BodyProxy.new(response[2]) { File.delete(file.path) }
|
467
478
|
|
@@ -523,7 +534,10 @@ class Shrine
|
|
523
534
|
|
524
535
|
# Returns disposition and filename formatted for the `Content-Disposition`
|
525
536
|
# header.
|
526
|
-
def content_disposition(
|
537
|
+
def content_disposition(file)
|
538
|
+
filename = self.filename
|
539
|
+
filename += File.extname(file.path) if File.extname(filename).empty?
|
540
|
+
|
527
541
|
ContentDisposition.format(disposition: disposition, filename: filename)
|
528
542
|
end
|
529
543
|
end
|
@@ -631,7 +645,8 @@ class Shrine
|
|
631
645
|
uploader.upload uploadable,
|
632
646
|
location: upload_location,
|
633
647
|
upload_options: upload_options,
|
634
|
-
delete: false # disable delete_raw plugin
|
648
|
+
delete: false, # disable delete_raw plugin
|
649
|
+
move: false # disable moving plugin
|
635
650
|
end
|
636
651
|
end
|
637
652
|
|
@@ -705,15 +720,14 @@ class Shrine
|
|
705
720
|
@secret_key = secret_key
|
706
721
|
end
|
707
722
|
|
708
|
-
# Returns a URL with the `signature` query parameter
|
709
|
-
|
710
|
-
|
711
|
-
path = Rack::Utils.escape_path(components.join("/"))
|
712
|
-
query = Rack::Utils.build_query(params)
|
723
|
+
# Returns a URL with the `signature` query parameter
|
724
|
+
def sign_url(url)
|
725
|
+
path, query = url.split("?")
|
713
726
|
|
714
|
-
|
727
|
+
params = Rack::Utils.parse_query(query.to_s)
|
728
|
+
params.merge!("signature" => generate_signature(url))
|
715
729
|
|
716
|
-
query = Rack::Utils.build_query(params
|
730
|
+
query = Rack::Utils.build_query(params)
|
717
731
|
|
718
732
|
"#{path}?#{query}"
|
719
733
|
end
|
@@ -722,12 +736,13 @@ class Shrine
|
|
722
736
|
# value in the `signature` query parameter. Raises `InvalidSignature` if
|
723
737
|
# the `signature` parameter is missing or its value doesn't match the
|
724
738
|
# calculated signature.
|
725
|
-
def verify_url(
|
726
|
-
path, query =
|
739
|
+
def verify_url(url)
|
740
|
+
path, query = url.split("?")
|
727
741
|
|
728
742
|
params = Rack::Utils.parse_query(query.to_s)
|
729
743
|
signature = params.delete("signature")
|
730
|
-
|
744
|
+
|
745
|
+
query = Rack::Utils.build_query(params)
|
731
746
|
|
732
747
|
verify_signature("#{path}?#{query}", signature)
|
733
748
|
end
|
@@ -13,6 +13,7 @@ class Shrine
|
|
13
13
|
end
|
14
14
|
|
15
15
|
uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
|
16
|
+
uploader.opts[:mime_type_analyzer_options] = opts.fetch(:analyzer_options, uploader.opts.fetch(:mime_type_analyzer_options, {}))
|
16
17
|
end
|
17
18
|
|
18
19
|
module ClassMethods
|
@@ -20,8 +21,13 @@ class Shrine
|
|
20
21
|
# analyzer.
|
21
22
|
def determine_mime_type(io)
|
22
23
|
analyzer = opts[:mime_type_analyzer]
|
24
|
+
|
23
25
|
analyzer = mime_type_analyzer(analyzer) if analyzer.is_a?(Symbol)
|
24
|
-
args =
|
26
|
+
args = if analyzer.is_a?(Proc)
|
27
|
+
[io, mime_type_analyzers].take(analyzer.arity.abs)
|
28
|
+
else
|
29
|
+
[io, opts[:mime_type_analyzer_options]]
|
30
|
+
end
|
25
31
|
|
26
32
|
mime_type = analyzer.call(*args)
|
27
33
|
io.rewind
|
@@ -40,7 +46,7 @@ class Shrine
|
|
40
46
|
|
41
47
|
# Returns callable mime type analyzer object.
|
42
48
|
def mime_type_analyzer(name)
|
43
|
-
MimeTypeAnalyzer.new(name)
|
49
|
+
MimeTypeAnalyzer.new(name)
|
44
50
|
end
|
45
51
|
end
|
46
52
|
|
@@ -68,8 +74,8 @@ class Shrine
|
|
68
74
|
@tool = tool
|
69
75
|
end
|
70
76
|
|
71
|
-
def call(io)
|
72
|
-
mime_type = send(:"extract_with_#{@tool}", io)
|
77
|
+
def call(io, options = {})
|
78
|
+
mime_type = send(:"extract_with_#{@tool}", io, options)
|
73
79
|
io.rewind
|
74
80
|
|
75
81
|
mime_type
|
@@ -77,7 +83,7 @@ class Shrine
|
|
77
83
|
|
78
84
|
private
|
79
85
|
|
80
|
-
def extract_with_file(io)
|
86
|
+
def extract_with_file(io, options)
|
81
87
|
require "open3"
|
82
88
|
|
83
89
|
return nil if io.eof? # file command returns "application/x-empty" for empty files
|
@@ -106,14 +112,14 @@ class Shrine
|
|
106
112
|
raise Error, "file command-line tool is not installed"
|
107
113
|
end
|
108
114
|
|
109
|
-
def extract_with_fastimage(io)
|
115
|
+
def extract_with_fastimage(io, options)
|
110
116
|
require "fastimage"
|
111
117
|
|
112
118
|
type = FastImage.type(io)
|
113
119
|
"image/#{type}" if type
|
114
120
|
end
|
115
121
|
|
116
|
-
def extract_with_filemagic(io)
|
122
|
+
def extract_with_filemagic(io, options)
|
117
123
|
require "filemagic"
|
118
124
|
|
119
125
|
return nil if io.eof? # FileMagic returns "application/x-empty" for empty files
|
@@ -123,22 +129,23 @@ class Shrine
|
|
123
129
|
end
|
124
130
|
end
|
125
131
|
|
126
|
-
def extract_with_mimemagic(io)
|
132
|
+
def extract_with_mimemagic(io, options)
|
127
133
|
require "mimemagic"
|
128
134
|
|
129
135
|
mime = MimeMagic.by_magic(io)
|
130
136
|
mime.type if mime
|
131
137
|
end
|
132
138
|
|
133
|
-
def extract_with_marcel(io)
|
139
|
+
def extract_with_marcel(io, options)
|
134
140
|
require "marcel"
|
135
141
|
|
136
142
|
return nil if io.eof? # marcel returns "application/octet-stream" for empty files
|
137
143
|
|
138
|
-
|
144
|
+
filename = (options[:filename_fallback] ? extract_filename(io) : nil)
|
145
|
+
Marcel::MimeType.for(io, name: filename)
|
139
146
|
end
|
140
147
|
|
141
|
-
def extract_with_mime_types(io)
|
148
|
+
def extract_with_mime_types(io, options)
|
142
149
|
require "mime/types"
|
143
150
|
|
144
151
|
if filename = extract_filename(io)
|
@@ -147,7 +154,7 @@ class Shrine
|
|
147
154
|
end
|
148
155
|
end
|
149
156
|
|
150
|
-
def extract_with_mini_mime(io)
|
157
|
+
def extract_with_mini_mime(io, options)
|
151
158
|
require "mini_mime"
|
152
159
|
|
153
160
|
if filename = extract_filename(io)
|
@@ -156,7 +163,7 @@ class Shrine
|
|
156
163
|
end
|
157
164
|
end
|
158
165
|
|
159
|
-
def extract_with_content_type(io)
|
166
|
+
def extract_with_content_type(io, options)
|
160
167
|
if io.respond_to?(:content_type) && io.content_type
|
161
168
|
io.content_type.split(";").first
|
162
169
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "roda"
|
4
|
-
|
5
3
|
class Shrine
|
6
4
|
module Plugins
|
7
5
|
# Documentation lives in [doc/plugins/download_endpoint.md] on GitHub.
|
@@ -14,14 +12,10 @@ class Shrine
|
|
14
12
|
end
|
15
13
|
|
16
14
|
def self.configure(uploader, opts = {})
|
17
|
-
uploader.opts[:
|
18
|
-
uploader.opts[:
|
19
|
-
uploader.opts[:download_endpoint_download_options] = opts.fetch(:download_options, uploader.opts.fetch(:download_endpoint_download_options, {}))
|
20
|
-
uploader.opts[:download_endpoint_disposition] = opts.fetch(:disposition, uploader.opts.fetch(:download_endpoint_disposition, "inline"))
|
21
|
-
uploader.opts[:download_endpoint_host] = opts.fetch(:host, uploader.opts[:download_endpoint_host])
|
22
|
-
uploader.opts[:download_endpoint_redirect] = opts.fetch(:redirect, uploader.opts.fetch(:download_endpoint_redirect, false))
|
15
|
+
uploader.opts[:download_endpoint] ||= { disposition: "inline", download_options: {} }
|
16
|
+
uploader.opts[:download_endpoint].merge!(opts)
|
23
17
|
|
24
|
-
Shrine.deprecation("The :storages download_endpoint option is deprecated, you should use UploadedFile#download_url for generating URLs to the download endpoint.") if uploader.opts[:
|
18
|
+
Shrine.deprecation("The :storages download_endpoint option is deprecated, you should use UploadedFile#download_url for generating URLs to the download endpoint.") if uploader.opts[:download_endpoint][:storages]
|
25
19
|
|
26
20
|
uploader.assign_download_endpoint(App) unless uploader.const_defined?(:DownloadEndpoint)
|
27
21
|
end
|
@@ -34,13 +28,13 @@ class Shrine
|
|
34
28
|
end
|
35
29
|
|
36
30
|
# Returns the Rack application that retrieves requested files.
|
37
|
-
def download_endpoint
|
38
|
-
new_download_endpoint(App)
|
31
|
+
def download_endpoint(**options)
|
32
|
+
new_download_endpoint(App, **options)
|
39
33
|
end
|
40
34
|
|
41
35
|
# Assigns the subclassed endpoint as the `DownloadEndpoint` constant.
|
42
|
-
def assign_download_endpoint(
|
43
|
-
@download_endpoint = new_download_endpoint(
|
36
|
+
def assign_download_endpoint(app_class)
|
37
|
+
@download_endpoint = new_download_endpoint(app_class)
|
44
38
|
|
45
39
|
const_set(:DownloadEndpoint, @download_endpoint)
|
46
40
|
deprecate_constant(:DownloadEndpoint)
|
@@ -48,13 +42,12 @@ class Shrine
|
|
48
42
|
|
49
43
|
private
|
50
44
|
|
51
|
-
def new_download_endpoint(
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
endpoint_class
|
45
|
+
def new_download_endpoint(app_class, **options)
|
46
|
+
app_class.new(
|
47
|
+
shrine_class: self,
|
48
|
+
**opts[:download_endpoint],
|
49
|
+
**options,
|
50
|
+
)
|
58
51
|
end
|
59
52
|
end
|
60
53
|
|
@@ -79,7 +72,7 @@ class Shrine
|
|
79
72
|
private
|
80
73
|
|
81
74
|
def download_storages
|
82
|
-
shrine_class.opts[:
|
75
|
+
shrine_class.opts[:download_endpoint][:storages]
|
83
76
|
end
|
84
77
|
end
|
85
78
|
|
@@ -101,88 +94,110 @@ class Shrine
|
|
101
94
|
end
|
102
95
|
|
103
96
|
def host
|
104
|
-
|
97
|
+
options[:host]
|
105
98
|
end
|
106
99
|
|
107
100
|
def prefix
|
108
|
-
|
101
|
+
options[:prefix]
|
109
102
|
end
|
110
103
|
|
111
|
-
def
|
112
|
-
file.shrine_class
|
104
|
+
def options
|
105
|
+
file.shrine_class.opts[:download_endpoint]
|
113
106
|
end
|
114
107
|
end
|
115
108
|
|
116
109
|
# Routes incoming requests. It first asserts that the storage is existent
|
117
110
|
# and allowed. Afterwards it proceeds with the file download using
|
118
111
|
# streaming.
|
119
|
-
class App
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
uploaded_file = shrine_class::UploadedFile.new(
|
125
|
-
"id" => id,
|
126
|
-
"storage" => storage_name,
|
127
|
-
)
|
128
|
-
|
129
|
-
serve_file(uploaded_file)
|
130
|
-
end
|
112
|
+
class App
|
113
|
+
# Writes given options to instance variables.
|
114
|
+
def initialize(options)
|
115
|
+
options.each do |name, value|
|
116
|
+
instance_variable_set("@#{name}", value)
|
131
117
|
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def call(env)
|
121
|
+
request = Rack::Request.new(env)
|
132
122
|
|
133
|
-
|
134
|
-
|
135
|
-
|
123
|
+
status, headers, body = catch(:halt) do
|
124
|
+
error!(405, "Method Not Allowed") unless request.get?
|
125
|
+
|
126
|
+
handle_request(request)
|
136
127
|
end
|
128
|
+
|
129
|
+
headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
|
130
|
+
|
131
|
+
[status, headers, body]
|
137
132
|
end
|
138
133
|
|
134
|
+
def inspect
|
135
|
+
"#<#{@shrine_class}::DownloadEndpoint>"
|
136
|
+
end
|
137
|
+
alias to_s inspect
|
138
|
+
|
139
139
|
private
|
140
140
|
|
141
|
+
def handle_request(request)
|
142
|
+
_, *components = request.path_info.split("/")
|
143
|
+
|
144
|
+
if components.count == 1
|
145
|
+
uploaded_file = get_uploaded_file(components.first)
|
146
|
+
elsif components.count == 2
|
147
|
+
# handle legacy "/:storage/:id" URLs
|
148
|
+
uploaded_file = @shrine_class::UploadedFile.new(
|
149
|
+
"storage" => components.first,
|
150
|
+
"id" => components.last,
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
serve_file(uploaded_file, request)
|
155
|
+
end
|
156
|
+
|
141
157
|
# Streams or redirects to the uploaded file.
|
142
|
-
def serve_file(uploaded_file)
|
143
|
-
if redirect
|
144
|
-
redirect_to_file(uploaded_file)
|
158
|
+
def serve_file(uploaded_file, request)
|
159
|
+
if @redirect
|
160
|
+
redirect_to_file(uploaded_file, request)
|
145
161
|
else
|
146
|
-
stream_file(uploaded_file)
|
162
|
+
stream_file(uploaded_file, request)
|
147
163
|
end
|
148
164
|
end
|
149
165
|
|
150
166
|
# Streams the uploaded file content.
|
151
|
-
def stream_file(uploaded_file)
|
152
|
-
open_file(uploaded_file)
|
167
|
+
def stream_file(uploaded_file, request)
|
168
|
+
open_file(uploaded_file, request)
|
153
169
|
|
154
170
|
response = uploaded_file.to_rack_response(
|
155
|
-
disposition: disposition,
|
156
|
-
range: env["HTTP_RANGE"],
|
171
|
+
disposition: @disposition,
|
172
|
+
range: request.env["HTTP_RANGE"],
|
157
173
|
)
|
158
174
|
|
159
175
|
response[1]["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
|
160
176
|
|
161
|
-
|
177
|
+
response
|
162
178
|
end
|
163
179
|
|
164
180
|
# Redirects to the uploaded file's direct URL or the specified URL proc.
|
165
|
-
def redirect_to_file(uploaded_file)
|
166
|
-
if redirect == true
|
181
|
+
def redirect_to_file(uploaded_file, request)
|
182
|
+
if @redirect == true
|
167
183
|
redirect_url = uploaded_file.url
|
168
184
|
else
|
169
|
-
redirect_url = redirect.call(uploaded_file, request)
|
185
|
+
redirect_url = @redirect.call(uploaded_file, request)
|
170
186
|
end
|
171
187
|
|
172
|
-
|
188
|
+
[302, { "Location" => redirect_url }, []]
|
173
189
|
end
|
174
190
|
|
175
|
-
def open_file(uploaded_file)
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
end
|
191
|
+
def open_file(uploaded_file, request)
|
192
|
+
download_options = @download_options
|
193
|
+
download_options = download_options.call(uploaded_file, request) if download_options.respond_to?(:call)
|
194
|
+
|
195
|
+
uploaded_file.open(**download_options)
|
181
196
|
end
|
182
197
|
|
183
198
|
# Returns a Shrine::UploadedFile, or returns 404 if file doesn't exist.
|
184
199
|
def get_uploaded_file(serialized)
|
185
|
-
uploaded_file = shrine_class::UploadedFile.urlsafe_load(serialized)
|
200
|
+
uploaded_file = @shrine_class::UploadedFile.urlsafe_load(serialized)
|
186
201
|
not_found! unless uploaded_file.exists?
|
187
202
|
uploaded_file
|
188
203
|
rescue Shrine::Error # storage not found
|
@@ -195,30 +210,7 @@ class Shrine
|
|
195
210
|
|
196
211
|
# Halts the request with the error message.
|
197
212
|
def error!(status, message)
|
198
|
-
|
199
|
-
response["Content-Type"] = "text/plain"
|
200
|
-
response.write(message)
|
201
|
-
request.halt
|
202
|
-
end
|
203
|
-
|
204
|
-
def download_options
|
205
|
-
opts[:download_options]
|
206
|
-
end
|
207
|
-
|
208
|
-
def redirect
|
209
|
-
opts[:redirect]
|
210
|
-
end
|
211
|
-
|
212
|
-
def disposition
|
213
|
-
opts[:disposition]
|
214
|
-
end
|
215
|
-
|
216
|
-
def shrine_class
|
217
|
-
opts[:shrine_class]
|
218
|
-
end
|
219
|
-
|
220
|
-
def storage_names
|
221
|
-
shrine_class.storages.keys.map(&:to_s)
|
213
|
+
throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
|
222
214
|
end
|
223
215
|
end
|
224
216
|
end
|