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
@@ -0,0 +1,271 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
# Core class which handles attaching files to model instances.
|
5
|
+
# Base implementation is defined in InstanceMethods and ClassMethods.
|
6
|
+
class Attacher
|
7
|
+
@shrine_class = ::Shrine
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Returns the Shrine class that this attacher class is namespaced
|
11
|
+
# under.
|
12
|
+
attr_accessor :shrine_class
|
13
|
+
|
14
|
+
# Since Attacher is anonymously subclassed when Shrine is subclassed,
|
15
|
+
# and then assigned to a constant of the Shrine subclass, make inspect
|
16
|
+
# reflect the likely name for the class.
|
17
|
+
def inspect
|
18
|
+
"#{shrine_class.inspect}::Attacher"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Block that is executed in context of Shrine::Attacher during
|
22
|
+
# validation. Example:
|
23
|
+
#
|
24
|
+
# Shrine::Attacher.validate do
|
25
|
+
# if get.size > 5*1024*1024
|
26
|
+
# errors << "is too big (max is 5 MB)"
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
def validate(&block)
|
30
|
+
define_method(:validate_block, &block)
|
31
|
+
private :validate_block
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InstanceMethods
|
36
|
+
# Returns the uploader that is used for the temporary storage.
|
37
|
+
attr_reader :cache
|
38
|
+
|
39
|
+
# Returns the uploader that is used for the permanent storage.
|
40
|
+
attr_reader :store
|
41
|
+
|
42
|
+
# Returns the context that will be sent to the uploader when uploading
|
43
|
+
# and deleting. Can be modified with additional data to be sent to the
|
44
|
+
# uploader.
|
45
|
+
attr_reader :context
|
46
|
+
|
47
|
+
# Returns an array of validation errors created on file assignment in
|
48
|
+
# the `Attacher.validate` block.
|
49
|
+
attr_reader :errors
|
50
|
+
|
51
|
+
# Initializes the necessary attributes.
|
52
|
+
def initialize(record, name, cache: :cache, store: :store)
|
53
|
+
@cache = shrine_class.new(cache)
|
54
|
+
@store = shrine_class.new(store)
|
55
|
+
@context = {record: record, name: name}
|
56
|
+
@errors = []
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the model instance associated with the attacher.
|
60
|
+
def record; context[:record]; end
|
61
|
+
|
62
|
+
# Returns the attachment name associated with the attacher.
|
63
|
+
def name; context[:name]; end
|
64
|
+
|
65
|
+
# Receives the attachment value from the form. It can receive an
|
66
|
+
# already cached file as a JSON string, otherwise it assumes that it's
|
67
|
+
# an IO object and uploads it to the temporary storage. The cached file
|
68
|
+
# is then written to the attachment attribute in the JSON format.
|
69
|
+
def assign(value, **options)
|
70
|
+
if value.is_a?(String)
|
71
|
+
return if value == "" || !cached?(uploaded_file(value))
|
72
|
+
assign_cached(uploaded_file(value))
|
73
|
+
else
|
74
|
+
uploaded_file = cache!(value, action: :cache, **options) if value
|
75
|
+
set(uploaded_file)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Accepts a Shrine::UploadedFile object and writes it to the attachment
|
80
|
+
# attribute. It then runs file validations, and records that the
|
81
|
+
# attachment has changed.
|
82
|
+
def set(uploaded_file)
|
83
|
+
file = get
|
84
|
+
@old = file unless uploaded_file == file
|
85
|
+
_set(uploaded_file)
|
86
|
+
validate
|
87
|
+
end
|
88
|
+
|
89
|
+
# Runs the validations defined by `Attacher.validate`.
|
90
|
+
def validate
|
91
|
+
errors.clear
|
92
|
+
validate_block if get
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns true if a new file has been attached.
|
96
|
+
def changed?
|
97
|
+
instance_variable_defined?(:@old)
|
98
|
+
end
|
99
|
+
alias attached? changed?
|
100
|
+
|
101
|
+
# Plugins can override this if they want something to be done before
|
102
|
+
# save.
|
103
|
+
def save
|
104
|
+
end
|
105
|
+
|
106
|
+
# Deletes the old file and promotes the new one. Typically this should
|
107
|
+
# be called after saving the model instance.
|
108
|
+
def finalize
|
109
|
+
return if !instance_variable_defined?(:@old)
|
110
|
+
replace
|
111
|
+
remove_instance_variable(:@old)
|
112
|
+
_promote(action: :store) if cached?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Delegates to #promote, overriden for backgrounding.
|
116
|
+
def _promote(uploaded_file = get, **options)
|
117
|
+
promote(uploaded_file, **options)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Uploads the cached file to store, and writes the stored file to the
|
121
|
+
# attachment attribute.
|
122
|
+
def promote(uploaded_file = get, **options)
|
123
|
+
stored_file = store!(uploaded_file, **options)
|
124
|
+
result = swap(stored_file) or _delete(stored_file, action: :abort)
|
125
|
+
result
|
126
|
+
end
|
127
|
+
|
128
|
+
# Calls #update, overriden in ORM plugins, and returns true if the
|
129
|
+
# attachment was successfully updated.
|
130
|
+
def swap(uploaded_file)
|
131
|
+
update(uploaded_file)
|
132
|
+
uploaded_file if uploaded_file == get
|
133
|
+
end
|
134
|
+
|
135
|
+
# Deletes the previous attachment that was replaced, typically called
|
136
|
+
# after the model instance is saved with the new attachment.
|
137
|
+
def replace
|
138
|
+
_delete(@old, action: :replace) if @old && !cached?(@old)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Deletes the current attachment, typically called after destroying the
|
142
|
+
# record.
|
143
|
+
def destroy
|
144
|
+
file = get
|
145
|
+
_delete(file, action: :destroy) if file && !cached?(file)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Delegates to #delete!, overriden for backgrounding.
|
149
|
+
def _delete(uploaded_file, **options)
|
150
|
+
delete!(uploaded_file, **options)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the URL to the attached file if it's present. It forwards any
|
154
|
+
# given URL options to the storage.
|
155
|
+
def url(**options)
|
156
|
+
get.url(**options) if read
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns true if attachment is present and cached.
|
160
|
+
def cached?(file = get)
|
161
|
+
file && cache.uploaded?(file)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns true if attachment is present and stored.
|
165
|
+
def stored?(file = get)
|
166
|
+
file && store.uploaded?(file)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns a Shrine::UploadedFile instantiated from the data written to
|
170
|
+
# the attachment attribute.
|
171
|
+
def get
|
172
|
+
uploaded_file(read) if read
|
173
|
+
end
|
174
|
+
|
175
|
+
# Reads from the `<attachment>_data` attribute on the model instance.
|
176
|
+
# It returns nil if the value is blank.
|
177
|
+
def read
|
178
|
+
value = record.send(data_attribute)
|
179
|
+
convert_after_read(value) unless value.nil? || value.empty?
|
180
|
+
end
|
181
|
+
|
182
|
+
# Uploads the file using the #cache uploader, passing the #context.
|
183
|
+
def cache!(io, **options)
|
184
|
+
Shrine.deprecation("Sending :phase to Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
185
|
+
cache.upload(io, context.merge(_equalize_phase_and_action(options)))
|
186
|
+
end
|
187
|
+
|
188
|
+
# Uploads the file using the #store uploader, passing the #context.
|
189
|
+
def store!(io, **options)
|
190
|
+
Shrine.deprecation("Sending :phase to Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
191
|
+
store.upload(io, context.merge(_equalize_phase_and_action(options)))
|
192
|
+
end
|
193
|
+
|
194
|
+
# Deletes the file using the uploader, passing the #context.
|
195
|
+
def delete!(uploaded_file, **options)
|
196
|
+
Shrine.deprecation("Sending :phase to Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
197
|
+
store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
|
198
|
+
end
|
199
|
+
|
200
|
+
# Enhances `Shrine.uploaded_file` with the ability to recognize uploaded
|
201
|
+
# files as JSON strings.
|
202
|
+
def uploaded_file(object, &block)
|
203
|
+
shrine_class.uploaded_file(object, &block)
|
204
|
+
end
|
205
|
+
|
206
|
+
# The name of the attribute on the model instance that is used to store
|
207
|
+
# the attachment data. Defaults to `<attachment>_data`.
|
208
|
+
def data_attribute
|
209
|
+
:"#{name}_data"
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns the Shrine class that this attacher's class is namespaced
|
213
|
+
# under.
|
214
|
+
def shrine_class
|
215
|
+
self.class.shrine_class
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
# Assigns a cached file.
|
221
|
+
def assign_cached(cached_file)
|
222
|
+
set(cached_file)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Writes the uploaded file to the attachment attribute. Overriden in ORM
|
226
|
+
# plugins to additionally save the model instance.
|
227
|
+
def update(uploaded_file)
|
228
|
+
_set(uploaded_file)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Performs validation actually.
|
232
|
+
# This method is redefined with `Attacher.validate`.
|
233
|
+
def validate_block
|
234
|
+
end
|
235
|
+
|
236
|
+
# Converts the UploadedFile to a data hash and writes it to the
|
237
|
+
# attribute.
|
238
|
+
def _set(uploaded_file)
|
239
|
+
data = convert_to_data(uploaded_file) if uploaded_file
|
240
|
+
write(data ? convert_before_write(data) : nil)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Writes to the `<attachment>_data` attribute on the model instance.
|
244
|
+
def write(value)
|
245
|
+
record.send(:"#{data_attribute}=", value)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns the data hash of the given UploadedFile.
|
249
|
+
def convert_to_data(uploaded_file)
|
250
|
+
uploaded_file.data
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns the hash value dumped to JSON.
|
254
|
+
def convert_before_write(value)
|
255
|
+
value.to_json
|
256
|
+
end
|
257
|
+
|
258
|
+
# Returns the read value unchanged.
|
259
|
+
def convert_after_read(value)
|
260
|
+
value
|
261
|
+
end
|
262
|
+
|
263
|
+
# Temporary method used for transitioning from :phase to :action.
|
264
|
+
def _equalize_phase_and_action(options)
|
265
|
+
options[:phase] = options[:action] if options.key?(:action)
|
266
|
+
options[:action] = options[:phase] if options.key?(:phase)
|
267
|
+
options
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
# Core class which creates attachment modules for specified attribute names
|
5
|
+
# that are included into model classes.
|
6
|
+
# Base implementation is defined in InstanceMethods and ClassMethods.
|
7
|
+
class Attachment < Module
|
8
|
+
@shrine_class = ::Shrine
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Returns the Shrine class that this attachment class is
|
12
|
+
# namespaced under.
|
13
|
+
attr_accessor :shrine_class
|
14
|
+
|
15
|
+
# Since Attachment is anonymously subclassed when Shrine is subclassed,
|
16
|
+
# and then assigned to a constant of the Shrine subclass, make inspect
|
17
|
+
# reflect the likely name for the class.
|
18
|
+
def inspect
|
19
|
+
"#{shrine_class.inspect}::Attachment"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
# Instantiates an attachment module for a given attribute name, which
|
25
|
+
# can then be included to a model class. Second argument will be passed
|
26
|
+
# to an attacher module.
|
27
|
+
def initialize(name, **options)
|
28
|
+
@name = name.to_sym
|
29
|
+
@options = options
|
30
|
+
|
31
|
+
define_attachment_methods!
|
32
|
+
end
|
33
|
+
|
34
|
+
# Defines attachment methods for the specified attachment name. These
|
35
|
+
# methods will be added to any model that includes this module.
|
36
|
+
def define_attachment_methods!
|
37
|
+
attachment = self
|
38
|
+
name = attachment_name
|
39
|
+
|
40
|
+
define_method :"#{name}_attacher" do |**options|
|
41
|
+
if !instance_variable_get(:"@#{name}_attacher") || options.any?
|
42
|
+
instance_variable_set(:"@#{name}_attacher", attachment.build_attacher(self, options))
|
43
|
+
else
|
44
|
+
instance_variable_get(:"@#{name}_attacher")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
define_method :"#{name}=" do |value|
|
49
|
+
send(:"#{name}_attacher").assign(value)
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method :"#{name}" do
|
53
|
+
send(:"#{name}_attacher").get
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method :"#{name}_url" do |*args|
|
57
|
+
send(:"#{name}_attacher").url(*args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates an instance of the corresponding Attacher subclass.
|
62
|
+
def build_attacher(object, options)
|
63
|
+
shrine_class::Attacher.new(object, @name, @options.merge(options))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns name of the attachment this module provides.
|
67
|
+
def attachment_name
|
68
|
+
@name
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns options that are to be passed to the Attacher.
|
72
|
+
def options
|
73
|
+
@options
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns class name with attachment name included.
|
77
|
+
#
|
78
|
+
# Shrine[:image].to_s #=> "#<Shrine::Attachment(image)>"
|
79
|
+
def to_s
|
80
|
+
"#<#{self.class.inspect}(#{attachment_name})>"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns class name with attachment name included.
|
84
|
+
#
|
85
|
+
# Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
|
86
|
+
def inspect
|
87
|
+
"#<#{self.class.inspect}(#{attachment_name})>"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the Shrine class that this attachment's class is namespaced
|
91
|
+
# under.
|
92
|
+
def shrine_class
|
93
|
+
self.class.shrine_class
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
# Module in which all Shrine plugins should be stored. Also contains logic
|
5
|
+
# for registering and loading plugins.
|
6
|
+
module Plugins
|
7
|
+
@plugins = {}
|
8
|
+
|
9
|
+
# If the registered plugin already exists, use it. Otherwise, require it
|
10
|
+
# and return it. This raises a LoadError if such a plugin doesn't exist,
|
11
|
+
# or a Shrine::Error if it exists but it does not register itself
|
12
|
+
# correctly.
|
13
|
+
def self.load_plugin(name)
|
14
|
+
unless plugin = @plugins[name]
|
15
|
+
require "shrine/plugins/#{name}"
|
16
|
+
raise Error, "plugin #{name} did not register itself correctly in Shrine::Plugins" unless plugin = @plugins[name]
|
17
|
+
end
|
18
|
+
plugin
|
19
|
+
end
|
20
|
+
|
21
|
+
# Register the given plugin with Shrine, so that it can be loaded using
|
22
|
+
# `Shrine.plugin` with a symbol. Should be used by plugin files. Example:
|
23
|
+
#
|
24
|
+
# Shrine::Plugins.register_plugin(:plugin_name, PluginModule)
|
25
|
+
def self.register_plugin(name, mod)
|
26
|
+
@plugins[name] = mod
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "json"
|
5
|
+
require "openssl"
|
6
|
+
|
7
|
+
class Shrine
|
8
|
+
module Plugins
|
9
|
+
# The `urlsafe_serialization` plugin provides the ability to serialize and
|
10
|
+
# deserialize a `Shrine::UploadedFile` in a way that's suitable for
|
11
|
+
# including in a URL.
|
12
|
+
#
|
13
|
+
# plugin :urlsafe_serialization
|
14
|
+
#
|
15
|
+
# The plugin defines `urlsafe_dump` and `urlsafe_load` methods on
|
16
|
+
# `Shrine::UploadedFile`. The file is first serialized to JSON, then
|
17
|
+
# encoded with base64.
|
18
|
+
#
|
19
|
+
# serialized = uploaded_file.urlsafe_dump
|
20
|
+
# # or
|
21
|
+
# serialized = MyUploader::UploadedFile.urlsafe_dump(uploaded_file)
|
22
|
+
# serialized #=> "eyJpZCI6IjlhZGM0NmIzZjI..."
|
23
|
+
#
|
24
|
+
# # ...
|
25
|
+
#
|
26
|
+
# uploaded_file = MyUploader::UploadedFile.urlsafe_load(serialized)
|
27
|
+
# uploaded_file #=> #<MyUploader::UploadedFile>
|
28
|
+
#
|
29
|
+
# ## Metadata
|
30
|
+
#
|
31
|
+
# By default no metadata is included in the serialization:
|
32
|
+
#
|
33
|
+
# uploaded_file.metadata #=> { ... metadata ... }
|
34
|
+
#
|
35
|
+
# serialized = MyUploader::UploadedFile.urlsafe_dump(uploaded_file)
|
36
|
+
# uploaded_file = MyUploader::UploadedFile.urlsafe_load(serialized)
|
37
|
+
#
|
38
|
+
# uploaded_file.metadata #=> {}
|
39
|
+
#
|
40
|
+
# The `:metadata` option can be used to specify metadata you want to
|
41
|
+
# serialize:
|
42
|
+
#
|
43
|
+
# serialized = MyUploader::UploadedFile.urlsafe_dump(uploaded_file, metadata: %w[size mime_type])
|
44
|
+
# uploaded_file = MyUploader::UploadedFile.urlsafe_load(serialized)
|
45
|
+
#
|
46
|
+
# uploaded_file.metadata #=> { "size" => 4394, "mime_type" => "image/jpeg" }
|
47
|
+
#
|
48
|
+
# ## Signing
|
49
|
+
#
|
50
|
+
# By default the seralization is done with simple JSON + base64 encoding.
|
51
|
+
# If you want to ensure the serialized data hasn't been tampred with, you
|
52
|
+
# can have it signed with a secret key.
|
53
|
+
#
|
54
|
+
# plugin :urlsafe_serialization, secret_key: "my secret key"
|
55
|
+
#
|
56
|
+
# Now the `urlsafe_dump` will automatically sign serialized data with your
|
57
|
+
# secret key, and `urlsafe_load` will automatically verify it.
|
58
|
+
#
|
59
|
+
# serialized = MyUploader::UploadedFile.urlsafe_dump(uploaded_file)
|
60
|
+
# serialized #=> "<signature>--<json-base64-encoded-data>"
|
61
|
+
#
|
62
|
+
# uploaded_file = MyUploader::UploadedFile.urlsafe_load(serialized) # verifies the signature
|
63
|
+
# uploaded_file #=> #<MyUploader::UploadedFile>
|
64
|
+
#
|
65
|
+
# If the signature is missing or invalid,
|
66
|
+
# `Shrine::Plugins::UrlsafeSerialization::InvalidSignature` exception is
|
67
|
+
# raised.
|
68
|
+
module UrlsafeSerialization
|
69
|
+
class InvalidSignature < Error; end
|
70
|
+
|
71
|
+
def self.configure(uploader, opts = {})
|
72
|
+
uploader.opts[:urlsafe_serialization] ||= {}
|
73
|
+
uploader.opts[:urlsafe_serialization].merge!(opts)
|
74
|
+
end
|
75
|
+
|
76
|
+
module FileMethods
|
77
|
+
def urlsafe_dump(**options)
|
78
|
+
self.class.urlsafe_dump(self, **options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module FileClassMethods
|
83
|
+
def urlsafe_dump(file, metadata: [])
|
84
|
+
data = file.data.dup
|
85
|
+
data["metadata"] = metadata
|
86
|
+
.map { |name| [name, file.metadata[name]] }
|
87
|
+
.to_h
|
88
|
+
|
89
|
+
urlsafe_serializer.dump(data)
|
90
|
+
end
|
91
|
+
|
92
|
+
def urlsafe_load(string)
|
93
|
+
data = urlsafe_serializer.load(string)
|
94
|
+
|
95
|
+
new(data)
|
96
|
+
end
|
97
|
+
|
98
|
+
def urlsafe_serializer
|
99
|
+
secret_key = shrine_class.opts[:urlsafe_serialization][:secret_key]
|
100
|
+
|
101
|
+
if secret_key
|
102
|
+
SecureSerializer.new(secret_key: secret_key)
|
103
|
+
else
|
104
|
+
Serializer.new
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Serializer
|
110
|
+
def dump(data)
|
111
|
+
base64_encode(json_encode(data))
|
112
|
+
end
|
113
|
+
|
114
|
+
def load(data)
|
115
|
+
json_decode(base64_decode(data))
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def json_encode(data)
|
121
|
+
JSON.generate(data)
|
122
|
+
end
|
123
|
+
|
124
|
+
def base64_encode(data)
|
125
|
+
Base64.urlsafe_encode64(data, padding: false)
|
126
|
+
end
|
127
|
+
|
128
|
+
def base64_decode(data)
|
129
|
+
Base64.urlsafe_decode64(data)
|
130
|
+
end
|
131
|
+
|
132
|
+
def json_decode(data)
|
133
|
+
JSON.parse(data)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class SecureSerializer < Serializer
|
138
|
+
attr_reader :secret_key
|
139
|
+
|
140
|
+
def initialize(secret_key:)
|
141
|
+
@secret_key = secret_key
|
142
|
+
end
|
143
|
+
|
144
|
+
def dump(data)
|
145
|
+
hmac_encode(super)
|
146
|
+
end
|
147
|
+
|
148
|
+
def load(data)
|
149
|
+
super(hmac_decode(data))
|
150
|
+
end
|
151
|
+
|
152
|
+
def hmac_encode(data)
|
153
|
+
"#{generate_hmac(data)}--#{data}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def hmac_decode(data)
|
157
|
+
data, hmac = data.split("--", 2).reverse
|
158
|
+
verify_hmac(hmac, data)
|
159
|
+
data
|
160
|
+
end
|
161
|
+
|
162
|
+
def verify_hmac(provided_hmac, data)
|
163
|
+
if provided_hmac.nil?
|
164
|
+
raise InvalidSignature, "signature is missing"
|
165
|
+
end
|
166
|
+
|
167
|
+
expected_hmac = generate_hmac(data)
|
168
|
+
|
169
|
+
if provided_hmac != expected_hmac
|
170
|
+
raise InvalidSignature, "provided signature doesn't match the expected"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def generate_hmac(data)
|
175
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret_key, data)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
register_plugin(:_urlsafe_serialization, UrlsafeSerialization)
|
181
|
+
end
|
182
|
+
end
|