shrine 2.13.0 → 2.14.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +20 -16
- data/doc/creating_storages.md +0 -21
- data/doc/design.md +1 -0
- data/doc/direct_s3.md +26 -15
- data/doc/metadata.md +67 -22
- data/doc/multiple_files.md +3 -3
- data/doc/processing.md +1 -1
- data/doc/retrieving_uploads.md +184 -0
- data/lib/shrine.rb +268 -900
- data/lib/shrine/attacher.rb +271 -0
- data/lib/shrine/attachment.rb +97 -0
- data/lib/shrine/plugins.rb +29 -0
- data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
- data/lib/shrine/plugins/activerecord.rb +16 -14
- data/lib/shrine/plugins/add_metadata.rb +58 -24
- data/lib/shrine/plugins/backgrounding.rb +6 -1
- data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
- data/lib/shrine/plugins/copy.rb +12 -8
- data/lib/shrine/plugins/data_uri.rb +23 -20
- data/lib/shrine/plugins/default_url_options.rb +5 -4
- data/lib/shrine/plugins/determine_mime_type.rb +24 -23
- data/lib/shrine/plugins/download_endpoint.rb +61 -73
- data/lib/shrine/plugins/migration_helpers.rb +17 -17
- data/lib/shrine/plugins/module_include.rb +9 -8
- data/lib/shrine/plugins/presign_endpoint.rb +13 -7
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +128 -36
- data/lib/shrine/plugins/refresh_metadata.rb +20 -5
- data/lib/shrine/plugins/remote_url.rb +8 -8
- data/lib/shrine/plugins/remove_attachment.rb +9 -9
- data/lib/shrine/plugins/sequel.rb +21 -18
- data/lib/shrine/plugins/tempfile.rb +68 -0
- data/lib/shrine/plugins/upload_endpoint.rb +3 -2
- data/lib/shrine/plugins/upload_options.rb +7 -6
- data/lib/shrine/plugins/validation_helpers.rb +2 -1
- data/lib/shrine/storage/file_system.rb +20 -17
- data/lib/shrine/storage/linter.rb +0 -7
- data/lib/shrine/storage/s3.rb +159 -50
- data/lib/shrine/uploaded_file.rb +258 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +7 -19
- metadata +41 -21
@@ -94,29 +94,31 @@ class Shrine
|
|
94
94
|
|
95
95
|
return unless model < ::ActiveRecord::Base
|
96
96
|
|
97
|
-
|
97
|
+
name = attachment_name
|
98
98
|
|
99
|
-
|
100
|
-
validate do
|
101
|
-
#{
|
102
|
-
errors.add(
|
99
|
+
if shrine_class.opts[:activerecord_validations]
|
100
|
+
model.validate do
|
101
|
+
send("#{name}_attacher").errors.each do |message|
|
102
|
+
errors.add(name, *message)
|
103
103
|
end
|
104
104
|
end
|
105
|
-
|
105
|
+
end
|
106
106
|
|
107
|
-
|
108
|
-
before_save do
|
109
|
-
|
107
|
+
if shrine_class.opts[:activerecord_callbacks]
|
108
|
+
model.before_save do
|
109
|
+
attacher = send("#{name}_attacher")
|
110
|
+
attacher.save if attacher.changed?
|
110
111
|
end
|
111
112
|
|
112
|
-
after_commit on: [:create, :update] do
|
113
|
-
|
113
|
+
model.after_commit on: [:create, :update] do
|
114
|
+
attacher = send("#{name}_attacher")
|
115
|
+
attacher.finalize if attacher.changed?
|
114
116
|
end
|
115
117
|
|
116
|
-
after_commit on: [:destroy] do
|
117
|
-
#{
|
118
|
+
model.after_commit on: [:destroy] do
|
119
|
+
send("#{name}_attacher").destroy
|
118
120
|
end
|
119
|
-
|
121
|
+
end
|
120
122
|
end
|
121
123
|
end
|
122
124
|
|
@@ -56,19 +56,42 @@ class Shrine
|
|
56
56
|
# "resolution" => movie.resolution,
|
57
57
|
# "frame_rate" => movie.frame_rate }
|
58
58
|
# end
|
59
|
+
#
|
60
|
+
# Any previously extracted metadata can be accessed via
|
61
|
+
# `context[:metadata]`:
|
62
|
+
#
|
63
|
+
# add_metadata :foo do |io, context|
|
64
|
+
# context[:metadata] #=>
|
65
|
+
# # {
|
66
|
+
# # "size" => 239823,
|
67
|
+
# # "filename" => "nature.jpg",
|
68
|
+
# # "mime_type" => "image/jpeg"
|
69
|
+
# # }
|
70
|
+
#
|
71
|
+
# "foo"
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# add_metadata :bar do |io, context|
|
75
|
+
# context[:metadata] #=>
|
76
|
+
# # {
|
77
|
+
# # "size" => 239823,
|
78
|
+
# # "filename" => "nature.jpg",
|
79
|
+
# # "mime_type" => "image/jpeg",
|
80
|
+
# # "foo" => "foo"
|
81
|
+
# # }
|
82
|
+
#
|
83
|
+
# "bar"
|
84
|
+
# end
|
59
85
|
module AddMetadata
|
60
86
|
def self.configure(uploader)
|
61
|
-
uploader.opts[:metadata]
|
87
|
+
uploader.opts[:metadata] ||= []
|
62
88
|
end
|
63
89
|
|
64
90
|
module ClassMethods
|
65
|
-
def add_metadata(name = nil, &block)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
else
|
70
|
-
opts[:metadata] << block
|
71
|
-
end
|
91
|
+
def add_metadata(name = nil, **options, &block)
|
92
|
+
opts[:metadata] << [name, options, block]
|
93
|
+
|
94
|
+
metadata_method(name) if name
|
72
95
|
end
|
73
96
|
|
74
97
|
def metadata_method(*names)
|
@@ -82,30 +105,41 @@ class Shrine
|
|
82
105
|
metadata[name.to_s]
|
83
106
|
end
|
84
107
|
end
|
85
|
-
|
86
|
-
def _metadata_proc(name, &block)
|
87
|
-
proc do |io, context|
|
88
|
-
value = instance_exec(io, context, &block)
|
89
|
-
{name.to_s => value} unless value.nil?
|
90
|
-
end
|
91
|
-
end
|
92
108
|
end
|
93
109
|
|
94
110
|
module InstanceMethods
|
95
|
-
def extract_metadata(io, context)
|
111
|
+
def extract_metadata(io, context = {})
|
96
112
|
metadata = super
|
113
|
+
context = context.merge(metadata: metadata)
|
114
|
+
|
115
|
+
extract_custom_metadata(io, context)
|
116
|
+
|
117
|
+
metadata
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def extract_custom_metadata(io, context)
|
123
|
+
opts[:metadata].each do |name, options, block|
|
124
|
+
result = instance_exec(io, context, &block)
|
125
|
+
metadata = {}
|
126
|
+
|
127
|
+
if name
|
128
|
+
metadata[name.to_s] = result
|
129
|
+
else
|
130
|
+
metadata.merge!(result) if result
|
131
|
+
end
|
97
132
|
|
98
|
-
opts[:metadata].each do |metadata_block|
|
99
|
-
custom_metadata = instance_exec(io, context, &metadata_block) || {}
|
100
|
-
io.rewind
|
101
133
|
# convert symbol keys to strings
|
102
|
-
|
103
|
-
|
134
|
+
metadata.keys.each do |key|
|
135
|
+
metadata[key.to_s] = metadata.delete(key) if key.is_a?(Symbol)
|
104
136
|
end
|
105
|
-
metadata.merge!(custom_metadata)
|
106
|
-
end
|
107
137
|
|
108
|
-
|
138
|
+
context[:metadata].merge!(metadata)
|
139
|
+
|
140
|
+
# rewind between metadata blocks
|
141
|
+
io.rewind
|
142
|
+
end
|
109
143
|
end
|
110
144
|
end
|
111
145
|
end
|
@@ -265,7 +265,12 @@ class Shrine
|
|
265
265
|
def swap(new_file)
|
266
266
|
if self.class.respond_to?(:find_record)
|
267
267
|
reloaded = self.class.find_record(record.class, record.id)
|
268
|
-
return if reloaded.nil?
|
268
|
+
return if reloaded.nil?
|
269
|
+
|
270
|
+
attacher = reloaded.send(:"#{name}_attacher") if reloaded.respond_to?(:"#{name}_attacher")
|
271
|
+
attacher ||= self.class.new(reloaded, name) # Shrine::Attachment is not used
|
272
|
+
|
273
|
+
return if attacher.get != self.get
|
269
274
|
end
|
270
275
|
super
|
271
276
|
end
|
@@ -22,16 +22,16 @@ class Shrine
|
|
22
22
|
def initialize(*)
|
23
23
|
super
|
24
24
|
|
25
|
-
|
26
|
-
def cached_#{@name}_data
|
27
|
-
#{@name}_attacher.read_cached
|
28
|
-
end
|
25
|
+
name = attachment_name
|
29
26
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
define_method :"cached_#{name}_data" do
|
28
|
+
send(:"#{name}_attacher").read_cached
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method :"cached_#{name}_data=" do |value|
|
32
|
+
Shrine.deprecation("Calling #cached_#{name}_data= is deprecated and will be removed in Shrine 3. You should use the original field name: `f.hidden_field :#{name}, value: record.cached_#{name}_data`.")
|
33
|
+
send(:"#{name}_attacher").assign(value)
|
34
|
+
end
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
data/lib/shrine/plugins/copy.rb
CHANGED
@@ -21,14 +21,18 @@ class Shrine
|
|
21
21
|
def initialize(*)
|
22
22
|
super
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
name = attachment_name
|
25
|
+
|
26
|
+
define_method :initialize_copy do |record|
|
27
|
+
super(record)
|
28
|
+
instance_variable_set(:"@#{name}_attacher", nil) # reload the attacher
|
29
|
+
attacher = send(:"#{name}_attacher")
|
30
|
+
attacher.send(:write, nil) # remove original attachment
|
31
|
+
attacher.copy(record.public_send(:"#{name}_attacher"))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Fix for JRuby
|
35
|
+
private :initialize_copy
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
@@ -54,6 +54,7 @@ class Shrine
|
|
54
54
|
# io = Shrine.data_uri("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")
|
55
55
|
# io.content_type #=> "image/png"
|
56
56
|
# io.size #=> 21
|
57
|
+
# io.read # decoded content
|
57
58
|
#
|
58
59
|
# When the content type is ommited, `text/plain` is assumed. The parser
|
59
60
|
# also supports raw data URIs which aren't base64-encoded.
|
@@ -64,6 +65,11 @@ class Shrine
|
|
64
65
|
# io.size #=> 11
|
65
66
|
# io.read #=> "raw content"
|
66
67
|
#
|
68
|
+
# You can also assign a filename:
|
69
|
+
#
|
70
|
+
# io = Shrine.data_uri("data:,content", filename: "foo.txt")
|
71
|
+
# io.original_filename #=> "foo.txt"
|
72
|
+
#
|
67
73
|
# ## `UploadedFile#data_uri` and `UploadedFile#base64`
|
68
74
|
#
|
69
75
|
# This plugin also adds UploadedFile#data_uri method, which returns a
|
@@ -94,15 +100,17 @@ class Shrine
|
|
94
100
|
module ClassMethods
|
95
101
|
# Parses the given data URI and creates an IO object from it.
|
96
102
|
#
|
97
|
-
# Shrine.data_uri("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")
|
98
|
-
# #=> #<Shrine::Plugins::DataUri::DataFile>
|
99
|
-
|
103
|
+
# io = Shrine.data_uri("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")
|
104
|
+
# io #=> #<Shrine::Plugins::DataUri::DataFile>
|
105
|
+
# io.content_type #=> "image/png"
|
106
|
+
# io.size #=> 21
|
107
|
+
# io.read # decoded content
|
108
|
+
def data_uri(uri, filename: nil)
|
100
109
|
info = parse_data_uri(uri)
|
101
110
|
|
102
111
|
content_type = info[:content_type] || DEFAULT_CONTENT_TYPE
|
103
112
|
content = info[:base64] ? Base64.decode64(info[:data]) : CGI.unescape(info[:data])
|
104
|
-
filename = opts[:data_uri_filename]
|
105
|
-
filename = filename.call(content_type) if filename
|
113
|
+
filename = opts[:data_uri_filename].call(content_type) if opts[:data_uri_filename]
|
106
114
|
|
107
115
|
data_file = DataFile.new(content, content_type: content_type, filename: filename)
|
108
116
|
info[:data].clear
|
@@ -118,14 +126,9 @@ class Shrine
|
|
118
126
|
media_type = scanner.scan(MEDIA_TYPE_REGEXP)
|
119
127
|
base64 = scanner.scan(BASE64_REGEXP)
|
120
128
|
scanner.scan(CONTENT_SEPARATOR) or raise ParseError, "data URI has invalid format"
|
129
|
+
content = scanner.post_match
|
121
130
|
|
122
|
-
content_type
|
123
|
-
|
124
|
-
{
|
125
|
-
content_type: content_type,
|
126
|
-
base64: !!base64,
|
127
|
-
data: scanner.post_match,
|
128
|
-
}
|
131
|
+
{ content_type: media_type, base64: !!base64, data: content }
|
129
132
|
end
|
130
133
|
end
|
131
134
|
|
@@ -133,15 +136,15 @@ class Shrine
|
|
133
136
|
def initialize(*)
|
134
137
|
super
|
135
138
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
139
|
+
name = attachment_name
|
140
|
+
|
141
|
+
define_method :"#{name}_data_uri=" do |uri|
|
142
|
+
send(:"#{name}_attacher").data_uri = uri
|
143
|
+
end
|
140
144
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
RUBY
|
145
|
+
define_method :"#{name}_data_uri" do
|
146
|
+
send(:"#{name}_attacher").data_uri
|
147
|
+
end
|
145
148
|
end
|
146
149
|
end
|
147
150
|
|
@@ -5,14 +5,14 @@ class Shrine
|
|
5
5
|
# The `default_url_options` plugin allows you to specify URL options that
|
6
6
|
# will be applied by default for uploaded files of specified storages.
|
7
7
|
#
|
8
|
-
# plugin :default_url_options, store: {download: true}
|
8
|
+
# plugin :default_url_options, store: { download: true }
|
9
9
|
#
|
10
10
|
# You can also generate the default URL options dynamically by using a
|
11
11
|
# block, which will receive the UploadedFile object along with any options
|
12
12
|
# that were passed to `UploadedFile#url`.
|
13
13
|
#
|
14
|
-
# plugin :default_url_options, store: ->(io, **options) do
|
15
|
-
# {response_content_disposition:
|
14
|
+
# plugin :default_url_options, store: -> (io, **options) do
|
15
|
+
# { response_content_disposition: ContentDisposition.attachment(io.original_filename) }
|
16
16
|
# end
|
17
17
|
#
|
18
18
|
# In both cases the default options are merged with options passed to
|
@@ -20,7 +20,8 @@ class Shrine
|
|
20
20
|
# default options.
|
21
21
|
module DefaultUrlOptions
|
22
22
|
def self.configure(uploader, options = {})
|
23
|
-
uploader.opts[:default_url_options]
|
23
|
+
uploader.opts[:default_url_options] ||= {}
|
24
|
+
uploader.opts[:default_url_options].merge!(options)
|
24
25
|
end
|
25
26
|
|
26
27
|
module FileMethods
|
@@ -51,10 +51,10 @@ class Shrine
|
|
51
51
|
# extension. Note that unlike other solutions, this analyzer is not
|
52
52
|
# guaranteed to return the actual MIME type of the file.
|
53
53
|
#
|
54
|
-
# :
|
55
|
-
# :
|
56
|
-
#
|
57
|
-
# the actual MIME type of the file.
|
54
|
+
# :content_type
|
55
|
+
# : Retrieves the value of the `#content_type` attribute of the IO object.
|
56
|
+
# Note that this value normally comes from the "Content-Type" request
|
57
|
+
# header, so it's not guaranteed to hold the actual MIME type of the file.
|
58
58
|
#
|
59
59
|
# A single analyzer is not going to properly recognize all types of files,
|
60
60
|
# so you can build your own custom analyzer for your requirements, where
|
@@ -88,6 +88,11 @@ class Shrine
|
|
88
88
|
# [fastimage]: https://github.com/sdsykes/fastimage
|
89
89
|
module DetermineMimeType
|
90
90
|
def self.configure(uploader, opts = {})
|
91
|
+
if opts[:analyzer] == :default
|
92
|
+
Shrine.deprecation("The :default analyzer of the determine_mime_type plugin has been renamed to :content_type. The :default alias will not be supported in Shrine 3.")
|
93
|
+
opts = opts.merge(analyzer: :content_type)
|
94
|
+
end
|
95
|
+
|
91
96
|
uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
|
92
97
|
end
|
93
98
|
|
@@ -95,16 +100,12 @@ class Shrine
|
|
95
100
|
# Determines the MIME type of the IO object by calling the specified
|
96
101
|
# analyzer.
|
97
102
|
def determine_mime_type(io)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
mime_type = analyzer.call(*args)
|
106
|
-
io.rewind
|
107
|
-
end
|
103
|
+
analyzer = opts[:mime_type_analyzer]
|
104
|
+
analyzer = mime_type_analyzer(analyzer) if analyzer.is_a?(Symbol)
|
105
|
+
args = [io, mime_type_analyzers].take(analyzer.arity.abs)
|
106
|
+
|
107
|
+
mime_type = analyzer.call(*args)
|
108
|
+
io.rewind
|
108
109
|
|
109
110
|
mime_type
|
110
111
|
end
|
@@ -127,15 +128,9 @@ class Shrine
|
|
127
128
|
module InstanceMethods
|
128
129
|
private
|
129
130
|
|
130
|
-
# Calls
|
131
|
-
# just reads the `#content_type` attribute, otherwise uses the specified
|
132
|
-
# MIME type analyzer.
|
131
|
+
# Calls the configured MIME type analyzer.
|
133
132
|
def extract_mime_type(io)
|
134
|
-
|
135
|
-
super
|
136
|
-
else
|
137
|
-
self.class.determine_mime_type(io)
|
138
|
-
end
|
133
|
+
self.class.determine_mime_type(io)
|
139
134
|
end
|
140
135
|
|
141
136
|
# Returns a hash of built-in MIME type analyzers.
|
@@ -145,7 +140,7 @@ class Shrine
|
|
145
140
|
end
|
146
141
|
|
147
142
|
class MimeTypeAnalyzer
|
148
|
-
SUPPORTED_TOOLS = [:fastimage, :file, :filemagic, :mimemagic, :marcel, :mime_types, :mini_mime]
|
143
|
+
SUPPORTED_TOOLS = [:fastimage, :file, :filemagic, :mimemagic, :marcel, :mime_types, :mini_mime, :content_type]
|
149
144
|
MAGIC_NUMBER = 256 * 1024
|
150
145
|
|
151
146
|
def initialize(tool)
|
@@ -242,6 +237,12 @@ class Shrine
|
|
242
237
|
end
|
243
238
|
end
|
244
239
|
|
240
|
+
def extract_with_content_type(io)
|
241
|
+
if io.respond_to?(:content_type) && io.content_type
|
242
|
+
io.content_type.split(";").first
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
245
246
|
def extract_filename(io)
|
246
247
|
if io.respond_to?(:original_filename)
|
247
248
|
io.original_filename
|
@@ -2,9 +2,6 @@
|
|
2
2
|
|
3
3
|
require "roda"
|
4
4
|
|
5
|
-
require "base64"
|
6
|
-
require "json"
|
7
|
-
|
8
5
|
class Shrine
|
9
6
|
module Plugins
|
10
7
|
# The `download_endpoint` plugin provides a Rack endpoint for downloading
|
@@ -41,15 +38,26 @@ class Shrine
|
|
41
38
|
# Links to the download endpoint are generated by calling
|
42
39
|
# `UploadedFile#download_url` instead of the usual `UploadedFile#url`.
|
43
40
|
#
|
44
|
-
# uploaded_file.download_url #=> "/attachments/
|
41
|
+
# uploaded_file.download_url #=> "/attachments/eyJpZCI6ImFkdzlyeTM..."
|
42
|
+
#
|
43
|
+
# ## Host
|
44
|
+
#
|
45
|
+
# You can specify download URL host via the `:host` plugin option:
|
46
|
+
#
|
47
|
+
# plugin :download_endpoint, host: "http://example.com"
|
48
|
+
#
|
49
|
+
# or by passing `:host` to `UploadedFile#download_url`:
|
45
50
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# recommended to either configure a CDN to serve these files:
|
51
|
+
# uploaded_file.download_url(host: "http://example.com")
|
52
|
+
# #=> "http//example.com/attachments/eyJpZCI6ImFkdzlyeTM..."
|
49
53
|
#
|
50
|
-
#
|
54
|
+
# ## Performance considerations
|
51
55
|
#
|
52
|
-
#
|
56
|
+
# Streaming files through the app might impact the request throughput,
|
57
|
+
# depending on the web server you're using. So it's recommended to use a
|
58
|
+
# CDN, which can be set via the `:host` option.
|
59
|
+
#
|
60
|
+
# Alternatively, you can have the endpoint redirect to the direct file URL:
|
53
61
|
#
|
54
62
|
# plugin :download_endpoint, redirect: true
|
55
63
|
# # or
|
@@ -57,13 +65,16 @@ class Shrine
|
|
57
65
|
# # return URL which the request will redirect to
|
58
66
|
# end
|
59
67
|
#
|
60
|
-
#
|
61
|
-
#
|
68
|
+
# ## Custom endpoint
|
69
|
+
#
|
70
|
+
# If you want to have more control on download requests, you can use the
|
71
|
+
# `rack_response` plugin which this plugin uses internally.
|
62
72
|
#
|
63
73
|
# [Roda]: https://github.com/jeremyevans/roda
|
64
74
|
module DownloadEndpoint
|
65
75
|
def self.load_dependencies(uploader, opts = {})
|
66
76
|
uploader.plugin :rack_response
|
77
|
+
uploader.plugin :_urlsafe_serialization
|
67
78
|
end
|
68
79
|
|
69
80
|
# Accepts the following options:
|
@@ -121,11 +132,7 @@ class Shrine
|
|
121
132
|
@download_endpoint = endpoint_class
|
122
133
|
|
123
134
|
const_set(:DownloadEndpoint, endpoint_class)
|
124
|
-
deprecate_constant(:DownloadEndpoint)
|
125
|
-
end
|
126
|
-
|
127
|
-
def download_endpoint_serializer
|
128
|
-
@download_endpoint_serializer ||= Serializer.new
|
135
|
+
deprecate_constant(:DownloadEndpoint)
|
129
136
|
end
|
130
137
|
end
|
131
138
|
|
@@ -142,34 +149,45 @@ class Shrine
|
|
142
149
|
end
|
143
150
|
end
|
144
151
|
|
145
|
-
|
146
|
-
|
152
|
+
# Returns file URL on the download endpoint.
|
153
|
+
def download_url(**options)
|
154
|
+
FileUrl.new(self).call(**options)
|
147
155
|
end
|
148
156
|
|
149
157
|
private
|
150
158
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
159
|
+
def download_storages
|
160
|
+
shrine_class.opts[:download_endpoint_storages]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class FileUrl
|
165
|
+
attr_reader :file
|
166
|
+
|
167
|
+
def initialize(file)
|
168
|
+
@file = file
|
157
169
|
end
|
158
170
|
|
159
|
-
def
|
160
|
-
|
171
|
+
def call(host: self.host)
|
172
|
+
[host, *prefix, path].join("/")
|
161
173
|
end
|
162
174
|
|
163
|
-
|
175
|
+
protected
|
176
|
+
|
177
|
+
def path
|
178
|
+
file.urlsafe_dump(metadata: %w[filename size mime_type])
|
179
|
+
end
|
180
|
+
|
181
|
+
def host
|
164
182
|
shrine_class.opts[:download_endpoint_host]
|
165
183
|
end
|
166
184
|
|
167
|
-
def
|
185
|
+
def prefix
|
168
186
|
shrine_class.opts[:download_endpoint_prefix]
|
169
187
|
end
|
170
188
|
|
171
|
-
def
|
172
|
-
shrine_class
|
189
|
+
def shrine_class
|
190
|
+
file.shrine_class
|
173
191
|
end
|
174
192
|
end
|
175
193
|
|
@@ -181,23 +199,25 @@ class Shrine
|
|
181
199
|
# handle legacy ":storage/:id" URLs
|
182
200
|
r.on storage_names do |storage_name|
|
183
201
|
r.get /(.*)/ do |id|
|
184
|
-
|
185
|
-
|
202
|
+
uploaded_file = shrine_class::UploadedFile.new(
|
203
|
+
"id" => id,
|
204
|
+
"storage" => storage_name,
|
205
|
+
)
|
206
|
+
|
207
|
+
serve_file(uploaded_file)
|
186
208
|
end
|
187
209
|
end
|
188
210
|
|
189
|
-
r.get /(.*)/ do |
|
190
|
-
|
191
|
-
serve_file(
|
211
|
+
r.get /(.*)/ do |serialized|
|
212
|
+
uploaded_file = get_uploaded_file(serialized)
|
213
|
+
serve_file(uploaded_file)
|
192
214
|
end
|
193
215
|
end
|
194
216
|
|
195
217
|
private
|
196
218
|
|
197
219
|
# Streams or redirects to the uploaded file.
|
198
|
-
def serve_file(
|
199
|
-
uploaded_file = get_uploaded_file(data)
|
200
|
-
|
220
|
+
def serve_file(uploaded_file)
|
201
221
|
if redirect
|
202
222
|
redirect_to_file(uploaded_file)
|
203
223
|
else
|
@@ -227,11 +247,11 @@ class Shrine
|
|
227
247
|
end
|
228
248
|
|
229
249
|
# Returns a Shrine::UploadedFile, or returns 404 if file doesn't exist.
|
230
|
-
def get_uploaded_file(
|
231
|
-
uploaded_file = shrine_class.
|
250
|
+
def get_uploaded_file(serialized)
|
251
|
+
uploaded_file = shrine_class::UploadedFile.urlsafe_load(serialized)
|
232
252
|
not_found! unless uploaded_file.exists?
|
233
253
|
uploaded_file
|
234
|
-
rescue Shrine::Error
|
254
|
+
rescue Shrine::Error # storage not found
|
235
255
|
not_found!
|
236
256
|
end
|
237
257
|
|
@@ -251,10 +271,6 @@ class Shrine
|
|
251
271
|
shrine_class.storages.keys.map(&:to_s)
|
252
272
|
end
|
253
273
|
|
254
|
-
def serializer
|
255
|
-
shrine_class.download_endpoint_serializer
|
256
|
-
end
|
257
|
-
|
258
274
|
def redirect
|
259
275
|
opts[:redirect]
|
260
276
|
end
|
@@ -267,34 +283,6 @@ class Shrine
|
|
267
283
|
opts[:shrine_class]
|
268
284
|
end
|
269
285
|
end
|
270
|
-
|
271
|
-
class Serializer
|
272
|
-
def dump(data)
|
273
|
-
base64_encode(json_encode(data))
|
274
|
-
end
|
275
|
-
|
276
|
-
def load(data)
|
277
|
-
json_decode(base64_decode(data))
|
278
|
-
end
|
279
|
-
|
280
|
-
private
|
281
|
-
|
282
|
-
def json_encode(data)
|
283
|
-
JSON.generate(data)
|
284
|
-
end
|
285
|
-
|
286
|
-
def base64_encode(data)
|
287
|
-
Base64.urlsafe_encode64(data)
|
288
|
-
end
|
289
|
-
|
290
|
-
def base64_decode(data)
|
291
|
-
Base64.urlsafe_decode64(data)
|
292
|
-
end
|
293
|
-
|
294
|
-
def json_decode(data)
|
295
|
-
JSON.parse(data)
|
296
|
-
end
|
297
|
-
end
|
298
286
|
end
|
299
287
|
|
300
288
|
register_plugin(:download_endpoint, DownloadEndpoint)
|