shrine 2.11.0 → 2.12.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 +28 -0
- data/README.md +25 -22
- data/doc/advantages.md +22 -10
- data/doc/carrierwave.md +13 -10
- data/doc/creating_storages.md +1 -1
- data/doc/direct_s3.md +6 -6
- data/doc/multiple_files.md +195 -51
- data/doc/paperclip.md +11 -8
- data/doc/processing.md +36 -29
- data/doc/refile.md +8 -7
- data/lib/shrine.rb +7 -7
- data/lib/shrine/plugins/data_uri.rb +1 -1
- data/lib/shrine/plugins/determine_mime_type.rb +3 -2
- data/lib/shrine/plugins/download_endpoint.rb +86 -37
- data/lib/shrine/plugins/dynamic_storage.rb +5 -1
- data/lib/shrine/plugins/parallelize.rb +0 -1
- data/lib/shrine/plugins/presign_endpoint.rb +1 -6
- data/lib/shrine/plugins/processing.rb +5 -9
- data/lib/shrine/plugins/rack_response.rb +1 -1
- data/lib/shrine/plugins/remote_url.rb +24 -10
- data/lib/shrine/plugins/signature.rb +1 -2
- data/lib/shrine/plugins/validation_helpers.rb +15 -2
- data/lib/shrine/plugins/versions.rb +18 -22
- data/lib/shrine/storage/file_system.rb +4 -3
- data/lib/shrine/storage/s3.rb +20 -12
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +8 -2
- metadata +18 -4
data/doc/paperclip.md
CHANGED
@@ -19,7 +19,8 @@ class Photo < ActiveRecord::Base
|
|
19
19
|
bucket: "my-bucket",
|
20
20
|
access_key_id: "abc",
|
21
21
|
secret_access_key: "xyz",
|
22
|
-
}
|
22
|
+
},
|
23
|
+
s3_region: "eu-west-1",
|
23
24
|
end
|
24
25
|
```
|
25
26
|
|
@@ -30,6 +31,7 @@ Shrine.storages[:store] = Shrine::Storage::S3.new(
|
|
30
31
|
bucket: "my-bucket",
|
31
32
|
access_key_id: "abc",
|
32
33
|
secret_access_key: "xyz",
|
34
|
+
region: "eu-west-1",
|
33
35
|
)
|
34
36
|
```
|
35
37
|
|
@@ -102,16 +104,17 @@ class ImageUploader < Shrine
|
|
102
104
|
plugin :versions
|
103
105
|
|
104
106
|
process(:store) do |io, context|
|
105
|
-
|
106
|
-
pipeline = ImageProcessing::MiniMagick.source(original)
|
107
|
+
versions = { original: io } # retain original
|
107
108
|
|
108
|
-
|
109
|
-
|
110
|
-
size_300 = pipeline.resize_to_limit!(300, 300)
|
109
|
+
io.download do |original|
|
110
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
111
111
|
|
112
|
-
|
112
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
113
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
114
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
115
|
+
end
|
113
116
|
|
114
|
-
|
117
|
+
versions # return the hash of processed files
|
115
118
|
end
|
116
119
|
end
|
117
120
|
```
|
data/doc/processing.md
CHANGED
@@ -91,7 +91,7 @@ defaults for the web.
|
|
91
91
|
Since we'll be storing multiple derivates of the original file, we'll need to
|
92
92
|
also load the `versions` plugin, which allows us to return a Hash of processed
|
93
93
|
files. For processing we'll be using the `ImageProcessing::MiniMagick` backend,
|
94
|
-
which performs processing with [ImageMagick]
|
94
|
+
which performs processing with [ImageMagick] or [GraphicsMagick].
|
95
95
|
|
96
96
|
```sh
|
97
97
|
$ brew install imagemagick
|
@@ -110,16 +110,17 @@ class ImageUploader < Shrine
|
|
110
110
|
plugin :delete_raw
|
111
111
|
|
112
112
|
process(:store) do |io, context|
|
113
|
-
|
114
|
-
pipeline = ImageProcessing::MiniMagick.source(original)
|
113
|
+
versions = { original: io } # retain original
|
115
114
|
|
116
|
-
|
117
|
-
|
118
|
-
size_300 = pipeline.resize_to_limit!(300, 300)
|
115
|
+
io.download do |original|
|
116
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
119
117
|
|
120
|
-
|
118
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
119
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
120
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
121
|
+
end
|
121
122
|
|
122
|
-
|
123
|
+
versions # return the hash of processed files
|
123
124
|
end
|
124
125
|
end
|
125
126
|
```
|
@@ -144,16 +145,17 @@ class ImageUploader < Shrine
|
|
144
145
|
plugin :delete_raw
|
145
146
|
|
146
147
|
process(:store) do |io, context|
|
147
|
-
|
148
|
-
pipeline = ImageProcessing::Vips.source(original)
|
148
|
+
versions = { original: io } # retain original
|
149
149
|
|
150
|
-
|
151
|
-
|
152
|
-
size_300 = pipeline.resize_to_limit!(300, 300)
|
150
|
+
io.download do |original|
|
151
|
+
pipeline = ImageProcessing::Vips.source(original) # instead of ImageProcessing::MiniMagick
|
153
152
|
|
154
|
-
|
153
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
154
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
155
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
156
|
+
end
|
155
157
|
|
156
|
-
|
158
|
+
versions # return the hash of processed files
|
157
159
|
end
|
158
160
|
end
|
159
161
|
```
|
@@ -218,23 +220,24 @@ class ImageUploader < Shrine
|
|
218
220
|
plugin :delete_raw
|
219
221
|
|
220
222
|
process(:store) do |io, context|
|
221
|
-
|
222
|
-
pipeline = ImageProcessing::Vips.source(original)
|
223
|
+
versions = { original: io } # retain original
|
223
224
|
|
224
|
-
|
225
|
-
|
226
|
-
pipeline = pipeline
|
227
|
-
.convert("jpeg")
|
228
|
-
.saver(interlace: true)
|
229
|
-
end
|
225
|
+
io.download do |original|
|
226
|
+
pipeline = ImageProcessing::Vips.source(original)
|
230
227
|
|
231
|
-
|
232
|
-
|
233
|
-
|
228
|
+
# Shrine::UploadedFile object contains information about the MIME type
|
229
|
+
unless %w[image/png].include?(io.mime_type)
|
230
|
+
pipeline = pipeline
|
231
|
+
.convert("jpeg")
|
232
|
+
.saver(interlace: true)
|
233
|
+
end
|
234
234
|
|
235
|
-
|
235
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
236
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
237
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
238
|
+
end
|
236
239
|
|
237
|
-
|
240
|
+
versions # return the hash of processed files
|
238
241
|
end
|
239
242
|
end
|
240
243
|
```
|
@@ -281,6 +284,7 @@ class VideoUploader < Shrine
|
|
281
284
|
|
282
285
|
{ original: io, transcoded: transcoded, screenshot: screenshot }
|
283
286
|
end
|
287
|
+
end
|
284
288
|
```
|
285
289
|
|
286
290
|
## On-the-fly processing
|
@@ -324,6 +328,7 @@ basic [configuration][Dragonfly configuration]:
|
|
324
328
|
Dragonfly.app.configure do
|
325
329
|
url_format "/attachments/:job"
|
326
330
|
secret "my secure secret" # used to generate the protective SHA
|
331
|
+
plugin :imagemagick
|
327
332
|
end
|
328
333
|
|
329
334
|
use Dragonfly::Middleware
|
@@ -340,6 +345,8 @@ updated):
|
|
340
345
|
|
341
346
|
```rb
|
342
347
|
Shrine::Storage::S3.new(upload_options: { acl: "public-read" }, **other_options)
|
348
|
+
# ...
|
349
|
+
Shrine.plugin :default_url_options, cache: { public: true }, store: { public: true }
|
343
350
|
```
|
344
351
|
|
345
352
|
Now you can generate Dragonfly URLs from `Shrine::UploadedFile` objects:
|
@@ -347,7 +354,7 @@ Now you can generate Dragonfly URLs from `Shrine::UploadedFile` objects:
|
|
347
354
|
```rb
|
348
355
|
def thumbnail_url(uploaded_file, dimensions)
|
349
356
|
Dragonfly.app
|
350
|
-
.fetch(uploaded_file.url
|
357
|
+
.fetch(uploaded_file.url)
|
351
358
|
.thumb(dimensions)
|
352
359
|
.url
|
353
360
|
end
|
data/doc/refile.md
CHANGED
@@ -72,16 +72,17 @@ class ImageUploader < Shrine
|
|
72
72
|
plugin :versions
|
73
73
|
|
74
74
|
process(:store) do |io, context|
|
75
|
-
|
76
|
-
pipeline = ImageProcessing::MiniMagick.source(original)
|
75
|
+
versions = { original: io } # retain original
|
77
76
|
|
78
|
-
|
79
|
-
|
80
|
-
size_300 = pipeline.resize_to_limit!(300, 300)
|
77
|
+
io.download do |original|
|
78
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
81
79
|
|
82
|
-
|
80
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
81
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
82
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
83
|
+
end
|
83
84
|
|
84
|
-
|
85
|
+
versions # return the hash of processed files
|
85
86
|
end
|
86
87
|
end
|
87
88
|
```
|
data/lib/shrine.rb
CHANGED
@@ -806,16 +806,17 @@ class Shrine
|
|
806
806
|
#
|
807
807
|
# # or
|
808
808
|
#
|
809
|
-
# uploaded_file.open { |io| io.read }
|
810
|
-
# #=> "..."
|
809
|
+
# uploaded_file.open { |io| io.read } # the IO is automatically closed
|
811
810
|
def open(*args)
|
812
|
-
|
811
|
+
@io.close if @io && !(@io.respond_to?(:closed?) && @io.closed?)
|
812
|
+
@io = storage.open(id, *args)
|
813
|
+
|
814
|
+
return @io unless block_given?
|
813
815
|
|
814
816
|
begin
|
815
|
-
@io = storage.open(id, *args)
|
816
817
|
yield @io
|
817
818
|
ensure
|
818
|
-
@io.close
|
819
|
+
@io.close
|
819
820
|
@io = nil
|
820
821
|
end
|
821
822
|
end
|
@@ -836,7 +837,6 @@ class Shrine
|
|
836
837
|
# # or
|
837
838
|
#
|
838
839
|
# uploaded_file.download { |tempfile| tempfile.read } # tempfile is deleted
|
839
|
-
# #=> "..."
|
840
840
|
def download(*args)
|
841
841
|
if storage.respond_to?(:download)
|
842
842
|
tempfile = storage.download(id, *args)
|
@@ -967,7 +967,7 @@ class Shrine
|
|
967
967
|
# Returns an opened IO object for the uploaded file by calling `#open`
|
968
968
|
# on the storage.
|
969
969
|
def io
|
970
|
-
@io
|
970
|
+
@io || open
|
971
971
|
end
|
972
972
|
end
|
973
973
|
end
|
@@ -151,7 +151,7 @@ class Shrine
|
|
151
151
|
# If it fails, it sets the error message and assigns the uri in an
|
152
152
|
# instance variable so that it shows up on the UI.
|
153
153
|
def data_uri=(uri)
|
154
|
-
return if uri == ""
|
154
|
+
return if uri == "" || uri.nil?
|
155
155
|
|
156
156
|
data_file = shrine_class.data_uri(uri)
|
157
157
|
assign(data_file)
|
@@ -177,13 +177,14 @@ class Shrine
|
|
177
177
|
|
178
178
|
status = thread.value
|
179
179
|
|
180
|
-
raise Error, stderr.read
|
180
|
+
raise Error, "file command failed to spawn: #{stderr.read}" if status.nil?
|
181
|
+
raise Error, "file command failed: #{stderr.read}" unless status.success?
|
181
182
|
$stderr.print(stderr.read)
|
182
183
|
|
183
184
|
stdout.read.strip
|
184
185
|
end
|
185
186
|
rescue Errno::ENOENT
|
186
|
-
raise Error, "
|
187
|
+
raise Error, "file command-line tool is not installed"
|
187
188
|
end
|
188
189
|
|
189
190
|
def extract_with_fastimage(io)
|
@@ -9,13 +9,15 @@ class Shrine
|
|
9
9
|
module Plugins
|
10
10
|
# The `download_endpoint` plugin provides a Rack endpoint for downloading
|
11
11
|
# uploaded files from specified storages. This can be useful when files
|
12
|
-
# from your
|
12
|
+
# from your storage isn't accessible over URL (e.g. database storages) or
|
13
13
|
# if you want to authenticate your downloads. It requires the [Roda] gem.
|
14
14
|
#
|
15
|
-
#
|
15
|
+
# You can configure the plugin with the path prefix which the endpoint will
|
16
|
+
# be mounted on.
|
16
17
|
#
|
17
|
-
#
|
18
|
-
#
|
18
|
+
# plugin :download_endpoint, prefix: "attachments"
|
19
|
+
#
|
20
|
+
# The endpoint should then be mounted on the specified prefix:
|
19
21
|
#
|
20
22
|
# # config.ru (Rack)
|
21
23
|
# map "/attachments" do
|
@@ -29,38 +31,31 @@ class Shrine
|
|
29
31
|
# mount Shrine.download_endpoint => "/attachments"
|
30
32
|
# end
|
31
33
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
# to the endpoint for files uploaded to specified storages:
|
34
|
+
# Any uploaded file can be downloaded through this endpoint. When a file is
|
35
|
+
# requested, its content will be efficiently streamed from the storage into
|
36
|
+
# the response body.
|
36
37
|
#
|
37
|
-
#
|
38
|
+
# Links to the download endpoint are generated by calling
|
39
|
+
# `UploadedFile#download_url` instead of the usual `UploadedFile#url`.
|
38
40
|
#
|
39
|
-
#
|
40
|
-
# : An array of storage keys for which `UploadedFile#url` should generate
|
41
|
-
# download endpoint URLs.
|
41
|
+
# uploaded_file.download_url #=> "/attachments/eyJpZCI6ImFkdzlyeTM5ODJpandoYWla"
|
42
42
|
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
43
|
+
# Note that streaming the file through your app might impact the request
|
44
|
+
# throughput of your app, depending on which web server is used. It's
|
45
|
+
# recommended to either configure a CDN to serve these files:
|
46
46
|
#
|
47
|
-
# :host
|
48
|
-
# : The host that you want the download URLs to use (e.g. your app's domain
|
49
|
-
# name or a CDN). By default URLs are relative.
|
47
|
+
# plugin :download_endpoint, host: "http://abc123.cloudfront.net"
|
50
48
|
#
|
51
|
-
# :
|
52
|
-
# : Can be set to "attachment" if you want that the user is always
|
53
|
-
# prompted to download the file when visiting the download URL.
|
54
|
-
# The default is "inline".
|
49
|
+
# or configure the endpoint to redirect to the direct file URL:
|
55
50
|
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
51
|
+
# plugin :download_endpoint, redirect: true
|
52
|
+
# # or
|
53
|
+
# plugin :download_endpoint, redirect: -> (uploaded_file, request) do
|
54
|
+
# # return URL which the request will redirect to
|
55
|
+
# end
|
60
56
|
#
|
61
|
-
#
|
62
|
-
# `rack_response` plugin
|
63
|
-
# from inside your router/controller.
|
57
|
+
# Alternatively, you can stream files yourself from your controller using
|
58
|
+
# the `rack_response` plugin, which this plugin uses internally.
|
64
59
|
#
|
65
60
|
# [Roda]: https://github.com/jeremyevans/roda
|
66
61
|
module DownloadEndpoint
|
@@ -68,13 +63,35 @@ class Shrine
|
|
68
63
|
uploader.plugin :rack_response
|
69
64
|
end
|
70
65
|
|
66
|
+
# Accepts the following options:
|
67
|
+
#
|
68
|
+
# :prefix
|
69
|
+
# : The location where the download endpoint was mounted. If it was
|
70
|
+
# mounted at the root level, this should be set to nil.
|
71
|
+
#
|
72
|
+
# :host
|
73
|
+
# : The host that you want the download URLs to use (e.g. your app's domain
|
74
|
+
# name or a CDN). By default URLs are relative.
|
75
|
+
#
|
76
|
+
# :disposition
|
77
|
+
# : Can be set to "attachment" if you want that the user is always
|
78
|
+
# prompted to download the file when visiting the download URL.
|
79
|
+
# The default is "inline".
|
80
|
+
#
|
81
|
+
# :redirect
|
82
|
+
# : If set to `true`, requests will redirect to the direct file URL. If
|
83
|
+
# set to a proc object, the proc will called with the `UploadedFile`
|
84
|
+
# instance and the `Rack::Request` object, and is expected to return
|
85
|
+
# the URL to which the request will redirect to. Defaults to `false`,
|
86
|
+
# meaning that the file content will be served through the endpoint.
|
71
87
|
def self.configure(uploader, opts = {})
|
72
88
|
uploader.opts[:download_endpoint_storages] = opts.fetch(:storages, uploader.opts[:download_endpoint_storages])
|
73
89
|
uploader.opts[:download_endpoint_prefix] = opts.fetch(:prefix, uploader.opts[:download_endpoint_prefix])
|
74
90
|
uploader.opts[:download_endpoint_disposition] = opts.fetch(:disposition, uploader.opts.fetch(:download_endpoint_disposition, "inline"))
|
75
91
|
uploader.opts[:download_endpoint_host] = opts.fetch(:host, uploader.opts[:download_endpoint_host])
|
92
|
+
uploader.opts[:download_endpoint_redirect] = opts.fetch(:redirect, uploader.opts.fetch(:download_endpoint_redirect, false))
|
76
93
|
|
77
|
-
|
94
|
+
Shrine.deprecation("The :storages download_endpoint option is deprecated, you should use UploadedFile#download_url for generating URLs to the download endpoint.") if uploader.opts[:download_endpoint_storages]
|
78
95
|
|
79
96
|
uploader.assign_download_endpoint(App) unless uploader.const_defined?(:DownloadEndpoint)
|
80
97
|
end
|
@@ -96,6 +113,7 @@ class Shrine
|
|
96
113
|
endpoint_class = Class.new(klass)
|
97
114
|
endpoint_class.opts[:shrine_class] = self
|
98
115
|
endpoint_class.opts[:disposition] = opts[:download_endpoint_disposition]
|
116
|
+
endpoint_class.opts[:redirect] = opts[:download_endpoint_redirect]
|
99
117
|
|
100
118
|
@download_endpoint = endpoint_class
|
101
119
|
|
@@ -113,19 +131,20 @@ class Shrine
|
|
113
131
|
# uploaded file's id. For other uploaded files that aren't in the list
|
114
132
|
# of storages it just returns their original URL.
|
115
133
|
def url(**options)
|
116
|
-
if
|
134
|
+
if download_storages && download_storages.include?(storage_key.to_sym)
|
135
|
+
Shrine.deprecation("The :storages option for download_endpoint plugin is deprecated and will be obsolete in Shrine 3. Use UploadedFile#download_url instead.")
|
117
136
|
download_url
|
118
137
|
else
|
119
138
|
super
|
120
139
|
end
|
121
140
|
end
|
122
141
|
|
123
|
-
private
|
124
|
-
|
125
142
|
def download_url
|
126
143
|
[download_host, *download_prefix, download_identifier].join("/")
|
127
144
|
end
|
128
145
|
|
146
|
+
private
|
147
|
+
|
129
148
|
# Generates URL-safe identifier from data, filtering only a subset of
|
130
149
|
# metadata that the endpoint needs to prevent the URL from being too
|
131
150
|
# long.
|
@@ -145,6 +164,10 @@ class Shrine
|
|
145
164
|
def download_prefix
|
146
165
|
shrine_class.opts[:download_endpoint_prefix]
|
147
166
|
end
|
167
|
+
|
168
|
+
def download_storages
|
169
|
+
shrine_class.opts[:download_endpoint_storages]
|
170
|
+
end
|
148
171
|
end
|
149
172
|
|
150
173
|
# Routes incoming requests. It first asserts that the storage is existent
|
@@ -156,21 +179,32 @@ class Shrine
|
|
156
179
|
r.on storage_names do |storage_name|
|
157
180
|
r.get /(.*)/ do |id|
|
158
181
|
data = { "id" => id, "storage" => storage_name, "metadata" => {} }
|
159
|
-
|
182
|
+
serve_file(data)
|
160
183
|
end
|
161
184
|
end
|
162
185
|
|
163
186
|
r.get /(.*)/ do |identifier|
|
164
187
|
data = serializer.load(identifier)
|
165
|
-
|
188
|
+
serve_file(data)
|
166
189
|
end
|
167
190
|
end
|
168
191
|
|
169
192
|
private
|
170
193
|
|
171
|
-
|
194
|
+
# Streams or redirects to the uploaded file.
|
195
|
+
def serve_file(data)
|
172
196
|
uploaded_file = get_uploaded_file(data)
|
173
|
-
|
197
|
+
|
198
|
+
if redirect
|
199
|
+
redirect_to_file(uploaded_file)
|
200
|
+
else
|
201
|
+
stream_file(uploaded_file)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Streams the uploaded file content.
|
206
|
+
def stream_file(uploaded_file)
|
207
|
+
range = env["HTTP_RANGE"]
|
174
208
|
|
175
209
|
status, headers, body = uploaded_file.to_rack_response(disposition: disposition, range: range)
|
176
210
|
headers["Cache-Control"] = "max-age=#{365*24*60*60}" # cache for a year
|
@@ -178,6 +212,17 @@ class Shrine
|
|
178
212
|
request.halt [status, headers, body]
|
179
213
|
end
|
180
214
|
|
215
|
+
# Redirects to the uploaded file's direct URL or the specified URL proc.
|
216
|
+
def redirect_to_file(uploaded_file)
|
217
|
+
if redirect == true
|
218
|
+
redirect_url = uploaded_file.url
|
219
|
+
else
|
220
|
+
redirect_url = redirect.call(uploaded_file, request)
|
221
|
+
end
|
222
|
+
|
223
|
+
request.redirect redirect_url
|
224
|
+
end
|
225
|
+
|
181
226
|
# Returns a Shrine::UploadedFile, or returns 404 if file doesn't exist.
|
182
227
|
def get_uploaded_file(data)
|
183
228
|
uploaded_file = shrine_class.uploaded_file(data)
|
@@ -207,6 +252,10 @@ class Shrine
|
|
207
252
|
shrine_class.download_endpoint_serializer
|
208
253
|
end
|
209
254
|
|
255
|
+
def redirect
|
256
|
+
opts[:redirect]
|
257
|
+
end
|
258
|
+
|
210
259
|
def disposition
|
211
260
|
opts[:disposition]
|
212
261
|
end
|