shrine 3.4.0 → 3.10.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/CHANGELOG.md +96 -0
- data/README.md +11 -11
- data/doc/advantages.md +1 -1
- data/doc/attacher.md +1 -1
- data/doc/carrierwave.md +2 -2
- data/doc/changing_derivatives.md +2 -1
- data/doc/changing_location.md +19 -6
- data/doc/design.md +4 -4
- data/doc/external/articles.md +16 -16
- data/doc/external/extensions.md +1 -1
- data/doc/getting_started.md +73 -30
- data/doc/multiple_files.md +57 -22
- data/doc/plugins/activerecord.md +91 -0
- data/doc/plugins/backgrounding.md +30 -4
- data/doc/plugins/data_uri.md +15 -0
- data/doc/plugins/derivation_endpoint.md +39 -1
- data/doc/plugins/derivatives.md +13 -2
- data/doc/plugins/download_endpoint.md +16 -4
- data/doc/plugins/dynamic_storage.md +15 -0
- data/doc/plugins/fallback_storage.md +90 -0
- data/doc/plugins/keep_files.md +6 -4
- data/doc/plugins/rack_response.md +9 -0
- data/doc/plugins/refresh_metadata.md +20 -0
- data/doc/plugins/sequel.md +57 -0
- data/doc/plugins/signature.md +8 -6
- data/doc/plugins/tempfile.md +14 -6
- data/doc/plugins/upload_options.md +12 -0
- data/doc/plugins/url_options.md +12 -0
- data/doc/plugins/validation_helpers.md +1 -1
- data/doc/processing.md +5 -3
- data/doc/refile.md +3 -3
- data/doc/release_notes/2.1.0.md +1 -1
- data/doc/release_notes/3.10.0.md +30 -0
- data/doc/release_notes/3.5.0.md +63 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/release_notes/3.7.0.md +75 -0
- data/doc/release_notes/3.7.1.md +31 -0
- data/doc/release_notes/3.8.0.md +37 -0
- data/doc/release_notes/3.9.0.md +42 -0
- data/doc/retrieving_uploads.md +1 -1
- data/doc/storage/s3.md +10 -0
- data/doc/testing.md +45 -17
- data/doc/upgrading_to_3.md +3 -5
- data/lib/shrine/attacher.rb +41 -29
- data/lib/shrine/attachment.rb +2 -2
- data/lib/shrine/plugins/_persistence.rb +1 -1
- data/lib/shrine/plugins/_urlsafe_serialization.rb +4 -4
- data/lib/shrine/plugins/activerecord.rb +25 -4
- data/lib/shrine/plugins/add_metadata.rb +2 -4
- data/lib/shrine/plugins/atomic_helpers.rb +7 -7
- data/lib/shrine/plugins/backgrounding.rb +10 -10
- data/lib/shrine/plugins/column.rb +7 -5
- data/lib/shrine/plugins/data_uri.rb +28 -3
- data/lib/shrine/plugins/default_url.rb +4 -4
- data/lib/shrine/plugins/delete_raw.rb +2 -2
- data/lib/shrine/plugins/derivation_endpoint.rb +65 -59
- data/lib/shrine/plugins/derivatives.rb +35 -21
- data/lib/shrine/plugins/download_endpoint.rb +72 -11
- data/lib/shrine/plugins/entity.rb +15 -9
- data/lib/shrine/plugins/fallback_storage.rb +51 -0
- data/lib/shrine/plugins/infer_extension.rb +5 -1
- data/lib/shrine/plugins/instrumentation.rb +13 -9
- data/lib/shrine/plugins/metadata_attributes.rb +1 -1
- data/lib/shrine/plugins/mirroring.rb +10 -10
- data/lib/shrine/plugins/model.rb +12 -10
- data/lib/shrine/plugins/presign_endpoint.rb +13 -10
- data/lib/shrine/plugins/pretty_location.rb +2 -2
- data/lib/shrine/plugins/processing.rb +3 -3
- data/lib/shrine/plugins/rack_file.rb +2 -2
- data/lib/shrine/plugins/rack_response.rb +12 -6
- data/lib/shrine/plugins/refresh_metadata.rb +6 -6
- data/lib/shrine/plugins/remote_url.rb +4 -4
- data/lib/shrine/plugins/remove_attachment.rb +2 -0
- data/lib/shrine/plugins/remove_invalid.rb +4 -0
- data/lib/shrine/plugins/restore_cached_data.rb +3 -3
- data/lib/shrine/plugins/sequel.rb +15 -1
- data/lib/shrine/plugins/signature.rb +2 -2
- data/lib/shrine/plugins/store_dimensions.rb +2 -2
- data/lib/shrine/plugins/tempfile.rb +0 -2
- data/lib/shrine/plugins/upload_endpoint.rb +7 -5
- data/lib/shrine/plugins/upload_options.rb +18 -4
- data/lib/shrine/plugins/url_options.rb +17 -3
- data/lib/shrine/plugins/validation.rb +8 -8
- data/lib/shrine/plugins/validation_helpers.rb +2 -2
- data/lib/shrine/plugins/versions.rb +10 -10
- data/lib/shrine/plugins.rb +6 -14
- data/lib/shrine/storage/file_system.rb +15 -19
- data/lib/shrine/storage/linter.rb +8 -8
- data/lib/shrine/storage/memory.rb +1 -3
- data/lib/shrine/storage/s3.rb +60 -40
- data/lib/shrine/uploaded_file.rb +23 -19
- data/lib/shrine/version.rb +1 -1
- data/lib/shrine.rb +24 -20
- data/shrine.gemspec +10 -8
- metadata +52 -26
|
@@ -15,8 +15,8 @@ class Shrine
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
# Deletes the file that was uploaded, unless it's an UploadedFile.
|
|
18
|
-
def _upload(io, delete: delete_raw?, **
|
|
19
|
-
super(io, delete
|
|
18
|
+
def _upload(io, delete: delete_raw?, **)
|
|
19
|
+
super(io, delete:, **)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def delete_raw?
|
|
@@ -28,8 +28,8 @@ class Shrine
|
|
|
28
28
|
uploader.opts[:derivation_endpoint] ||= { options: {}, derivations: {} }
|
|
29
29
|
uploader.opts[:derivation_endpoint][:options].merge!(opts)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
fail Error, "must provide :secret_key option to derivation_endpoint plugin"
|
|
31
|
+
if !uploader.opts[:derivation_endpoint][:options][:secret_key] && !uploader.opts[:derivation_endpoint][:options][:signer]
|
|
32
|
+
fail Error, "must provide :secret_key option to derivation_endpoint plugin when no custom signer is set"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# instrumentation plugin integration
|
|
@@ -48,7 +48,7 @@ class Shrine
|
|
|
48
48
|
# It uses a trick where it removes the derivation path prefix from the
|
|
49
49
|
# path info before calling the Rack app, which is what web framework
|
|
50
50
|
# routers do before they're calling a mounted Rack app.
|
|
51
|
-
def derivation_response(env, **
|
|
51
|
+
def derivation_response(env, **)
|
|
52
52
|
script_name = env["SCRIPT_NAME"]
|
|
53
53
|
path_info = env["PATH_INFO"]
|
|
54
54
|
|
|
@@ -61,7 +61,7 @@ class Shrine
|
|
|
61
61
|
env["SCRIPT_NAME"] += match.to_s
|
|
62
62
|
env["PATH_INFO"] = match.post_match
|
|
63
63
|
|
|
64
|
-
derivation_endpoint(**
|
|
64
|
+
derivation_endpoint(**).call(env)
|
|
65
65
|
ensure
|
|
66
66
|
env["SCRIPT_NAME"] = script_name
|
|
67
67
|
env["PATH_INFO"] = path_info
|
|
@@ -87,15 +87,15 @@ class Shrine
|
|
|
87
87
|
# Generates a URL to a derivation with the receiver as the source file.
|
|
88
88
|
# Any arguments provided will be included in the URL and passed to the
|
|
89
89
|
# derivation block. Accepts additional URL options.
|
|
90
|
-
def derivation_url(name,
|
|
91
|
-
derivation(name, *
|
|
90
|
+
def derivation_url(name, *, **)
|
|
91
|
+
derivation(name, *).url(**)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Calls the specified derivation with the receiver as the source file,
|
|
95
95
|
# returning a Rack response triple. The derivation endpoint ultimately
|
|
96
96
|
# calls this method.
|
|
97
|
-
def derivation_response(name,
|
|
98
|
-
derivation(name,
|
|
97
|
+
def derivation_response(name, *, env:, **)
|
|
98
|
+
derivation(name, *, **).response(env)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Returns a Shrine::Derivation object created from the provided
|
|
@@ -129,14 +129,14 @@ class Shrine
|
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
# Returns an URL to the derivation.
|
|
132
|
-
def url(**
|
|
132
|
+
def url(**)
|
|
133
133
|
Derivation::Url.new(self).call(
|
|
134
134
|
host: option(:host),
|
|
135
135
|
prefix: option(:prefix),
|
|
136
136
|
expires_in: option(:expires_in),
|
|
137
137
|
version: option(:version),
|
|
138
138
|
metadata: option(:metadata),
|
|
139
|
-
|
|
139
|
+
**,
|
|
140
140
|
)
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -158,8 +158,8 @@ class Shrine
|
|
|
158
158
|
|
|
159
159
|
# Uploads the derivation result to a dedicated destination on the specified
|
|
160
160
|
# Shrine storage.
|
|
161
|
-
def upload(file = nil, **
|
|
162
|
-
Derivation::Upload.new(self).call(file, **
|
|
161
|
+
def upload(file = nil, **)
|
|
162
|
+
Derivation::Upload.new(self).call(file, **)
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
# Returns a Shrine::UploadedFile object pointing to the uploaded derivative
|
|
@@ -184,7 +184,7 @@ class Shrine
|
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
def self.option(name, default: nil, result: nil)
|
|
187
|
-
options[name] = { default
|
|
187
|
+
options[name] = { default:, result: }
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
option :cache_control, default: -> { default_cache_control }
|
|
@@ -197,6 +197,7 @@ class Shrine
|
|
|
197
197
|
option :metadata, default: -> { [] }
|
|
198
198
|
option :prefix
|
|
199
199
|
option :secret_key
|
|
200
|
+
option :signer
|
|
200
201
|
option :type
|
|
201
202
|
option :upload, default: -> { false }
|
|
202
203
|
option :upload_location, default: -> { default_upload_location }, result: -> (o) { upload_location(o) }
|
|
@@ -216,7 +217,7 @@ class Shrine
|
|
|
216
217
|
option_definition = self.class.options.fetch(name)
|
|
217
218
|
|
|
218
219
|
value = options.fetch(name) { shrine_class.derivation_options[name] }
|
|
219
|
-
value = instance_exec(&value) if value.is_a?(Proc)
|
|
220
|
+
value = instance_exec(&value) if value.is_a?(Proc) && value.arity == 0
|
|
220
221
|
|
|
221
222
|
if value.nil?
|
|
222
223
|
default = option_definition[:default]
|
|
@@ -300,20 +301,37 @@ class Shrine
|
|
|
300
301
|
end
|
|
301
302
|
|
|
302
303
|
class Derivation::Url < Derivation::Command
|
|
303
|
-
delegate :name, :args, :source, :secret_key
|
|
304
|
+
delegate :name, :args, :source, :secret_key, :signer
|
|
304
305
|
|
|
305
|
-
def call(host: nil, prefix: nil, **
|
|
306
|
-
[host, *prefix
|
|
306
|
+
def call(host: nil, prefix: nil, metadata: [], format: nil, **)
|
|
307
|
+
base_url = [host, *prefix].join("/")
|
|
308
|
+
path = path_identifier(metadata:, format:)
|
|
309
|
+
|
|
310
|
+
if signer
|
|
311
|
+
url = [base_url, path].join("/")
|
|
312
|
+
signer.call(url, **)
|
|
313
|
+
else
|
|
314
|
+
signed_part = signed_url("#{path}?#{query(**)}")
|
|
315
|
+
[base_url, signed_part].join("/")
|
|
316
|
+
end
|
|
307
317
|
end
|
|
308
318
|
|
|
309
319
|
private
|
|
310
320
|
|
|
311
|
-
def
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
def path_identifier(metadata: [], format: nil)
|
|
322
|
+
path = [
|
|
323
|
+
name,
|
|
324
|
+
*args,
|
|
325
|
+
source.urlsafe_dump(metadata:)
|
|
326
|
+
].map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
|
|
327
|
+
format ? "#{path}.#{format}" : path
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def query(expires_in: nil,
|
|
331
|
+
type: nil,
|
|
332
|
+
filename: nil,
|
|
333
|
+
disposition: nil,
|
|
334
|
+
version: nil)
|
|
317
335
|
|
|
318
336
|
params = {}
|
|
319
337
|
params[:expires_at] = (Time.now + expires_in).to_i if expires_in
|
|
@@ -322,23 +340,7 @@ class Shrine
|
|
|
322
340
|
params[:filename] = filename if filename
|
|
323
341
|
params[:disposition] = disposition if disposition
|
|
324
342
|
|
|
325
|
-
|
|
326
|
-
source_component = source.urlsafe_dump(metadata: metadata)
|
|
327
|
-
|
|
328
|
-
# generate plain URL
|
|
329
|
-
url = plain_url(name, *args, source_component, params)
|
|
330
|
-
|
|
331
|
-
# generate signed URL
|
|
332
|
-
signed_url(url)
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def plain_url(*components, params)
|
|
336
|
-
# When using Rack < 2, Rack::Utils#escape_path will escape '/'.
|
|
337
|
-
# Escape each component and then join them together.
|
|
338
|
-
path = components.map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
|
|
339
|
-
query = Rack::Utils.build_query(params)
|
|
340
|
-
|
|
341
|
-
"#{path}?#{query}"
|
|
343
|
+
Rack::Utils.build_query(params)
|
|
342
344
|
end
|
|
343
345
|
|
|
344
346
|
def signed_url(url)
|
|
@@ -364,7 +366,9 @@ class Shrine
|
|
|
364
366
|
handle_request(request)
|
|
365
367
|
end
|
|
366
368
|
|
|
367
|
-
headers[
|
|
369
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
|
370
|
+
headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
|
|
371
|
+
body.map(&:bytesize).inject(0, :+).to_s
|
|
368
372
|
|
|
369
373
|
[status, headers, body]
|
|
370
374
|
end
|
|
@@ -379,10 +383,11 @@ class Shrine
|
|
|
379
383
|
# Returns "404 Not Found" if derivation block is not defined, or if source
|
|
380
384
|
# file was not found on the storage.
|
|
381
385
|
def handle_request(request)
|
|
382
|
-
verify_signature!(request)
|
|
386
|
+
verify_signature!(request) if secret_key
|
|
383
387
|
check_expiry!(request)
|
|
384
388
|
|
|
385
389
|
name, *args, serialized_file = request.path_info.split("/")[1..-1]
|
|
390
|
+
serialized_file = serialized_file.sub(/\.\w+$/, "")
|
|
386
391
|
|
|
387
392
|
name = name.to_sym
|
|
388
393
|
uploaded_file = shrine_class::UploadedFile.urlsafe_load(serialized_file)
|
|
@@ -480,19 +485,18 @@ class Shrine
|
|
|
480
485
|
# `Content-Type` and `Content-Disposition` response headers from derivation
|
|
481
486
|
# options and file extension of the derivation result.
|
|
482
487
|
def file_response(file, env)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
status = response[0]
|
|
488
|
+
status, headers, body = rack_file_response(file.path, env)
|
|
486
489
|
|
|
490
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
|
487
491
|
headers = {
|
|
488
|
-
"Content-Type" => type ||
|
|
489
|
-
"Content-Length" =>
|
|
492
|
+
"Content-Type" => type || headers["Content-Type"],
|
|
493
|
+
"Content-Length" => headers["Content-Length"],
|
|
490
494
|
"Content-Disposition" => content_disposition(file),
|
|
491
|
-
"Content-Range" =>
|
|
495
|
+
"Content-Range" => headers["Content-Range"],
|
|
492
496
|
"Accept-Ranges" => "bytes",
|
|
493
497
|
}.compact
|
|
494
498
|
|
|
495
|
-
body = Rack::BodyProxy.new(
|
|
499
|
+
body = Rack::BodyProxy.new(body) { File.delete(file.path) }
|
|
496
500
|
|
|
497
501
|
file.close
|
|
498
502
|
|
|
@@ -513,8 +517,10 @@ class Shrine
|
|
|
513
517
|
|
|
514
518
|
if upload_redirect
|
|
515
519
|
redirect_url = uploaded_file.url(**upload_redirect_url_options)
|
|
520
|
+
headers = { "Location" => redirect_url }
|
|
521
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
|
516
522
|
|
|
517
|
-
[302,
|
|
523
|
+
[302, headers, []]
|
|
518
524
|
else
|
|
519
525
|
if derivative && File.exist?(derivative.path)
|
|
520
526
|
file_response(derivative, env)
|
|
@@ -554,7 +560,7 @@ class Shrine
|
|
|
554
560
|
filename = self.filename
|
|
555
561
|
filename += File.extname(file.path) if File.extname(filename).empty?
|
|
556
562
|
|
|
557
|
-
ContentDisposition.format(disposition
|
|
563
|
+
ContentDisposition.format(disposition:, filename:)
|
|
558
564
|
end
|
|
559
565
|
end
|
|
560
566
|
|
|
@@ -593,9 +599,9 @@ class Shrine
|
|
|
593
599
|
end
|
|
594
600
|
|
|
595
601
|
# Calls the derivation block.
|
|
596
|
-
def derive(*
|
|
602
|
+
def derive(*)
|
|
597
603
|
instrument_derivation do
|
|
598
|
-
derivation.instance_exec(
|
|
604
|
+
derivation.instance_exec(*, &derivation_block)
|
|
599
605
|
end
|
|
600
606
|
end
|
|
601
607
|
|
|
@@ -603,7 +609,7 @@ class Shrine
|
|
|
603
609
|
def instrument_derivation(&block)
|
|
604
610
|
return yield unless shrine_class.respond_to?(:instrument)
|
|
605
611
|
|
|
606
|
-
shrine_class.instrument(:derivation, { derivation:
|
|
612
|
+
shrine_class.instrument(:derivation, { derivation: }, &block)
|
|
607
613
|
end
|
|
608
614
|
|
|
609
615
|
# Massages the derivation result, ensuring it's opened in binary mode,
|
|
@@ -642,22 +648,22 @@ class Shrine
|
|
|
642
648
|
# Uploads the derivation result to the dedicated location on the storage.
|
|
643
649
|
# If a file object is given, uploads that to the storage, otherwise calls
|
|
644
650
|
# the derivation block and uploads the result.
|
|
645
|
-
def call(derivative = nil, **
|
|
651
|
+
def call(derivative = nil, **)
|
|
646
652
|
if derivative
|
|
647
|
-
upload(derivative, **
|
|
653
|
+
upload(derivative, **)
|
|
648
654
|
else
|
|
649
|
-
upload(derivation.generate, delete: true, **
|
|
655
|
+
upload(derivation.generate, delete: true, **)
|
|
650
656
|
end
|
|
651
657
|
end
|
|
652
658
|
|
|
653
659
|
private
|
|
654
660
|
|
|
655
|
-
def upload(io, **
|
|
661
|
+
def upload(io, **)
|
|
656
662
|
shrine_class.upload io, upload_storage,
|
|
657
663
|
location: upload_location,
|
|
658
664
|
upload_options: upload_options,
|
|
659
665
|
action: :derivation,
|
|
660
|
-
**
|
|
666
|
+
**
|
|
661
667
|
end
|
|
662
668
|
end
|
|
663
669
|
|
|
@@ -12,18 +12,18 @@ class Shrine
|
|
|
12
12
|
}.inspect}"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.load_dependencies(uploader,
|
|
15
|
+
def self.load_dependencies(uploader, **)
|
|
16
16
|
uploader.plugin :default_url
|
|
17
|
-
|
|
18
|
-
AttacherMethods.prepend(VersionsCompatibility) if versions_compatibility
|
|
19
17
|
end
|
|
20
18
|
|
|
21
|
-
def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
|
|
22
|
-
uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key } }
|
|
19
|
+
def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, versions_compatibility: false, **opts)
|
|
20
|
+
uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key }, mutex: true }
|
|
23
21
|
uploader.opts[:derivatives].merge!(opts)
|
|
24
22
|
|
|
25
23
|
# instrumentation plugin integration
|
|
26
24
|
uploader.subscribe(:derivatives, &log_subscriber) if uploader.respond_to?(:subscribe)
|
|
25
|
+
|
|
26
|
+
uploader::Attacher.include(VersionsCompatibility) if versions_compatibility
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
module AttachmentMethods
|
|
@@ -109,7 +109,7 @@ class Shrine
|
|
|
109
109
|
super(**options)
|
|
110
110
|
|
|
111
111
|
@derivatives = derivatives
|
|
112
|
-
@derivatives_mutex = Mutex.new
|
|
112
|
+
@derivatives_mutex = Mutex.new if shrine_class.derivatives_options[:mutex]
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
# Convenience method for accessing derivatives.
|
|
@@ -139,7 +139,7 @@ class Shrine
|
|
|
139
139
|
# Allows generating a URL to the derivative by passing the derivative
|
|
140
140
|
# name.
|
|
141
141
|
#
|
|
142
|
-
# attacher.add_derivatives(thumb: thumb)
|
|
142
|
+
# attacher.add_derivatives({ thumb: thumb })
|
|
143
143
|
# attacher.url(:thumb) #=> "https://example.org/thumb.jpg"
|
|
144
144
|
def url(*path, **options)
|
|
145
145
|
return super if path.empty?
|
|
@@ -161,7 +161,7 @@ class Shrine
|
|
|
161
161
|
# attacher.stored?(attacher.derivatives[:thumb]) #=> true
|
|
162
162
|
def promote(**options)
|
|
163
163
|
super
|
|
164
|
-
promote_derivatives
|
|
164
|
+
promote_derivatives(**options)
|
|
165
165
|
create_derivatives if create_derivatives_on_promote?
|
|
166
166
|
end
|
|
167
167
|
|
|
@@ -180,7 +180,7 @@ class Shrine
|
|
|
180
180
|
|
|
181
181
|
# In addition to deleting the main file it also deletes any derivatives.
|
|
182
182
|
#
|
|
183
|
-
# attacher.add_derivatives(thumb: thumb)
|
|
183
|
+
# attacher.add_derivatives({ thumb: thumb })
|
|
184
184
|
# attacher.derivatives[:thumb].exists? #=> true
|
|
185
185
|
# attacher.destroy
|
|
186
186
|
# attacher.derivatives[:thumb].exists? #=> false
|
|
@@ -208,7 +208,7 @@ class Shrine
|
|
|
208
208
|
# # {
|
|
209
209
|
# # thumb: #<Shrine::UploadedFile>,
|
|
210
210
|
# # }
|
|
211
|
-
# attacher.add_derivatives(cropped: cropped)
|
|
211
|
+
# attacher.add_derivatives({ cropped: cropped })
|
|
212
212
|
# attacher.derivatives #=>
|
|
213
213
|
# # {
|
|
214
214
|
# # thumb: #<Shrine::UploadedFile>,
|
|
@@ -239,7 +239,7 @@ class Shrine
|
|
|
239
239
|
|
|
240
240
|
# Uploads given hash of files.
|
|
241
241
|
#
|
|
242
|
-
# hash = attacher.upload_derivatives(thumb: thumb)
|
|
242
|
+
# hash = attacher.upload_derivatives({ thumb: thumb })
|
|
243
243
|
# hash[:thumb] #=> #<Shrine::UploadedFile>
|
|
244
244
|
def upload_derivatives(files, **options)
|
|
245
245
|
map_derivative(files) do |path, file|
|
|
@@ -298,10 +298,10 @@ class Shrine
|
|
|
298
298
|
# Deep merges given uploaded derivatives with current derivatives.
|
|
299
299
|
#
|
|
300
300
|
# attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
|
|
301
|
-
# attacher.merge_derivatives(two: uploaded_file)
|
|
301
|
+
# attacher.merge_derivatives({ two: uploaded_file })
|
|
302
302
|
# attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
|
|
303
303
|
def merge_derivatives(new_derivatives)
|
|
304
|
-
|
|
304
|
+
derivatives_synchronize do
|
|
305
305
|
merged_derivatives = deep_merge_derivatives(derivatives, new_derivatives)
|
|
306
306
|
set_derivatives(merged_derivatives)
|
|
307
307
|
end
|
|
@@ -379,7 +379,7 @@ class Shrine
|
|
|
379
379
|
|
|
380
380
|
# Deletes given hash of uploaded files.
|
|
381
381
|
#
|
|
382
|
-
# attacher.delete_derivatives(thumb: uploaded_file)
|
|
382
|
+
# attacher.delete_derivatives({ thumb: uploaded_file })
|
|
383
383
|
# uploaded_file.exists? #=> false
|
|
384
384
|
def delete_derivatives(derivatives = self.derivatives)
|
|
385
385
|
map_derivative(derivatives) { |_, derivative| derivative.delete }
|
|
@@ -387,7 +387,7 @@ class Shrine
|
|
|
387
387
|
|
|
388
388
|
# Sets the given hash of uploaded files as derivatives.
|
|
389
389
|
#
|
|
390
|
-
# attacher.set_derivatives(thumb: uploaded_file)
|
|
390
|
+
# attacher.set_derivatives({ thumb: uploaded_file })
|
|
391
391
|
# attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
|
|
392
392
|
def set_derivatives(derivatives)
|
|
393
393
|
self.derivatives = derivatives
|
|
@@ -398,7 +398,7 @@ class Shrine
|
|
|
398
398
|
# Adds derivative data into the hash.
|
|
399
399
|
#
|
|
400
400
|
# attacher.attach(io)
|
|
401
|
-
# attacher.add_derivatives(thumb: thumb)
|
|
401
|
+
# attacher.add_derivatives({ thumb: thumb })
|
|
402
402
|
# attacher.data
|
|
403
403
|
# #=>
|
|
404
404
|
# # {
|
|
@@ -431,12 +431,12 @@ class Shrine
|
|
|
431
431
|
# attacher.load_data({
|
|
432
432
|
# "id" => "...",
|
|
433
433
|
# "storage" => "store",
|
|
434
|
-
# "metadata" => {
|
|
434
|
+
# "metadata" => {},
|
|
435
435
|
# "derivatives" => {
|
|
436
436
|
# "thumb" => {
|
|
437
437
|
# "id" => "...",
|
|
438
438
|
# "storage" => "store",
|
|
439
|
-
# "metadata" => {
|
|
439
|
+
# "metadata" => {},
|
|
440
440
|
# }
|
|
441
441
|
# }
|
|
442
442
|
# })
|
|
@@ -459,15 +459,19 @@ class Shrine
|
|
|
459
459
|
# attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
|
|
460
460
|
# attacher.change(file)
|
|
461
461
|
# attacher.derivatives #=> {}
|
|
462
|
+
#
|
|
463
|
+
# # With keep_derivatives: true
|
|
464
|
+
# attacher.change(file)
|
|
465
|
+
# attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
|
|
462
466
|
def change(*)
|
|
463
467
|
result = super
|
|
464
|
-
set_derivatives({})
|
|
468
|
+
set_derivatives({}) unless shrine_class.derivatives_options[:keep_derivatives]
|
|
465
469
|
result
|
|
466
470
|
end
|
|
467
471
|
|
|
468
472
|
# Sets a hash of derivatives.
|
|
469
473
|
#
|
|
470
|
-
# attacher.derivatives = { thumb: Shrine.uploaded_file(
|
|
474
|
+
# attacher.derivatives = { thumb: Shrine.uploaded_file(data) }
|
|
471
475
|
# attacher.derivatives #=> { thumb: #<Shrine::UploadedFile ...> }
|
|
472
476
|
def derivatives=(derivatives)
|
|
473
477
|
unless derivatives.is_a?(Hash)
|
|
@@ -479,7 +483,9 @@ class Shrine
|
|
|
479
483
|
|
|
480
484
|
# Iterates through nested derivatives and maps results.
|
|
481
485
|
#
|
|
482
|
-
# attacher.map_derivative(derivatives)
|
|
486
|
+
# attacher.map_derivative(derivatives) do |path, file|
|
|
487
|
+
# # ...
|
|
488
|
+
# end
|
|
483
489
|
def map_derivative(derivatives, **options, &block)
|
|
484
490
|
shrine_class.map_derivative(derivatives, **options, &block)
|
|
485
491
|
end
|
|
@@ -544,6 +550,14 @@ class Shrine
|
|
|
544
550
|
def create_derivatives_on_promote?
|
|
545
551
|
shrine_class.derivatives_options[:create_on_promote]
|
|
546
552
|
end
|
|
553
|
+
|
|
554
|
+
def derivatives_synchronize
|
|
555
|
+
if @derivatives_mutex
|
|
556
|
+
@derivatives_mutex.synchronize { yield }
|
|
557
|
+
else
|
|
558
|
+
yield
|
|
559
|
+
end
|
|
560
|
+
end
|
|
547
561
|
end
|
|
548
562
|
|
|
549
563
|
module ClassMethods
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
3
6
|
class Shrine
|
|
4
7
|
module Plugins
|
|
5
8
|
# Documentation can be found on https://shrinerb.com/docs/plugins/download_endpoint
|
|
@@ -16,11 +19,11 @@ class Shrine
|
|
|
16
19
|
|
|
17
20
|
module ClassMethods
|
|
18
21
|
# Returns the Rack application that retrieves requested files.
|
|
19
|
-
def download_endpoint(**
|
|
22
|
+
def download_endpoint(**)
|
|
20
23
|
Shrine::DownloadEndpoint.new(
|
|
21
24
|
shrine_class: self,
|
|
22
25
|
**opts[:download_endpoint],
|
|
23
|
-
|
|
26
|
+
**,
|
|
24
27
|
)
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -30,7 +33,7 @@ class Shrine
|
|
|
30
33
|
# It uses a trick where it removes the download path prefix from the
|
|
31
34
|
# path info before calling the Rack app, which is what web framework
|
|
32
35
|
# routers do before they're calling a mounted Rack app.
|
|
33
|
-
def download_response(env, **
|
|
36
|
+
def download_response(env, **)
|
|
34
37
|
script_name = env["SCRIPT_NAME"]
|
|
35
38
|
path_info = env["PATH_INFO"]
|
|
36
39
|
|
|
@@ -43,7 +46,7 @@ class Shrine
|
|
|
43
46
|
env["SCRIPT_NAME"] += match.to_s
|
|
44
47
|
env["PATH_INFO"] = match.post_match
|
|
45
48
|
|
|
46
|
-
download_endpoint(**
|
|
49
|
+
download_endpoint(**).call(env)
|
|
47
50
|
ensure
|
|
48
51
|
env["SCRIPT_NAME"] = script_name
|
|
49
52
|
env["PATH_INFO"] = path_info
|
|
@@ -53,8 +56,8 @@ class Shrine
|
|
|
53
56
|
|
|
54
57
|
module FileMethods
|
|
55
58
|
# Returns file URL on the download endpoint.
|
|
56
|
-
def download_url(**
|
|
57
|
-
FileUrl.new(self).call(**
|
|
59
|
+
def download_url(**)
|
|
60
|
+
FileUrl.new(self).call(**)
|
|
58
61
|
end
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -65,14 +68,36 @@ class Shrine
|
|
|
65
68
|
@file = file
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
def call(host: self.host)
|
|
69
|
-
[
|
|
71
|
+
def call(host: self.host, expires_in: nil)
|
|
72
|
+
path = file.urlsafe_dump(metadata: %w[filename size mime_type])
|
|
73
|
+
|
|
74
|
+
query = signature_as_query(path: path, expires_in: expires_in)
|
|
75
|
+
|
|
76
|
+
path = [host, *prefix, path].join("/")
|
|
77
|
+
path += "?#{query}" if query
|
|
78
|
+
path
|
|
70
79
|
end
|
|
71
80
|
|
|
72
81
|
protected
|
|
73
82
|
|
|
74
|
-
def path
|
|
75
|
-
|
|
83
|
+
def signature_as_query(path:, expires_in:)
|
|
84
|
+
expires_in = default_expires_in if expires_in.nil?
|
|
85
|
+
raise(Error, "secret_key is required for expiring URLs") if !secret_key && expires_in
|
|
86
|
+
raise(Error, "expires_in is required for expiring URLs") if secret_key && !expires_in
|
|
87
|
+
|
|
88
|
+
return nil unless expires_in
|
|
89
|
+
|
|
90
|
+
expires_at = (Time.now + expires_in).to_i
|
|
91
|
+
signature = OpenSSL::HMAC.digest(
|
|
92
|
+
OpenSSL::Digest::SHA256.new,
|
|
93
|
+
secret_key,
|
|
94
|
+
"#{path}--#{expires_at}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
Rack::Utils.build_query(
|
|
98
|
+
signature: Base64.urlsafe_encode64(signature),
|
|
99
|
+
expires_at: expires_at
|
|
100
|
+
)
|
|
76
101
|
end
|
|
77
102
|
|
|
78
103
|
def host
|
|
@@ -83,6 +108,14 @@ class Shrine
|
|
|
83
108
|
options[:prefix]
|
|
84
109
|
end
|
|
85
110
|
|
|
111
|
+
def default_expires_in
|
|
112
|
+
options[:expires_in]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def secret_key
|
|
116
|
+
options[:secret_key]
|
|
117
|
+
end
|
|
118
|
+
|
|
86
119
|
def options
|
|
87
120
|
file.shrine_class.opts[:download_endpoint]
|
|
88
121
|
end
|
|
@@ -113,7 +146,9 @@ class Shrine
|
|
|
113
146
|
handle_request(request)
|
|
114
147
|
end
|
|
115
148
|
|
|
116
|
-
headers[
|
|
149
|
+
headers = Rack::Headers[headers] if Rack.release >= "3"
|
|
150
|
+
headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
|
|
151
|
+
body.map(&:bytesize).inject(0, :+).to_s
|
|
117
152
|
|
|
118
153
|
[status, headers, body]
|
|
119
154
|
end
|
|
@@ -127,6 +162,9 @@ class Shrine
|
|
|
127
162
|
|
|
128
163
|
def handle_request(request)
|
|
129
164
|
_, serialized, * = request.path_info.split("/")
|
|
165
|
+
signature, expires_at = request.params.values_at("signature", "expires_at")
|
|
166
|
+
|
|
167
|
+
check_signature!(serialized, signature, expires_at) if @secret_key
|
|
130
168
|
|
|
131
169
|
uploaded_file = get_uploaded_file(serialized)
|
|
132
170
|
|
|
@@ -182,12 +220,35 @@ class Shrine
|
|
|
182
220
|
@shrine_class::UploadedFile.urlsafe_load(serialized)
|
|
183
221
|
rescue Shrine::Error # storage not found
|
|
184
222
|
not_found!
|
|
223
|
+
rescue JSON::ParserError, ArgumentError => error # invalid serialized component
|
|
224
|
+
raise if error.is_a?(ArgumentError) && error.message != "invalid base64"
|
|
225
|
+
bad_request!("Invalid serialized file")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def check_signature!(serialized, signature, expires_at)
|
|
229
|
+
if expires_at && expires_at.to_i < Time.now.to_i
|
|
230
|
+
error!(400, "URL has expired")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
calculated_signature = OpenSSL::HMAC.digest(
|
|
234
|
+
OpenSSL::Digest::SHA256.new,
|
|
235
|
+
@secret_key,
|
|
236
|
+
"#{serialized}--#{expires_at}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if !Rack::Utils.secure_compare(signature, Base64.urlsafe_encode64(calculated_signature))
|
|
240
|
+
error!(403, "Signature does not match")
|
|
241
|
+
end
|
|
185
242
|
end
|
|
186
243
|
|
|
187
244
|
def not_found!
|
|
188
245
|
error!(404, "File Not Found")
|
|
189
246
|
end
|
|
190
247
|
|
|
248
|
+
def bad_request!(message)
|
|
249
|
+
error!(400, message)
|
|
250
|
+
end
|
|
251
|
+
|
|
191
252
|
# Halts the request with the error message.
|
|
192
253
|
def error!(status, message)
|
|
193
254
|
throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
|