shrine 2.3.1 → 2.4.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 +212 -89
- data/doc/attacher.md +227 -0
- data/doc/design.md +5 -19
- data/doc/paperclip.md +2 -1
- data/doc/testing.md +266 -0
- data/lib/shrine.rb +220 -168
- data/lib/shrine/plugins/activerecord.rb +23 -14
- data/lib/shrine/plugins/backgrounding.rb +3 -3
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -5
- data/lib/shrine/plugins/copy.rb +10 -9
- data/lib/shrine/plugins/data_uri.rb +5 -0
- data/lib/shrine/plugins/default_url_options.rb +17 -4
- data/lib/shrine/plugins/direct_upload.rb +6 -11
- data/lib/shrine/plugins/download_endpoint.rb +8 -24
- data/lib/shrine/plugins/multi_delete.rb +1 -1
- data/lib/shrine/plugins/processing.rb +8 -0
- data/lib/shrine/plugins/remote_url.rb +5 -0
- data/lib/shrine/plugins/remove_attachment.rb +3 -10
- data/lib/shrine/plugins/sequel.rb +28 -25
- data/lib/shrine/plugins/versions.rb +12 -1
- data/lib/shrine/storage/file_system.rb +16 -14
- data/lib/shrine/storage/linter.rb +6 -0
- data/lib/shrine/storage/s3.rb +51 -25
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +2 -2
- metadata +8 -7
- data/lib/shrine/plugins/concatenation.rb +0 -73
@@ -11,9 +11,9 @@ class Shrine
|
|
11
11
|
#
|
12
12
|
# Now the attachment module will add additional callbacks to the model:
|
13
13
|
#
|
14
|
-
# *
|
15
|
-
# *
|
16
|
-
# *
|
14
|
+
# * "before save" -- Used by the `recache` plugin.
|
15
|
+
# * "after commit" (save) -- Promotes the attachment, deletes replaced ones.
|
16
|
+
# * "after commit" (destroy) -- Deletes the attachment.
|
17
17
|
#
|
18
18
|
# Note that ActiveRecord versions 3.x and 4.x have errors automatically
|
19
19
|
# silenced in hooks, which can make debugging more difficult, so it's
|
@@ -22,10 +22,6 @@ class Shrine
|
|
22
22
|
# # This is the default in ActiveRecord 5
|
23
23
|
# ActiveRecord::Base.raise_in_transactional_callbacks = true
|
24
24
|
#
|
25
|
-
# Also note that if your tests are wrapped in transactions, the
|
26
|
-
# `after_commit` callbacks won't get called, so in order to test uploading
|
27
|
-
# you should first disable transactions for those tests.
|
28
|
-
#
|
29
25
|
# If you want to put promoting/deleting into a background job, see the
|
30
26
|
# `backgrounding` plugin.
|
31
27
|
#
|
@@ -38,16 +34,15 @@ class Shrine
|
|
38
34
|
# before_save do
|
39
35
|
# if avatar_data_changed? && avatar_attacher.cached?
|
40
36
|
# # cached
|
41
|
-
#
|
42
|
-
#
|
43
|
-
# if avatar_data_changed? && avatar_attacher.stored?
|
37
|
+
# elsif avatar_data_changed? && avatar_attacher.stored?
|
44
38
|
# # promoted
|
45
39
|
# end
|
46
40
|
# end
|
47
41
|
# end
|
48
42
|
#
|
49
|
-
# If you don't want
|
50
|
-
#
|
43
|
+
# If you don't want the attachment module to add any callbacks to the
|
44
|
+
# model, and would instead prefer to call these actions manually, you can
|
45
|
+
# disable callbacks:
|
51
46
|
#
|
52
47
|
# plugin :activerecord, callbacks: false
|
53
48
|
#
|
@@ -62,8 +57,8 @@ class Shrine
|
|
62
57
|
# validates_presence_of :avatar
|
63
58
|
# end
|
64
59
|
#
|
65
|
-
# If
|
66
|
-
#
|
60
|
+
# If don't want the attachment module to merge file validations errors into
|
61
|
+
# model errors, you can disable it:
|
67
62
|
#
|
68
63
|
# plugin :activerecord, validations: false
|
69
64
|
module Activerecord
|
@@ -119,6 +114,20 @@ class Shrine
|
|
119
114
|
super
|
120
115
|
record.save(validate: false)
|
121
116
|
end
|
117
|
+
|
118
|
+
# If the data attribute represents a JSON column, it needs to receive a
|
119
|
+
# Hash.
|
120
|
+
def convert_before_write(value)
|
121
|
+
activerecord_json_column? ? value : super
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns true if the data attribute represents a JSON or JSONB column.
|
125
|
+
def activerecord_json_column?
|
126
|
+
return false unless record.is_a?(ActiveRecord::Base)
|
127
|
+
return false unless column = record.class.columns_hash[data_attribute.to_s]
|
128
|
+
|
129
|
+
[:json, :jsonb].include?(column.type)
|
130
|
+
end
|
122
131
|
end
|
123
132
|
end
|
124
133
|
|
@@ -1,8 +1,8 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
|
-
# The `backgrounding` plugin enables you to
|
4
|
-
# of files from record's lifecycle
|
5
|
-
#
|
3
|
+
# The `backgrounding` plugin enables you to move processing, storing and
|
4
|
+
# deleting of files from record's lifecycle into background jobs. This is
|
5
|
+
# generally useful if you're doing processing and/or your store is
|
6
6
|
# something other than Storage::FileSystem.
|
7
7
|
#
|
8
8
|
# Shrine.plugin :backgrounding
|
@@ -8,12 +8,13 @@ class Shrine
|
|
8
8
|
#
|
9
9
|
# The plugin adds `#cached_<attachment>_data` to the model, which returns
|
10
10
|
# the cached file as JSON, and should be used to set the value of the
|
11
|
-
# hidden form field
|
11
|
+
# hidden form field.
|
12
12
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
13
|
+
# @user.cached_avatar_data #=> '{"storage":"cache","id":"...","metadata":{...}}'
|
14
|
+
#
|
15
|
+
# This method delegates to `Attacher#read_cached`:
|
16
|
+
#
|
17
|
+
# attacher.read_cached #=> '{"storage":"cache","id":"...","metadata":{...}}'
|
17
18
|
module CachedAttachmentData
|
18
19
|
module AttachmentMethods
|
19
20
|
def initialize(*)
|
data/lib/shrine/plugins/copy.rb
CHANGED
@@ -23,7 +23,7 @@ class Shrine
|
|
23
23
|
def initialize_copy(record)
|
24
24
|
super
|
25
25
|
@#{@name}_attacher = nil # reload the attacher
|
26
|
-
|
26
|
+
#{@name}_attacher.send(:write, nil) # remove original attachment
|
27
27
|
#{@name}_attacher.copy(record.#{@name}_attacher)
|
28
28
|
end
|
29
29
|
RUBY
|
@@ -34,15 +34,16 @@ class Shrine
|
|
34
34
|
def copy(attacher)
|
35
35
|
options = {action: :copy, move: false}
|
36
36
|
|
37
|
-
if attacher.cached?
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
37
|
+
copied_attachment = if attacher.cached?
|
38
|
+
cache!(attacher.get, **options)
|
39
|
+
elsif attacher.stored?
|
40
|
+
store!(attacher.get, **options)
|
41
|
+
else
|
42
|
+
nil
|
43
|
+
end
|
44
44
|
|
45
|
-
|
45
|
+
@old = get
|
46
|
+
_set(copied_attachment)
|
46
47
|
end
|
47
48
|
end
|
48
49
|
end
|
@@ -18,6 +18,11 @@ class Shrine
|
|
18
18
|
# user.avatar.mime_type #=> "image/png"
|
19
19
|
# user.avatar.size #=> 43423
|
20
20
|
#
|
21
|
+
# You can also use `#data_uri=` and `#data_uri` methods directly on the
|
22
|
+
# `Shrine::Attacher`:
|
23
|
+
#
|
24
|
+
# attacher.data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
25
|
+
#
|
21
26
|
# If you want the uploaded file to have an extension, you can generate a
|
22
27
|
# filename based on the content type of the data URI:
|
23
28
|
#
|
@@ -5,8 +5,17 @@ class Shrine
|
|
5
5
|
#
|
6
6
|
# plugin :default_url_options, store: {download: true}
|
7
7
|
#
|
8
|
-
#
|
9
|
-
#
|
8
|
+
# You can also generate the default URL options dynamically by using a
|
9
|
+
# block, which will receive the UploadedFile object along with any options
|
10
|
+
# that were passed to `UploadedFile#url`.
|
11
|
+
#
|
12
|
+
# plugin :default_url_options, store: ->(io, **options) do
|
13
|
+
# {response_content_disposition: "attachment; filename=\"#{io.original_filename}\""}
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# In both cases the default options are merged with options passed to
|
17
|
+
# `UploadedFile#url`, and the latter will always have precedence over
|
18
|
+
# default options.
|
10
19
|
module DefaultUrlOptions
|
11
20
|
def self.configure(uploader, options = {})
|
12
21
|
uploader.opts[:default_url_options] = (uploader.opts[:default_url_options] || {}).merge(options)
|
@@ -14,14 +23,18 @@ class Shrine
|
|
14
23
|
|
15
24
|
module FileMethods
|
16
25
|
def url(**options)
|
26
|
+
default_options = default_url_options
|
27
|
+
default_options = default_options.call(self, **options) if default_options.respond_to?(:call)
|
28
|
+
default_options ||= {}
|
29
|
+
|
17
30
|
super(default_options.merge(options))
|
18
31
|
end
|
19
32
|
|
20
33
|
private
|
21
34
|
|
22
|
-
def
|
35
|
+
def default_url_options
|
23
36
|
options = shrine_class.opts[:default_url_options]
|
24
|
-
options[storage_key.to_sym]
|
37
|
+
options[storage_key.to_sym]
|
25
38
|
end
|
26
39
|
end
|
27
40
|
end
|
@@ -60,7 +60,9 @@ class Shrine
|
|
60
60
|
# plugin :direct_upload, max_size: 5*1024*1024 # 5 MB
|
61
61
|
#
|
62
62
|
# Note that this option doesn't affect presigned uploads, there you can
|
63
|
-
# apply filesize limit when generating a presign.
|
63
|
+
# apply filesize limit when generating a presign. The filesize constraint
|
64
|
+
# here is for security purposes, you should still perform file validations
|
65
|
+
# on attaching.
|
64
66
|
#
|
65
67
|
# ## Presigns
|
66
68
|
#
|
@@ -96,11 +98,11 @@ class Shrine
|
|
96
98
|
# plugin :direct_upload, presign_options: {acl: "public-read"}
|
97
99
|
#
|
98
100
|
# plugin :direct_upload, presign_options: ->(request) do
|
99
|
-
# filename = request.params["filename"]
|
101
|
+
# filename = request.params["filename"]
|
100
102
|
#
|
101
103
|
# {
|
102
|
-
# content_length_range: 0..(10*1024*1024),
|
103
|
-
# content_disposition: "attachment; filename
|
104
|
+
# content_length_range: 0..(10*1024*1024), # limit filesize to 10MB
|
105
|
+
# content_disposition: "attachment; filename=\"#{filename}\"", # download with original filename
|
104
106
|
# }
|
105
107
|
# end
|
106
108
|
#
|
@@ -111,13 +113,6 @@ class Shrine
|
|
111
113
|
# See the [Direct Uploads to S3] guide for further instructions on how to
|
112
114
|
# hook the presigned uploads to a form.
|
113
115
|
#
|
114
|
-
# ### Testing presigns
|
115
|
-
#
|
116
|
-
# If you want to test presigned uploads, but don't want to use Amazon S3 in
|
117
|
-
# tests for performance reasons, you can simply swap out S3 with a storage
|
118
|
-
# like FileSystem. The presigns will still get generated, but will simply
|
119
|
-
# point to this endpoint's upload route instead.
|
120
|
-
#
|
121
116
|
# ## Allowed storages
|
122
117
|
#
|
123
118
|
# By default only uploads to `:cache` are allowed, to prevent the
|
@@ -97,6 +97,8 @@ class Shrine
|
|
97
97
|
# and allowed. Afterwards it proceeds with the file download using
|
98
98
|
# streaming.
|
99
99
|
class App < Roda
|
100
|
+
plugin :streaming
|
101
|
+
|
100
102
|
route do |r|
|
101
103
|
r.on ":storage" do |storage_key|
|
102
104
|
@storage = get_storage(storage_key)
|
@@ -105,26 +107,19 @@ class Shrine
|
|
105
107
|
filename = request.path.split("/").last
|
106
108
|
extname = File.extname(filename)
|
107
109
|
|
108
|
-
response["Content-Disposition"] = "#{disposition}; filename
|
110
|
+
response["Content-Disposition"] = "#{disposition}; filename=\"#{filename}\""
|
109
111
|
response["Content-Type"] = Rack::Mime.mime_type(extname)
|
110
112
|
|
111
|
-
io =
|
113
|
+
io = storage.open(id)
|
112
114
|
response["Content-Length"] = io.size.to_s if io.size
|
113
115
|
|
114
|
-
|
115
|
-
if io.respond_to?(:each_chunk)
|
116
|
-
io.each_chunk { |chunk|
|
116
|
+
stream(callback: ->{io.close}) do |out|
|
117
|
+
if io.respond_to?(:each_chunk) # Down::ChunkedIO
|
118
|
+
io.each_chunk { |chunk| out << chunk }
|
117
119
|
else
|
118
|
-
|
120
|
+
out << io.read(16*1024, buffer ||= "") until io.eof?
|
119
121
|
end
|
120
122
|
end
|
121
|
-
|
122
|
-
proxy = Rack::BodyProxy.new(body) do
|
123
|
-
io.close
|
124
|
-
io.delete if io.class.name == "Tempfile"
|
125
|
-
end
|
126
|
-
|
127
|
-
r.halt response.finish_with_body(proxy)
|
128
123
|
end
|
129
124
|
end
|
130
125
|
end
|
@@ -138,17 +133,6 @@ class Shrine
|
|
138
133
|
shrine_class.find_storage(storage_key)
|
139
134
|
end
|
140
135
|
|
141
|
-
def get_stream_io(id)
|
142
|
-
if storage.respond_to?(:stream)
|
143
|
-
warn "Storage#stream is deprecated, in Shrine 3 the download_endpoint plugin will use only Storage#open. You should update your storage library."
|
144
|
-
stream = storage.enum_for(:stream, id)
|
145
|
-
chunks = Enumerator.new { |y| y << Array(stream.next)[0] }
|
146
|
-
Down::ChunkedIO.new(size: Array(stream.peek)[1], chunks: chunks)
|
147
|
-
else
|
148
|
-
storage.open(id)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
136
|
# Halts the request if storage is not allowed.
|
153
137
|
def allow_storage!(storage_key)
|
154
138
|
if !allowed_storages.map(&:to_s).include?(storage_key)
|
@@ -7,7 +7,7 @@ class Shrine
|
|
7
7
|
#
|
8
8
|
# This plugin allows you pass an array of files to `Shrine#delete`.
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# uploader.delete([file1, file2, file3])
|
11
11
|
#
|
12
12
|
# Now if you're using Storage::S3, deleting an array of files will issue a
|
13
13
|
# single HTTP request. Some other storages may support multi deletes as
|
@@ -20,6 +20,14 @@ class Shrine
|
|
20
20
|
# in any block to signal that no processing was performed and that the
|
21
21
|
# original file should be used.
|
22
22
|
#
|
23
|
+
# The `.process` call is just a shorthand for
|
24
|
+
#
|
25
|
+
# def process(io, context)
|
26
|
+
# if context[:action] == :store
|
27
|
+
# # ...
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
23
31
|
# If you want the result of processing to be multiple files, use the
|
24
32
|
# `versions` plugin.
|
25
33
|
module Processing
|
@@ -15,6 +15,11 @@ class Shrine
|
|
15
15
|
# user.avatar.size #=> 43423
|
16
16
|
# user.avatar.original_filename #=> "cool-image.png"
|
17
17
|
#
|
18
|
+
# You can also use `#remote_url=` and `#remote_url` methods directly on the
|
19
|
+
# `Shrine::Attacher`:
|
20
|
+
#
|
21
|
+
# attacher.remote_url = "http://example.com/cool-image.png"
|
22
|
+
#
|
18
23
|
# The file will by default be downloaded using [Down], which is a wrapper
|
19
24
|
# around the open-uri standard library. It's a good practice to limit the
|
20
25
|
# maximum filesize of the remote file:
|
@@ -7,16 +7,9 @@ class Shrine
|
|
7
7
|
#
|
8
8
|
# If for example your attachment is called "avatar", this plugin will add
|
9
9
|
# `#remove_avatar` and `#remove_avatar=` methods to your model. This allows
|
10
|
-
# you to
|
10
|
+
# you to add a form field for removing attachments:
|
11
11
|
#
|
12
|
-
#
|
13
|
-
# <%= f.hidden_field :avatar, value: @user.avatar_data %>
|
14
|
-
# <%= f.file_field :avatar %>
|
15
|
-
# Remove attachment: <%= f.check_box :remove_avatar %>
|
16
|
-
# <% end %>
|
17
|
-
#
|
18
|
-
# Now when the checkbox is ticked and the form is submitted, the attached
|
19
|
-
# file will be removed.
|
12
|
+
# form.check_box :remove_avatar
|
20
13
|
module RemoveAttachment
|
21
14
|
module AttachmentMethods
|
22
15
|
def initialize(*)
|
@@ -49,7 +42,7 @@ class Shrine
|
|
49
42
|
|
50
43
|
# Rails sends "0" or "false" if the checkbox hasn't been ticked.
|
51
44
|
def remove?
|
52
|
-
remove && remove != "" && remove !~ /\
|
45
|
+
remove && remove != "" && remove !~ /\A(0|false)\z/
|
53
46
|
end
|
54
47
|
end
|
55
48
|
end
|
@@ -11,13 +11,9 @@ class Shrine
|
|
11
11
|
#
|
12
12
|
# Now the attachment module will add additional callbacks to the model:
|
13
13
|
#
|
14
|
-
# *
|
15
|
-
# *
|
16
|
-
# *
|
17
|
-
#
|
18
|
-
# Also note that if your tests are wrapped in transactions, the
|
19
|
-
# `after_commit` callbacks won't get called, so in order to test uploading
|
20
|
-
# you should first disable transactions for those tests.
|
14
|
+
# * "before save" -- Used by the `recache` plugin.
|
15
|
+
# * "after commit" (save) -- Promotes the attachment, deletes replaced ones.
|
16
|
+
# * "after commit" (destroy) -- Deletes the attachment.
|
21
17
|
#
|
22
18
|
# If you want to put promoting/deleting into a background job, see the
|
23
19
|
# `backgrounding` plugin.
|
@@ -33,16 +29,15 @@ class Shrine
|
|
33
29
|
#
|
34
30
|
# if changed_columns.include?(:avatar) && avatar_attacher.cached?
|
35
31
|
# # cached
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# if changed_columns.include?(:avatar) && avatar_attacher.stored?
|
32
|
+
# elsif changed_columns.include?(:avatar) && avatar_attacher.stored?
|
39
33
|
# # promoted
|
40
34
|
# end
|
41
35
|
# end
|
42
36
|
# end
|
43
37
|
#
|
44
|
-
# If you don't want
|
45
|
-
#
|
38
|
+
# If you don't want the attachment module to add any callbacks to the
|
39
|
+
# model, and would instead prefer to call these actions manually, you can
|
40
|
+
# disable callbacks:
|
46
41
|
#
|
47
42
|
# plugin :sequel, callbacks: false
|
48
43
|
#
|
@@ -57,8 +52,8 @@ class Shrine
|
|
57
52
|
# validates_presence_of :avatar
|
58
53
|
# end
|
59
54
|
#
|
60
|
-
# If
|
61
|
-
#
|
55
|
+
# If don't want the attachment module to merge file validations errors into
|
56
|
+
# model errors, you can disable it:
|
62
57
|
#
|
63
58
|
# plugin :sequel, validations: false
|
64
59
|
module Sequel
|
@@ -90,14 +85,14 @@ class Shrine
|
|
90
85
|
#{@name}_attacher.save if #{@name}_attacher.attached?
|
91
86
|
end
|
92
87
|
|
93
|
-
def
|
88
|
+
def after_save
|
94
89
|
super
|
95
|
-
#{@name}_attacher.finalize if #{@name}_attacher.attached?
|
90
|
+
db.after_commit{#{@name}_attacher.finalize} if #{@name}_attacher.attached?
|
96
91
|
end
|
97
92
|
|
98
|
-
def
|
93
|
+
def after_destroy
|
99
94
|
super
|
100
|
-
#{@name}_attacher.destroy
|
95
|
+
db.after_commit{#{@name}_attacher.destroy} if #{@name}_attacher.read
|
101
96
|
end
|
102
97
|
RUBY
|
103
98
|
end
|
@@ -111,13 +106,6 @@ class Shrine
|
|
111
106
|
end
|
112
107
|
|
113
108
|
module AttacherMethods
|
114
|
-
# Support for Postgres JSON columns.
|
115
|
-
def read
|
116
|
-
value = super
|
117
|
-
value = value.to_hash if value.respond_to?(:to_hash)
|
118
|
-
value
|
119
|
-
end
|
120
|
-
|
121
109
|
private
|
122
110
|
|
123
111
|
# Saves the record after assignment, skipping validations.
|
@@ -125,6 +113,21 @@ class Shrine
|
|
125
113
|
super
|
126
114
|
record.save_changes(validate: false)
|
127
115
|
end
|
116
|
+
|
117
|
+
# If the data represents a JSON column with `pg_json` Sequel extension
|
118
|
+
# loaded, a `Sequel::Postgres::JSONHashBase` object will be returned,
|
119
|
+
# which we convert into a Hash.
|
120
|
+
def convert_after_read(value)
|
121
|
+
sequel_json_column? ? value.to_hash : super
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns true if the data attribute represents a JSON or JSONB column.
|
125
|
+
def sequel_json_column?
|
126
|
+
return false unless record.is_a?(::Sequel::Model)
|
127
|
+
return false unless column = record.class.db_schema[data_attribute]
|
128
|
+
|
129
|
+
[:json, :jsonb].include?(column[:type])
|
130
|
+
end
|
128
131
|
end
|
129
132
|
end
|
130
133
|
|