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.

@@ -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(name, *args, source_component, params)
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(*components)
327
+ def signed_url(url)
316
328
  signer = UrlSigner.new(secret_key)
317
- signer.signed_url(*components)
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
- content_type = type || response[1]["Content-Type"]
453
- content_length = response[1]["Content-Length"]
454
- content_range = response[1]["Content-Range"]
455
-
456
- filename = self.filename
457
- filename += File.extname(file.path) if File.extname(filename).empty?
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(filename)
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 generated from the
709
- # given path components and query parameters.
710
- def signed_url(*components, params)
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
- signature = generate_signature("#{path}?#{query}")
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.merge(signature: signature))
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(path_with_query)
726
- path, query = path_with_query.split("?")
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
- query = Rack::Utils.build_query(params)
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 = [io, mime_type_analyzers].take(analyzer.arity.abs)
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).method(:call)
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
- Marcel::MimeType.for(io, name: extract_filename(io))
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[:download_endpoint_storages] = opts.fetch(:storages, uploader.opts[:download_endpoint_storages])
18
- uploader.opts[:download_endpoint_prefix] = opts.fetch(:prefix, uploader.opts[:download_endpoint_prefix])
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[:download_endpoint_storages]
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(klass)
43
- @download_endpoint = new_download_endpoint(klass)
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(klass)
52
- endpoint_class = Class.new(klass)
53
- endpoint_class.opts[:shrine_class] = self
54
- endpoint_class.opts[:download_options] = opts[:download_endpoint_download_options]
55
- endpoint_class.opts[:disposition] = opts[:download_endpoint_disposition]
56
- endpoint_class.opts[:redirect] = opts[:download_endpoint_redirect]
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[:download_endpoint_storages]
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
- shrine_class.opts[:download_endpoint_host]
97
+ options[:host]
105
98
  end
106
99
 
107
100
  def prefix
108
- shrine_class.opts[:download_endpoint_prefix]
101
+ options[:prefix]
109
102
  end
110
103
 
111
- def shrine_class
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 < Roda
120
- route do |r|
121
- # handle legacy ":storage/:id" URLs
122
- r.on storage_names do |storage_name|
123
- r.get /(.*)/ do |id|
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
- r.get /(.*)/ do |serialized|
134
- uploaded_file = get_uploaded_file(serialized)
135
- serve_file(uploaded_file)
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
- request.halt response
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
- request.redirect redirect_url
188
+ [302, { "Location" => redirect_url }, []]
173
189
  end
174
190
 
175
- def open_file(uploaded_file)
176
- if download_options.respond_to?(:call)
177
- uploaded_file.open(**download_options.call(uploaded_file, request))
178
- else
179
- uploaded_file.open(**download_options)
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
- response.status = status
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