shrine 2.5.0 → 2.6.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/README.md +14 -13
- data/doc/attacher.md +7 -6
- data/doc/carrierwave.md +19 -17
- data/doc/design.md +1 -1
- data/doc/direct_s3.md +8 -5
- data/doc/multiple_files.md +4 -4
- data/doc/paperclip.md +7 -6
- data/doc/refile.md +67 -4
- data/doc/securing_uploads.md +41 -25
- data/doc/testing.md +6 -15
- data/lib/shrine.rb +19 -10
- data/lib/shrine/plugins/activerecord.rb +4 -4
- data/lib/shrine/plugins/add_metadata.rb +7 -3
- data/lib/shrine/plugins/background_helpers.rb +1 -1
- data/lib/shrine/plugins/backgrounding.rb +19 -6
- data/lib/shrine/plugins/cached_attachment_data.rb +4 -4
- data/lib/shrine/plugins/data_uri.rb +105 -31
- data/lib/shrine/plugins/default_url.rb +1 -1
- data/lib/shrine/plugins/delete_raw.rb +7 -3
- data/lib/shrine/plugins/determine_mime_type.rb +96 -44
- data/lib/shrine/plugins/direct_upload.rb +3 -1
- data/lib/shrine/plugins/download_endpoint.rb +14 -5
- data/lib/shrine/plugins/logging.rb +4 -4
- data/lib/shrine/plugins/metadata_attributes.rb +61 -0
- data/lib/shrine/plugins/migration_helpers.rb +1 -1
- data/lib/shrine/plugins/rack_file.rb +54 -30
- data/lib/shrine/plugins/recache.rb +1 -1
- data/lib/shrine/plugins/refresh_metadata.rb +29 -0
- data/lib/shrine/plugins/remote_url.rb +26 -4
- data/lib/shrine/plugins/remove_invalid.rb +5 -4
- data/lib/shrine/plugins/restore_cached_data.rb +10 -13
- data/lib/shrine/plugins/sequel.rb +4 -4
- data/lib/shrine/plugins/signature.rb +146 -0
- data/lib/shrine/plugins/store_dimensions.rb +68 -24
- data/lib/shrine/plugins/validation_helpers.rb +48 -29
- data/lib/shrine/plugins/versions.rb +16 -8
- data/lib/shrine/storage/file_system.rb +27 -16
- data/lib/shrine/storage/s3.rb +99 -58
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +1 -1
- metadata +9 -6
data/doc/securing_uploads.md
CHANGED
@@ -8,8 +8,7 @@ the obvious ones to not-so-obvious ones, and try to provide solutions.
|
|
8
8
|
## Validate file type
|
9
9
|
|
10
10
|
Almost always you will be accepting certain types of files, and it's a good
|
11
|
-
idea to create a whitelist (or
|
12
|
-
types.
|
11
|
+
idea to create a whitelist (or a blacklist) of extensions and MIME types.
|
13
12
|
|
14
13
|
By default Shrine stores the MIME type derived from the extension, which means
|
15
14
|
it's not guaranteed to hold the actual MIME type of the the file. However, you
|
@@ -22,8 +21,8 @@ class MyUploader < Shrine
|
|
22
21
|
plugin :determine_mime_type
|
23
22
|
|
24
23
|
Attacher.validate do
|
25
|
-
validate_extension_inclusion [
|
26
|
-
validate_mime_type_inclusion [
|
24
|
+
validate_extension_inclusion %w[jpg jpeg png gif]
|
25
|
+
validate_mime_type_inclusion %w[image/jpeg image/png image/gif]
|
27
26
|
end
|
28
27
|
end
|
29
28
|
```
|
@@ -49,7 +48,7 @@ end
|
|
49
48
|
In the following sections we talk about various strategies to prevent files from
|
50
49
|
being uploaded to cache and the temporary directory.
|
51
50
|
|
52
|
-
###
|
51
|
+
### Limiting filesize in direct uploads
|
53
52
|
|
54
53
|
If you're doing direct uploads with the `direct_upload` plugin, you can pass
|
55
54
|
in the `:max_size` option, which will refuse too large files and automatically
|
@@ -68,34 +67,32 @@ plugin :direct_upload, presign: ->(request) do
|
|
68
67
|
end
|
69
68
|
```
|
70
69
|
|
71
|
-
###
|
70
|
+
### Limiting filesize at application level
|
72
71
|
|
73
|
-
If
|
74
|
-
|
72
|
+
If your application is accepting file uploads, it's good practice to limit the
|
73
|
+
maximum allowed `Content-Length` before calling `params` for the first time,
|
74
|
+
to avoid Rack parsing the multipart request parameters and creating a Tempfile
|
75
|
+
for uploads that are obviously attempts of attacks.
|
75
76
|
|
76
77
|
```rb
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
if request.content_length >= 100*1024*1024 # 100MB
|
79
|
+
response.status = 413 # Request Entity Too Large
|
80
|
+
response.body = "The uploaded file was too large (maximum is 100MB)"
|
81
|
+
request.halt
|
82
|
+
end
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
-
application server (nginx or apache):
|
84
|
+
request.params # Rack parses the multipart request params
|
85
|
+
```
|
85
86
|
|
86
|
-
|
87
|
-
|
87
|
+
Alternatively you can allow uploads of any size to temporary Shrine storage,
|
88
|
+
but tell Shrine to immediately delete the file if it failed validations by
|
89
|
+
loading the `remove_invalid` plugin.
|
88
90
|
|
89
|
-
|
90
|
-
|
91
|
-
server {
|
92
|
-
# ...
|
93
|
-
client_max_body_size 20M;
|
94
|
-
}
|
95
|
-
}
|
91
|
+
```rb
|
92
|
+
plugin :remove_invalid
|
96
93
|
```
|
97
94
|
|
98
|
-
### Paranoid limiting
|
95
|
+
### Paranoid filesize limiting
|
99
96
|
|
100
97
|
If you want to make sure that no large files ever get to your storages, and
|
101
98
|
you don't really care about the error message, you can use the `hooks` plugin
|
@@ -140,6 +137,25 @@ end
|
|
140
137
|
If you're doing processing on caching, you can use the fastimage gem directly
|
141
138
|
in a conditional.
|
142
139
|
|
140
|
+
## Prevent metadata tampering
|
141
|
+
|
142
|
+
When cached file is retained on validation errors or it was direct uploaded,
|
143
|
+
the uploaded file representation is assigned to the attacher. This also
|
144
|
+
includes any file metadata. By default Shrine won't attempt to re-extract
|
145
|
+
metadata, because for remote storages that requires an additional HTTP request,
|
146
|
+
which might not be feasible depending on the application requirements.
|
147
|
+
|
148
|
+
However, this means that the attacker can directly upload a malicious file
|
149
|
+
(because direct uploads aren't validated), and then modify the metadata hash so
|
150
|
+
that it passes Shrine validations, before submitting the cached file to your
|
151
|
+
app. To guard yourself from such attacks, you can load the
|
152
|
+
`restore_cached_data` plugin, which will automatically re-extract metadata from
|
153
|
+
cached files on assignment and override the received metadata.
|
154
|
+
|
155
|
+
```rb
|
156
|
+
plugin :restore_cached_data
|
157
|
+
```
|
158
|
+
|
143
159
|
## Limit number of files
|
144
160
|
|
145
161
|
When doing direct uploads, it's a good idea to apply some kind of throttling to
|
data/doc/testing.md
CHANGED
@@ -80,20 +80,11 @@ factory :photo do
|
|
80
80
|
end
|
81
81
|
```
|
82
82
|
|
83
|
-
On the other hand, if you're setting up test data using YAML fixtures,
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
```rb
|
90
|
-
Shrine.plugin :data_uri
|
91
|
-
```
|
92
|
-
```yml
|
93
|
-
# test/fixtures/photos.yml
|
94
|
-
photo:
|
95
|
-
image_data_uri: "data:image/png;base64,<%= Base64.encode64(File.binread("test/files/image.png")) %>"
|
96
|
-
```
|
83
|
+
On the other hand, if you're setting up test data using Rails' YAML fixtures,
|
84
|
+
you unfortunately won't be able to use them for assigning files. This is
|
85
|
+
because Rails fixtures only allow assigning primitive data types, and don't
|
86
|
+
allow you to specify Shrine attributes, you can only assign to columns
|
87
|
+
directly.
|
97
88
|
|
98
89
|
## Background jobs
|
99
90
|
|
@@ -222,7 +213,7 @@ return a hash of unprocessed original files:
|
|
222
213
|
class ImageUploader
|
223
214
|
def process(io, context)
|
224
215
|
if context[:action] == :store
|
225
|
-
{small: io, medium: io, large: io}
|
216
|
+
{small: io.download, medium: io.download, large: io.download}
|
226
217
|
end
|
227
218
|
end
|
228
219
|
end
|
data/lib/shrine.rb
CHANGED
@@ -2,6 +2,7 @@ require "shrine/version"
|
|
2
2
|
|
3
3
|
require "securerandom"
|
4
4
|
require "json"
|
5
|
+
require "tempfile"
|
5
6
|
|
6
7
|
class Shrine
|
7
8
|
# A generic exception used by Shrine.
|
@@ -137,7 +138,7 @@ class Shrine
|
|
137
138
|
self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods)
|
138
139
|
self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods)
|
139
140
|
plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
|
140
|
-
|
141
|
+
plugin
|
141
142
|
end
|
142
143
|
|
143
144
|
# Retrieves the storage under the given identifier (can be a Symbol or
|
@@ -151,7 +152,7 @@ class Shrine
|
|
151
152
|
# model class. Example:
|
152
153
|
#
|
153
154
|
# class Photo
|
154
|
-
# include Shrine
|
155
|
+
# include Shrine.attachment(:image) # creates a Shrine::Attachment object
|
155
156
|
# end
|
156
157
|
def attachment(name)
|
157
158
|
self::Attachment.new(name)
|
@@ -166,7 +167,7 @@ class Shrine
|
|
166
167
|
def uploaded_file(object, &block)
|
167
168
|
case object
|
168
169
|
when String
|
169
|
-
|
170
|
+
deprecation("Giving a string to Shrine.uploaded_file is deprecated and won't be possible in Shrine 3. Use Attacher#uploaded_file instead.")
|
170
171
|
uploaded_file(JSON.parse(object), &block)
|
171
172
|
when Hash
|
172
173
|
uploaded_file(self::UploadedFile.new(object), &block)
|
@@ -176,6 +177,11 @@ class Shrine
|
|
176
177
|
raise Error, "cannot convert #{object.inspect} to a #{self}::UploadedFile"
|
177
178
|
end
|
178
179
|
end
|
180
|
+
|
181
|
+
# Prints a deprecation warning to standard error.
|
182
|
+
def deprecation(message)
|
183
|
+
warn "SHRINE DEPRECATION WARNING: #{message}"
|
184
|
+
end
|
179
185
|
end
|
180
186
|
|
181
187
|
module InstanceMethods
|
@@ -340,7 +346,8 @@ class Shrine
|
|
340
346
|
# Retrieves the location for the given IO and context. First it looks
|
341
347
|
# for the `:location` option, otherwise it calls #generate_location.
|
342
348
|
def get_location(io, context)
|
343
|
-
context[:location] || generate_location(io, context)
|
349
|
+
location = context[:location] || generate_location(io, context)
|
350
|
+
location or raise Error, "location generated for #{io.inspect} was nil (context = #{context})"
|
344
351
|
end
|
345
352
|
|
346
353
|
# If the IO object is a Shrine::UploadedFile, it simply copies over its
|
@@ -492,7 +499,7 @@ class Shrine
|
|
492
499
|
# is then written to the attachment attribute in the JSON format.
|
493
500
|
def assign(value)
|
494
501
|
if value.is_a?(String)
|
495
|
-
return if value == "" ||
|
502
|
+
return if value == "" || !cache.uploaded?(uploaded_file(value))
|
496
503
|
assign_cached(uploaded_file(value))
|
497
504
|
else
|
498
505
|
uploaded_file = cache!(value, action: :cache) if value
|
@@ -504,7 +511,7 @@ class Shrine
|
|
504
511
|
# attribute. It then runs file validations, and records that the
|
505
512
|
# attachment has changed.
|
506
513
|
def set(uploaded_file)
|
507
|
-
@old = get
|
514
|
+
@old = get unless uploaded_file == get
|
508
515
|
_set(uploaded_file)
|
509
516
|
validate
|
510
517
|
end
|
@@ -516,9 +523,10 @@ class Shrine
|
|
516
523
|
end
|
517
524
|
|
518
525
|
# Returns true if a new file has been attached.
|
519
|
-
def
|
526
|
+
def changed?
|
520
527
|
instance_variable_defined?(:@old)
|
521
528
|
end
|
529
|
+
alias attached? changed?
|
522
530
|
|
523
531
|
# Plugins can override this if they want something to be done before
|
524
532
|
# save.
|
@@ -528,6 +536,7 @@ class Shrine
|
|
528
536
|
# Deletes the old file and promotes the new one. Typically this should
|
529
537
|
# be called after saving the model instance.
|
530
538
|
def finalize
|
539
|
+
return if !instance_variable_defined?(:@old)
|
531
540
|
replace
|
532
541
|
remove_instance_variable(:@old)
|
533
542
|
_promote(action: :store) if cached?
|
@@ -601,19 +610,19 @@ class Shrine
|
|
601
610
|
|
602
611
|
# Uploads the file using the #cache uploader, passing the #context.
|
603
612
|
def cache!(io, **options)
|
604
|
-
|
613
|
+
Shrine.deprecation("Sending :phase to Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
605
614
|
cache.upload(io, context.merge(_equalize_phase_and_action(options)))
|
606
615
|
end
|
607
616
|
|
608
617
|
# Uploads the file using the #store uploader, passing the #context.
|
609
618
|
def store!(io, **options)
|
610
|
-
|
619
|
+
Shrine.deprecation("Sending :phase to Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
611
620
|
store.upload(io, context.merge(_equalize_phase_and_action(options)))
|
612
621
|
end
|
613
622
|
|
614
623
|
# Deletes the file using the uploader, passing the #context.
|
615
624
|
def delete!(uploaded_file, **options)
|
616
|
-
|
625
|
+
Shrine.deprecation("Sending :phase to Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
617
626
|
store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
|
618
627
|
end
|
619
628
|
|
@@ -29,7 +29,7 @@ class Shrine
|
|
29
29
|
# saves again with a stored attachment, you can detect this in callbacks:
|
30
30
|
#
|
31
31
|
# class User < ActiveRecord::Base
|
32
|
-
# include ImageUploader
|
32
|
+
# include ImageUploader::Attachment.new(:avatar)
|
33
33
|
#
|
34
34
|
# before_save do
|
35
35
|
# if avatar_data_changed? && avatar_attacher.cached?
|
@@ -53,7 +53,7 @@ class Shrine
|
|
53
53
|
# of the attachment, you can do it directly on the model.
|
54
54
|
#
|
55
55
|
# class User < ActiveRecord::Base
|
56
|
-
# include ImageUploader
|
56
|
+
# include ImageUploader::Attachment.new(:avatar)
|
57
57
|
# validates_presence_of :avatar
|
58
58
|
# end
|
59
59
|
#
|
@@ -85,11 +85,11 @@ class Shrine
|
|
85
85
|
|
86
86
|
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1 if opts[:activerecord_callbacks]
|
87
87
|
before_save do
|
88
|
-
#{@name}_attacher.save if #{@name}_attacher.
|
88
|
+
#{@name}_attacher.save if #{@name}_attacher.changed?
|
89
89
|
end
|
90
90
|
|
91
91
|
after_commit on: [:create, :update] do
|
92
|
-
#{@name}_attacher.finalize if #{@name}_attacher.
|
92
|
+
#{@name}_attacher.finalize if #{@name}_attacher.changed?
|
93
93
|
end
|
94
94
|
|
95
95
|
after_commit on: [:destroy] do
|
@@ -17,7 +17,7 @@ class Shrine
|
|
17
17
|
# document.pages
|
18
18
|
#
|
19
19
|
# You can also extract multiple metadata values at once, by using
|
20
|
-
# `add_metadata` without an argument.
|
20
|
+
# `add_metadata` without an argument, and returning a hash of metadata.
|
21
21
|
#
|
22
22
|
# add_metadata do |io, context|
|
23
23
|
# movie = FFMPEG::Movie.new(io.path)
|
@@ -73,9 +73,13 @@ class Shrine
|
|
73
73
|
metadata = super
|
74
74
|
|
75
75
|
opts[:metadata].each do |metadata_block|
|
76
|
-
custom_metadata = instance_exec(io, context, &metadata_block)
|
76
|
+
custom_metadata = instance_exec(io, context, &metadata_block) || {}
|
77
77
|
io.rewind
|
78
|
-
|
78
|
+
# convert symbol keys to strings
|
79
|
+
custom_metadata.keys.each do |key|
|
80
|
+
custom_metadata[key.to_s] = custom_metadata.delete(key) if key.is_a?(Symbol)
|
81
|
+
end
|
82
|
+
metadata.merge!(custom_metadata)
|
79
83
|
end
|
80
84
|
|
81
85
|
metadata
|
@@ -1,3 +1,3 @@
|
|
1
|
-
|
1
|
+
Shrine.deprecation("The background_helpers plugin has been renamed to \"backgrounding\". Loading the plugin through \"background_helpers\" will stop working in Shrine 3.")
|
2
2
|
require "shrine/plugins/backgrounding"
|
3
3
|
Shrine::Plugins.register_plugin(:background_helpers, Shrine::Plugins::Backgrounding)
|
@@ -5,17 +5,22 @@ class Shrine
|
|
5
5
|
# useful if you're doing processing and/or you're storing files on an
|
6
6
|
# external storage service.
|
7
7
|
#
|
8
|
-
# plugin :backgrounding
|
9
|
-
#
|
10
|
-
# ## Usage
|
11
|
-
#
|
12
8
|
# The plugin provides `Attacher.promote` and `Attacher.delete` methods,
|
13
9
|
# which allow you to hook up to promoting and deleting and spawn background
|
14
10
|
# jobs, by passing a block.
|
15
11
|
#
|
12
|
+
# Shrine.plugin :backgrounding
|
16
13
|
# Shrine::Attacher.promote { |data| PromoteJob.perform_async(data) }
|
17
14
|
# Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
18
15
|
#
|
16
|
+
# If you don't want to apply backgrounding for all uploaders, you can
|
17
|
+
# declare the hooks only for specific uploaders.
|
18
|
+
#
|
19
|
+
# class MyUploader < Shrine
|
20
|
+
# Attacher.promote { |data| PromoteJob.perform_async(data) }
|
21
|
+
# Attacher.delete { |data| DeleteJob.perform_async(data) }
|
22
|
+
# end
|
23
|
+
#
|
19
24
|
# The yielded `data` variable is a serializable hash containing all context
|
20
25
|
# needed for promotion/deletion. Now you just need to declare the job
|
21
26
|
# classes, and inside them call `Attacher.promote` or `Attacher.delete`,
|
@@ -23,7 +28,6 @@ class Shrine
|
|
23
28
|
#
|
24
29
|
# class PromoteJob
|
25
30
|
# include Sidekiq::Worker
|
26
|
-
#
|
27
31
|
# def perform(data)
|
28
32
|
# Shrine::Attacher.promote(data)
|
29
33
|
# end
|
@@ -31,7 +35,6 @@ class Shrine
|
|
31
35
|
#
|
32
36
|
# class DeleteJob
|
33
37
|
# include Sidekiq::Worker
|
34
|
-
#
|
35
38
|
# def perform(data)
|
36
39
|
# Shrine::Attacher.delete(data)
|
37
40
|
# end
|
@@ -45,6 +48,16 @@ class Shrine
|
|
45
48
|
# the foreground before kicking off a background job, you can use the
|
46
49
|
# `recache` plugin.
|
47
50
|
#
|
51
|
+
# In your application you can use `Attacher#cached?` and `Attacher#stored?`
|
52
|
+
# to differentiate between your background job being in progress and
|
53
|
+
# having completed.
|
54
|
+
#
|
55
|
+
# if user.avatar_attacher.cached? # background job is still in progress
|
56
|
+
# # ...
|
57
|
+
# elsif user.avatar_attacher.stored? # background job has finished
|
58
|
+
# # ...
|
59
|
+
# end
|
60
|
+
#
|
48
61
|
# ## `Attacher.promote` and `Attacher.delete`
|
49
62
|
#
|
50
63
|
# In background jobs, `Attacher.promote` and `Attacher.delete` will resolve
|
@@ -10,11 +10,11 @@ class Shrine
|
|
10
10
|
# the cached file as JSON, and should be used to set the value of the
|
11
11
|
# hidden form field.
|
12
12
|
#
|
13
|
-
# @user.cached_avatar_data #=> '{"
|
13
|
+
# @user.cached_avatar_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
|
14
14
|
#
|
15
15
|
# This method delegates to `Attacher#read_cached`:
|
16
16
|
#
|
17
|
-
# attacher.read_cached #=> '{"
|
17
|
+
# attacher.read_cached #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
|
18
18
|
module CachedAttachmentData
|
19
19
|
module AttachmentMethods
|
20
20
|
def initialize(*)
|
@@ -26,7 +26,7 @@ class Shrine
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def cached_#{@name}_data=(value)
|
29
|
-
|
29
|
+
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`.")
|
30
30
|
#{@name}_attacher.assign(value)
|
31
31
|
end
|
32
32
|
RUBY
|
@@ -35,7 +35,7 @@ class Shrine
|
|
35
35
|
|
36
36
|
module AttacherMethods
|
37
37
|
def read_cached
|
38
|
-
get.to_json if cached? &&
|
38
|
+
get.to_json if cached? && changed?
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
require "base64"
|
2
|
+
require "strscan"
|
3
|
+
require "cgi/util"
|
2
4
|
require "stringio"
|
5
|
+
require "forwardable"
|
3
6
|
|
4
7
|
class Shrine
|
5
8
|
module Plugins
|
@@ -19,42 +22,105 @@ class Shrine
|
|
19
22
|
# user.avatar.size #=> 43423
|
20
23
|
#
|
21
24
|
# You can also use `#data_uri=` and `#data_uri` methods directly on the
|
22
|
-
# `Shrine::Attacher
|
25
|
+
# `Shrine::Attacher` (which the model methods just delegate to):
|
23
26
|
#
|
24
27
|
# attacher.data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
25
28
|
#
|
26
|
-
# If you want the uploaded file to have an extension, you can generate a
|
27
|
-
# filename based on the content type of the data URI:
|
28
|
-
#
|
29
|
-
# plugin :data_uri, filename: ->(content_type) do
|
30
|
-
# extension = MIME::Types[content_type].first.preferred_extension
|
31
|
-
# "data_uri.#{extension}"
|
32
|
-
# end
|
33
|
-
#
|
34
29
|
# If the data URI wasn't correctly parsed, an error message will be added to
|
35
30
|
# the attachment column. You can change the default error message:
|
36
31
|
#
|
37
32
|
# plugin :data_uri, error_message: "data URI was invalid"
|
38
33
|
# plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
|
39
34
|
#
|
35
|
+
# If you just want to parse the data URI and create an IO object from it,
|
36
|
+
# you can do that with `Shrine.data_uri`. If the data URI cannot be parsed,
|
37
|
+
# a `Shrine::Plugins::DataUri::ParseError` will be raised.
|
38
|
+
#
|
39
|
+
# # or YourUploader.data_uri("...")
|
40
|
+
# io = Shrine.data_uri("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")
|
41
|
+
# io.content_type #=> "image/png"
|
42
|
+
# io.size #=> 21
|
43
|
+
#
|
44
|
+
# When the content type is ommited, `text/plain` is assumed. The parser
|
45
|
+
# also supports raw data URIs which aren't base64-encoded.
|
46
|
+
#
|
47
|
+
# # or YourUploader.data_uri("...")
|
48
|
+
# io = Shrine.data_uri("data:,raw%20content")
|
49
|
+
# io.content_type #=> "text/plain"
|
50
|
+
# io.size #=> 11
|
51
|
+
# io.read #=> "raw content"
|
52
|
+
#
|
53
|
+
# The created IO object won't convey any file extension (because it doesn't
|
54
|
+
# have a filename), but you can generate a filename based on the content
|
55
|
+
# type of the data URI:
|
56
|
+
#
|
57
|
+
# require "mime/types"
|
58
|
+
#
|
59
|
+
# plugin :data_uri, filename: ->(content_type) do
|
60
|
+
# extension = MIME::Types[content_type].first.preferred_extension
|
61
|
+
# "data_uri.#{extension}"
|
62
|
+
# end
|
63
|
+
#
|
40
64
|
# This plugin also adds a `UploadedFile#data_uri` method (and `#base64`),
|
41
65
|
# which returns a base64-encoded data URI of any UploadedFile:
|
42
66
|
#
|
43
|
-
#
|
44
|
-
#
|
67
|
+
# uploaded_file.data_uri #=> "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
68
|
+
# uploaded_file.base64 #=> "iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
45
69
|
#
|
46
70
|
# [data URIs]: https://tools.ietf.org/html/rfc2397
|
47
71
|
# [HTML5 Canvas]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
|
48
72
|
module DataUri
|
49
|
-
|
73
|
+
class ParseError < Error; end
|
74
|
+
|
75
|
+
DATA_REGEXP = /data:/
|
76
|
+
MEDIA_TYPE_REGEXP = /[-\w.+]+\/[-\w.+]+(;[-\w.+]+=[^;,]+)*/
|
77
|
+
BASE64_REGEXP = /;base64/
|
78
|
+
CONTENT_SEPARATOR = /,/
|
50
79
|
DEFAULT_CONTENT_TYPE = "text/plain"
|
51
|
-
DATA_URI_REGEXP = /\Adata:([-\w.+]+\/[-\w.+]+)?(;base64)?,(.*)\z/m
|
52
80
|
|
53
81
|
def self.configure(uploader, opts = {})
|
54
82
|
uploader.opts[:data_uri_filename] = opts.fetch(:filename, uploader.opts[:data_uri_filename])
|
55
83
|
uploader.opts[:data_uri_error_message] = opts.fetch(:error_message, uploader.opts[:data_uri_error_message])
|
56
84
|
end
|
57
85
|
|
86
|
+
module ClassMethods
|
87
|
+
# Parses the given data URI and creates an IO object from it.
|
88
|
+
#
|
89
|
+
# Shrine.data_uri("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")
|
90
|
+
# #=> #<Shrine::Plugins::DataUri::DataFile>
|
91
|
+
def data_uri(uri)
|
92
|
+
info = parse_data_uri(uri)
|
93
|
+
|
94
|
+
content_type = info[:content_type] || DEFAULT_CONTENT_TYPE
|
95
|
+
content = info[:base64] ? Base64.decode64(info[:data]) : CGI.unescape(info[:data])
|
96
|
+
filename = opts[:data_uri_filename]
|
97
|
+
filename = filename.call(content_type) if filename
|
98
|
+
|
99
|
+
data_file = DataFile.new(content, content_type: content_type, filename: filename)
|
100
|
+
info[:data].clear
|
101
|
+
|
102
|
+
data_file
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def parse_data_uri(uri)
|
108
|
+
scanner = StringScanner.new(uri)
|
109
|
+
scanner.scan(DATA_REGEXP) or raise ParseError, "data URI has invalid format"
|
110
|
+
media_type = scanner.scan(MEDIA_TYPE_REGEXP)
|
111
|
+
base64 = scanner.scan(BASE64_REGEXP)
|
112
|
+
scanner.scan(CONTENT_SEPARATOR) or raise ParseError, "data URI has invalid format"
|
113
|
+
|
114
|
+
content_type = media_type[/^[^;]+/] if media_type
|
115
|
+
|
116
|
+
{
|
117
|
+
content_type: content_type,
|
118
|
+
base64: !!base64,
|
119
|
+
data: scanner.post_match,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
58
124
|
module AttachmentMethods
|
59
125
|
def initialize(*)
|
60
126
|
super
|
@@ -79,19 +145,13 @@ class Shrine
|
|
79
145
|
def data_uri=(uri)
|
80
146
|
return if uri == ""
|
81
147
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
else
|
90
|
-
message = shrine_class.opts[:data_uri_error_message] || DEFAULT_ERROR_MESSAGE
|
91
|
-
message = message.call(uri) if message.respond_to?(:call)
|
92
|
-
errors.replace [message]
|
93
|
-
@data_uri = uri
|
94
|
-
end
|
148
|
+
data_file = shrine_class.data_uri(uri)
|
149
|
+
assign(data_file)
|
150
|
+
rescue ParseError => error
|
151
|
+
message = shrine_class.opts[:data_uri_error_message] || error.message
|
152
|
+
message = message.call(uri) if message.respond_to?(:call)
|
153
|
+
errors.replace [message]
|
154
|
+
@data_uri = uri
|
95
155
|
end
|
96
156
|
|
97
157
|
# Form builders require the reader as well.
|
@@ -108,18 +168,32 @@ class Shrine
|
|
108
168
|
|
109
169
|
# Returns contents of the file base64-encoded.
|
110
170
|
def base64
|
111
|
-
|
112
|
-
Base64.encode64(
|
171
|
+
binary = open { |io| io.read }
|
172
|
+
result = Base64.encode64(binary).chomp
|
173
|
+
binary.clear # deallocate string
|
174
|
+
result
|
113
175
|
end
|
114
176
|
end
|
115
177
|
|
116
|
-
class DataFile
|
178
|
+
class DataFile
|
117
179
|
attr_reader :content_type, :original_filename
|
118
180
|
|
119
181
|
def initialize(content, content_type: nil, filename: nil)
|
120
|
-
@content_type
|
182
|
+
@content_type = content_type
|
121
183
|
@original_filename = filename
|
122
|
-
|
184
|
+
@io = StringIO.new(content)
|
185
|
+
end
|
186
|
+
|
187
|
+
def to_io
|
188
|
+
@io
|
189
|
+
end
|
190
|
+
|
191
|
+
extend Forwardable
|
192
|
+
delegate Shrine::IO_METHODS.keys => :@io
|
193
|
+
|
194
|
+
def close
|
195
|
+
@io.close
|
196
|
+
@io.string.clear # deallocate string
|
123
197
|
end
|
124
198
|
end
|
125
199
|
end
|