shrine 2.1.1 → 2.2.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 +214 -248
- data/doc/carrierwave.md +11 -22
- data/doc/changing_location.md +3 -3
- data/doc/creating_storages.md +48 -21
- data/doc/direct_s3.md +37 -29
- data/doc/paperclip.md +7 -9
- data/doc/refile.md +8 -11
- data/doc/regenerating_versions.md +14 -19
- data/lib/shrine.rb +53 -23
- data/lib/shrine/plugins/activerecord.rb +36 -15
- data/lib/shrine/plugins/add_metadata.rb +50 -0
- data/lib/shrine/plugins/backgrounding.rb +22 -13
- data/lib/shrine/plugins/backup.rb +4 -3
- data/lib/shrine/plugins/data_uri.rb +1 -1
- data/lib/shrine/plugins/delete_promoted.rb +1 -1
- data/lib/shrine/plugins/delete_raw.rb +1 -1
- data/lib/shrine/plugins/determine_mime_type.rb +12 -3
- data/lib/shrine/plugins/direct_upload.rb +55 -65
- data/lib/shrine/plugins/download_endpoint.rb +7 -3
- data/lib/shrine/plugins/logging.rb +1 -1
- data/lib/shrine/plugins/moving.rb +6 -5
- data/lib/shrine/plugins/processing.rb +50 -0
- data/lib/shrine/plugins/rack_file.rb +3 -0
- data/lib/shrine/plugins/recache.rb +8 -12
- data/lib/shrine/plugins/remove_invalid.rb +1 -1
- data/lib/shrine/plugins/restore_cached_data.rb +1 -3
- data/lib/shrine/plugins/sequel.rb +40 -19
- data/lib/shrine/plugins/versions.rb +22 -23
- data/lib/shrine/storage/file_system.rb +0 -5
- data/lib/shrine/storage/linter.rb +4 -9
- data/lib/shrine/storage/s3.rb +28 -13
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +3 -2
- metadata +24 -9
@@ -1,7 +1,5 @@
|
|
1
1
|
require "roda"
|
2
|
-
|
3
2
|
require "json"
|
4
|
-
require "securerandom"
|
5
3
|
|
6
4
|
class Shrine
|
7
5
|
module Plugins
|
@@ -10,18 +8,33 @@ class Shrine
|
|
10
8
|
#
|
11
9
|
# plugin :direct_upload
|
12
10
|
#
|
13
|
-
#
|
11
|
+
# The Roda endpoint provides two routes:
|
12
|
+
#
|
13
|
+
# * `POST /:storage/upload`
|
14
|
+
# * `GET /:storage/presign`
|
15
|
+
#
|
16
|
+
# This first route is for doing direct uploads to your app, the received
|
17
|
+
# file will be uploaded the underlying storage. The second route is for
|
18
|
+
# doing direct uploads to a 3rd-party service, it will return the URL where
|
19
|
+
# the file can be uploaded to, along with the necessary request parameters.
|
20
|
+
#
|
21
|
+
# This is how you can mount the endpoint in a Rails application:
|
14
22
|
#
|
15
23
|
# Rails.application.routes.draw do
|
16
|
-
# mount ImageUploader::UploadEndpoint => "/
|
24
|
+
# mount ImageUploader::UploadEndpoint => "/images"
|
17
25
|
# end
|
18
26
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
27
|
+
# Now your application will get `POST /images/cache/upload` and `GET
|
28
|
+
# /images/cache/presign` routes. Whether you upload files to your app or to
|
29
|
+
# to a 3rd-party service, you'll probably want to use a JavaScript file
|
30
|
+
# upload library like [jQuery-File-Upload] or [Dropzone].
|
31
|
+
#
|
32
|
+
# ## Uploads
|
23
33
|
#
|
24
|
-
#
|
34
|
+
# The upload route accepts a "file" query parameter, and returns the
|
35
|
+
# uploaded file in JSON format:
|
36
|
+
#
|
37
|
+
# # POST /images/cache/upload
|
25
38
|
# {
|
26
39
|
# "id": "43kewit94.jpg",
|
27
40
|
# "storage": "cache",
|
@@ -32,14 +45,13 @@ class Shrine
|
|
32
45
|
# }
|
33
46
|
# }
|
34
47
|
#
|
48
|
+
# Once you've uploaded the file, you can assign the result to the hidden
|
49
|
+
# attachment field in the form, or immediately send it to the server.
|
50
|
+
#
|
35
51
|
# Note that the endpoint uploads the file standalone, without any knowledge
|
36
52
|
# of the record, so `context[:record]` and `context[:name]` will be nil.
|
37
53
|
#
|
38
|
-
#
|
39
|
-
# hidden attachment field in the form. There are many great JavaScript
|
40
|
-
# libraries for file uploads, most popular being [jQuery-File-Upload].
|
41
|
-
#
|
42
|
-
# ## Limiting filesize
|
54
|
+
# ### Limiting filesize
|
43
55
|
#
|
44
56
|
# It's good idea to limit the maximum filesize of uploaded files, if you
|
45
57
|
# set the `:max_size` option, files which are too big will get
|
@@ -47,22 +59,15 @@ class Shrine
|
|
47
59
|
#
|
48
60
|
# plugin :direct_upload, max_size: 5*1024*1024 # 5 MB
|
49
61
|
#
|
50
|
-
# Note that this option doesn't affect presigned uploads,
|
51
|
-
# limit
|
52
|
-
#
|
53
|
-
# ## Presigning
|
54
|
-
#
|
55
|
-
# An alternative to the direct endpoint is uploading directly to the
|
56
|
-
# underlying storage (currently only supported by Amazon S3). These uploads
|
57
|
-
# usually require extra information from the server, you can enable that
|
58
|
-
# route by passing `presign: true`:
|
62
|
+
# Note that this option doesn't affect presigned uploads, there you can
|
63
|
+
# apply filesize limit when generating a presign.
|
59
64
|
#
|
60
|
-
#
|
65
|
+
# ## Presigns
|
61
66
|
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
# looks something like this:
|
67
|
+
# The presign route returns the URL to the 3rd-party service to which you
|
68
|
+
# can upload the file, along with the necessary query parameters.
|
65
69
|
#
|
70
|
+
# # GET /images/cache/presign
|
66
71
|
# {
|
67
72
|
# "url" => "https://my-bucket.s3-eu-west-1.amazonaws.com",
|
68
73
|
# "fields" => {
|
@@ -75,39 +80,40 @@ class Shrine
|
|
75
80
|
# }
|
76
81
|
# }
|
77
82
|
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
# generated randomly without an extension, but you can add it:
|
83
|
+
# If you want that the generated location includes a file extension, you
|
84
|
+
# can specify the `extension` query parameter: `GET
|
85
|
+
# /:storage/presign?extension=.png`.
|
82
86
|
#
|
83
|
-
#
|
87
|
+
# You can also completely change how the key is generated, with
|
88
|
+
# `:presign_location`:
|
84
89
|
#
|
85
|
-
#
|
90
|
+
# plugin :direct_upload, presign_location: ->(request) { "${filename}" }
|
86
91
|
#
|
87
|
-
#
|
92
|
+
# This presign route internally calls `#presign` on the storage, which also
|
93
|
+
# accepts some service-specific options. You can generate these additional
|
94
|
+
# options per-request with `:presign_options`:
|
88
95
|
#
|
89
|
-
#
|
90
|
-
# can pass `:presign_options` with a hash or a block (which gets yielded
|
91
|
-
# Roda's request object):
|
96
|
+
# plugin :direct_upload, presign_options: {acl: "public-read"}
|
92
97
|
#
|
93
|
-
# plugin :direct_upload,
|
94
|
-
#
|
95
|
-
# plugin :direct_upload, presign: true, presign_options: ->(request) do
|
98
|
+
# plugin :direct_upload, presign_options: ->(request) do
|
96
99
|
# options = {}
|
97
100
|
# options[:content_length_range] = 0..(5*1024*1024) # limit the filesize to 5 MB
|
98
101
|
# options[:content_type] = request.params["content_type"] # use "content_type" query parameter
|
99
102
|
# options
|
100
103
|
# end
|
101
104
|
#
|
105
|
+
# Both `:presign_location` and `:presign_options` in their block versions
|
106
|
+
# are yielded an instance of [`Roda::Request`].
|
107
|
+
#
|
102
108
|
# See the [Direct Uploads to S3] guide for further instructions on how to
|
103
109
|
# hook the presigned uploads to a form.
|
104
110
|
#
|
105
111
|
# ### Testing presigns
|
106
112
|
#
|
107
|
-
# If you want to test presigned uploads, but don't want to
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
113
|
+
# If you want to test presigned uploads, but don't want to use Amazon S3 in
|
114
|
+
# tests for performance reasons, you can simply swap out S3 with a storage
|
115
|
+
# like FileSystem. The presigns will still get generated, but will simply
|
116
|
+
# point to this endpoint's upload route instead.
|
111
117
|
#
|
112
118
|
# ## Allowed storages
|
113
119
|
#
|
@@ -117,18 +123,6 @@ class Shrine
|
|
117
123
|
#
|
118
124
|
# plugin :direct_upload, allowed_storages: [:cache, :store]
|
119
125
|
#
|
120
|
-
# ## Authentication
|
121
|
-
#
|
122
|
-
# If you want to authenticate the endpoint, you should be able to do it
|
123
|
-
# easily if your web framework has a good enough router. For example, in
|
124
|
-
# Rails you could add a `constraints` directive:
|
125
|
-
#
|
126
|
-
# Rails.application.routes.draw do
|
127
|
-
# constraints(->(r){r.env["warden"].authenticate!}) do
|
128
|
-
# mount ImageUploader::UploadEndpoint => "/attachments/images"
|
129
|
-
# end
|
130
|
-
# end
|
131
|
-
#
|
132
126
|
# ## Customizing endpoint
|
133
127
|
#
|
134
128
|
# Since the endpoint is a [Roda] app, it can be easily customized via
|
@@ -150,6 +144,7 @@ class Shrine
|
|
150
144
|
#
|
151
145
|
# [Roda]: https://github.com/jeremyevans/roda
|
152
146
|
# [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
|
147
|
+
# [Dropzone]: https://github.com/enyo/dropzone
|
153
148
|
# [supports]: https://github.com/blueimp/jQuery-File-Upload/wiki/Options#progress
|
154
149
|
# ["accept" attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept
|
155
150
|
# [`Roda::RodaRequest`]: http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Base/RequestMethods.html
|
@@ -161,7 +156,6 @@ class Shrine
|
|
161
156
|
|
162
157
|
def self.configure(uploader, opts = {})
|
163
158
|
uploader.opts[:direct_upload_allowed_storages] = opts.fetch(:allowed_storages, uploader.opts.fetch(:direct_upload_allowed_storages, [:cache]))
|
164
|
-
uploader.opts[:direct_upload_presign] = opts.fetch(:presign, uploader.opts[:direct_upload_presign])
|
165
159
|
uploader.opts[:direct_upload_presign_options] = opts.fetch(:presign_options, uploader.opts.fetch(:direct_upload_presign_options, {}))
|
166
160
|
uploader.opts[:direct_upload_presign_location] = opts.fetch(:presign_location, uploader.opts[:direct_upload_presign_location])
|
167
161
|
uploader.opts[:direct_upload_max_size] = opts.fetch(:max_size, uploader.opts[:direct_upload_max_size])
|
@@ -201,7 +195,7 @@ class Shrine
|
|
201
195
|
uploaded_file = upload(file, context)
|
202
196
|
|
203
197
|
json uploaded_file
|
204
|
-
end
|
198
|
+
end
|
205
199
|
|
206
200
|
r.get "presign" do
|
207
201
|
location = get_presign_location
|
@@ -210,7 +204,7 @@ class Shrine
|
|
210
204
|
presign_data = generate_presign(location, options)
|
211
205
|
|
212
206
|
json presign_data
|
213
|
-
end
|
207
|
+
end
|
214
208
|
end
|
215
209
|
end
|
216
210
|
|
@@ -226,14 +220,14 @@ class Shrine
|
|
226
220
|
|
227
221
|
# Retrieves the context for the upload.
|
228
222
|
def get_context(name)
|
229
|
-
context = {phase: :cache}
|
223
|
+
context = {action: :cache, phase: :cache}
|
230
224
|
|
231
225
|
if name != "upload"
|
232
226
|
warn "The \"POST /:storage/:name\" route of the direct_upload Shrine plugin is deprecated, and it will be removed in Shrine 3. Use \"POST /:storage/upload\" instead."
|
233
227
|
context[:name] = name
|
234
228
|
end
|
235
229
|
|
236
|
-
|
230
|
+
unless presign_storage?
|
237
231
|
context[:location] = request.params["key"]
|
238
232
|
end
|
239
233
|
|
@@ -339,10 +333,6 @@ class Shrine
|
|
339
333
|
shrine_class.opts[:direct_upload_allowed_storages]
|
340
334
|
end
|
341
335
|
|
342
|
-
def presign?
|
343
|
-
shrine_class.opts[:direct_upload_presign]
|
344
|
-
end
|
345
|
-
|
346
336
|
def presign_options
|
347
337
|
shrine_class.opts[:direct_upload_presign_options]
|
348
338
|
end
|
@@ -25,7 +25,8 @@ class Shrine
|
|
25
25
|
# user.avatar.url #=> "/attachments/store/sdg0lsf8.jpg"
|
26
26
|
#
|
27
27
|
# :storages
|
28
|
-
# : An array of storage keys which the download endpoint should be
|
28
|
+
# : An array of storage keys which the download endpoint should be applied
|
29
|
+
# on.
|
29
30
|
#
|
30
31
|
# :prefix
|
31
32
|
# : The location where the download endpoint was mounted. If it was
|
@@ -110,17 +111,20 @@ class Shrine
|
|
110
111
|
io = get_stream_io(id)
|
111
112
|
response["Content-Length"] = io.size.to_s if io.size
|
112
113
|
|
113
|
-
|
114
|
+
body = Enumerator.new do |y|
|
114
115
|
if io.respond_to?(:each_chunk)
|
115
116
|
io.each_chunk { |chunk| y.yield(chunk) }
|
116
117
|
else
|
117
118
|
y.yield io.read(16*1024, buffer ||= "") until io.eof?
|
118
119
|
end
|
120
|
+
end
|
121
|
+
|
122
|
+
proxy = Rack::BodyProxy.new(body) do
|
119
123
|
io.close
|
120
124
|
io.delete if io.class.name == "Tempfile"
|
121
125
|
end
|
122
126
|
|
123
|
-
r.halt response.finish_with_body(
|
127
|
+
r.halt response.finish_with_body(proxy)
|
124
128
|
end
|
125
129
|
end
|
126
130
|
end
|
@@ -1,14 +1,15 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
|
-
# The moving plugin
|
4
|
-
#
|
5
|
-
# instantaneous regardless of the filesize, so
|
6
|
-
#
|
3
|
+
# The moving plugin will *move* files to storages instead of copying them,
|
4
|
+
# when the storage supports it. For FileSystem this will issue a `mv`
|
5
|
+
# command, which is instantaneous regardless of the filesize, so in that
|
6
|
+
# case loading this plugin can significantly speed up the attachment
|
7
|
+
# process.
|
7
8
|
#
|
8
9
|
# plugin :moving
|
9
10
|
#
|
10
11
|
# By default files will be moved whenever the storage supports it. If you
|
11
|
-
# want moving to happen only for certain storages, you can set
|
12
|
+
# want moving to happen only for certain storages, you can set `:storages`:
|
12
13
|
#
|
13
14
|
# plugin :moving, storages: [:cache]
|
14
15
|
module Moving
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Shrine
|
2
|
+
module Plugins
|
3
|
+
# The process plugin allows you to declaratively define
|
4
|
+
# file processing for specified actions, allowing you to transform
|
5
|
+
#
|
6
|
+
# def process(io, context)
|
7
|
+
# if context[:action] == :store
|
8
|
+
# # ...
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# into
|
13
|
+
#
|
14
|
+
# process(:store) do |io, context|
|
15
|
+
# # ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# The declarations are additive and inherited, so for the same action you
|
19
|
+
# can declare multiple blocks, and they will be performed in the same order,
|
20
|
+
# where output from previous will be input to next. You can return `nil`
|
21
|
+
# in any block to signal that no processing was performed and that the
|
22
|
+
# original file should be used.
|
23
|
+
module Processing
|
24
|
+
def self.configure(uploader)
|
25
|
+
uploader.opts[:processing] = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def process(action, &block)
|
30
|
+
opts[:processing][action] ||= []
|
31
|
+
opts[:processing][action] << block
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InstanceMethods
|
36
|
+
def process(io, context = {})
|
37
|
+
pipeline = opts[:processing][context[:action]] || []
|
38
|
+
|
39
|
+
result = pipeline.inject(io) do |input, processing|
|
40
|
+
instance_exec(input, context, &processing) || input
|
41
|
+
end
|
42
|
+
|
43
|
+
result unless result == io
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
register_plugin(:processing, Processing)
|
49
|
+
end
|
50
|
+
end
|
@@ -20,6 +20,9 @@ class Shrine
|
|
20
20
|
# and this is what is passed to `Shrine#upload`.
|
21
21
|
#
|
22
22
|
# plugin :rack_file
|
23
|
+
#
|
24
|
+
# Note that this plugin is not needed in Rails applications, because Rails
|
25
|
+
# already wraps Rack uploaded files in `ActionDispatch::Http::UploadedFile`.
|
23
26
|
module RackFile
|
24
27
|
module AttacherMethods
|
25
28
|
# Checks whether a file is a Rack file hash, and in that case wraps the
|
@@ -6,25 +6,21 @@ class Shrine
|
|
6
6
|
# the user immediately sees them) and other versions you want to generate
|
7
7
|
# in the promotion phase in a background job.
|
8
8
|
#
|
9
|
-
#
|
9
|
+
# plugin :recache
|
10
|
+
# plugin :processing
|
10
11
|
#
|
11
|
-
#
|
12
|
-
#
|
12
|
+
# process(:recache) do |io, context|
|
13
|
+
# # perform cheap processing
|
14
|
+
# end
|
13
15
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# when :recache
|
17
|
-
# # generate cheap versions
|
18
|
-
# when :store
|
19
|
-
# # generate more expensive versions
|
20
|
-
# end
|
21
|
-
# end
|
16
|
+
# process(:store) do |io, context|
|
17
|
+
# # perform more expensive processing
|
22
18
|
# end
|
23
19
|
module Recache
|
24
20
|
module AttacherMethods
|
25
21
|
def save
|
26
22
|
if get && cache.uploaded?(get)
|
27
|
-
_set cache!(get,
|
23
|
+
_set cache!(get, action: :recache)
|
28
24
|
end
|
29
25
|
super
|
30
26
|
end
|
@@ -12,10 +12,8 @@ class Shrine
|
|
12
12
|
|
13
13
|
def assign_cached(cached_file)
|
14
14
|
uploaded_file(cached_file) do |file|
|
15
|
-
file.
|
16
|
-
real_metadata = cache.extract_metadata(file, context)
|
15
|
+
real_metadata = file.open { cache.extract_metadata(file, context) }
|
17
16
|
file.metadata.update(real_metadata)
|
18
|
-
file.close
|
19
17
|
end
|
20
18
|
|
21
19
|
super(cached_file)
|
@@ -41,6 +41,11 @@ class Shrine
|
|
41
41
|
# end
|
42
42
|
# end
|
43
43
|
#
|
44
|
+
# If you don't want callbacks (e.g. you want to use the attacher object
|
45
|
+
# directly), you can turn them off:
|
46
|
+
#
|
47
|
+
# plugin :sequel, callbacks: false
|
48
|
+
#
|
44
49
|
# ## Validations
|
45
50
|
#
|
46
51
|
# Additionally, any Shrine validation errors will added to Sequel's
|
@@ -51,36 +56,52 @@ class Shrine
|
|
51
56
|
# include ImageUploader[:avatar]
|
52
57
|
# validates_presence_of :avatar
|
53
58
|
# end
|
59
|
+
#
|
60
|
+
# If you're doing validation separately from your models, you can turn off
|
61
|
+
# validations for your models:
|
62
|
+
#
|
63
|
+
# plugin :sequel, validations: false
|
54
64
|
module Sequel
|
65
|
+
def self.configure(uploader, opts = {})
|
66
|
+
uploader.opts[:sequel_callbacks] = opts.fetch(:callbacks, uploader.opts.fetch(:sequel_callbacks, true))
|
67
|
+
uploader.opts[:sequel_validations] = opts.fetch(:validations, uploader.opts.fetch(:sequel_validations, true))
|
68
|
+
end
|
69
|
+
|
55
70
|
module AttachmentMethods
|
56
71
|
def included(model)
|
57
72
|
super
|
58
73
|
|
59
74
|
return unless model < ::Sequel::Model
|
60
75
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
76
|
+
if shrine_class.opts[:sequel_validations]
|
77
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
78
|
+
def validate
|
79
|
+
super
|
80
|
+
#{@name}_attacher.errors.each do |message|
|
81
|
+
errors.add(:#{@name}, message)
|
82
|
+
end
|
66
83
|
end
|
67
|
-
|
84
|
+
RUBY
|
85
|
+
end
|
68
86
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
87
|
+
if shrine_class.opts[:sequel_callbacks]
|
88
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
89
|
+
def before_save
|
90
|
+
super
|
91
|
+
#{@name}_attacher.save if #{@name}_attacher.attached?
|
92
|
+
end
|
73
93
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
94
|
+
def after_commit
|
95
|
+
super
|
96
|
+
#{@name}_attacher.finalize if #{@name}_attacher.attached?
|
97
|
+
end
|
78
98
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
99
|
+
def after_destroy_commit
|
100
|
+
super
|
101
|
+
#{@name}_attacher.destroy
|
102
|
+
end
|
103
|
+
RUBY
|
104
|
+
end
|
84
105
|
end
|
85
106
|
end
|
86
107
|
|