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.

@@ -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
- original = io.download
106
- pipeline = ImageProcessing::MiniMagick.source(original)
107
+ versions = { original: io } # retain original
107
108
 
108
- size_800 = pipeline.resize_to_limit!(800, 800)
109
- size_500 = pipeline.resize_to_limit!(500, 500)
110
- size_300 = pipeline.resize_to_limit!(300, 300)
109
+ io.download do |original|
110
+ pipeline = ImageProcessing::MiniMagick.source(original)
111
111
 
112
- original.close!
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
- { original: io, large: size_800, medium: size_500, small: size_300 }
117
+ versions # return the hash of processed files
115
118
  end
116
119
  end
117
120
  ```
@@ -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]/[GraphicsMagick].
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
- original = io.download
114
- pipeline = ImageProcessing::MiniMagick.source(original)
113
+ versions = { original: io } # retain original
115
114
 
116
- size_800 = pipeline.resize_to_limit!(800, 800)
117
- size_500 = pipeline.resize_to_limit!(500, 500)
118
- size_300 = pipeline.resize_to_limit!(300, 300)
115
+ io.download do |original|
116
+ pipeline = ImageProcessing::MiniMagick.source(original)
119
117
 
120
- original.close!
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
- { original: io, large: size_800, medium: size_500, small: size_300 }
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
- original = io.download
148
- pipeline = ImageProcessing::Vips.source(original)
148
+ versions = { original: io } # retain original
149
149
 
150
- size_800 = pipeline.resize_to_limit!(800, 800)
151
- size_500 = pipeline.resize_to_limit!(500, 500)
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
- original.close!
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
- { original: io, large: size_800, medium: size_500, small: size_300 }
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
- original = io.download
222
- pipeline = ImageProcessing::Vips.source(original)
223
+ versions = { original: io } # retain original
223
224
 
224
- # the `io` object contains the MIME type of the original file
225
- if io.mime_type != "image/png"
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
- size_800 = pipeline.resize_to_limit!(800, 800)
232
- size_500 = pipeline.resize_to_limit!(500, 500)
233
- size_300 = pipeline.resize_to_limit!(300, 300)
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
- original.close!
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
- { original: io, large: size_800, medium: size_500, small: size_300 }
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(public: true))
357
+ .fetch(uploaded_file.url)
351
358
  .thumb(dimensions)
352
359
  .url
353
360
  end
@@ -72,16 +72,17 @@ class ImageUploader < Shrine
72
72
  plugin :versions
73
73
 
74
74
  process(:store) do |io, context|
75
- original = io.download
76
- pipeline = ImageProcessing::MiniMagick.source(original)
75
+ versions = { original: io } # retain original
77
76
 
78
- size_800 = pipeline.resize_to_limit!(800, 800)
79
- size_500 = pipeline.resize_to_limit!(500, 500)
80
- size_300 = pipeline.resize_to_limit!(300, 300)
77
+ io.download do |original|
78
+ pipeline = ImageProcessing::MiniMagick.source(original)
81
79
 
82
- original.close!
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
- { original: io, large: size_800, medium: size_500, small: size_300 }
85
+ versions # return the hash of processed files
85
86
  end
86
87
  end
87
88
  ```
@@ -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
- return to_io unless block_given?
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 if @io
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 ||= storage.open(id)
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 unless status.success?
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, "The `file` command-line tool is not installed"
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 storages aren't accessible over URL (e.g. database storages) or
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
- # plugin :download_endpoint, storages: [:store], prefix: "attachments"
15
+ # You can configure the plugin with the path prefix which the endpoint will
16
+ # be mounted on.
16
17
  #
17
- # After loading the plugin the endpoint should be mounted on the specified
18
- # prefix:
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
- # Now all stored files can be downloaded through the endpoint, and the
33
- # endpoint will efficiently stream the file from the storage when the
34
- # storage supports it. `UploadedFile#url` will automatically return the URL
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
- # user.avatar.url #=> "/attachments/eyJpZCI6ImFkdzlyeTM5ODJpandoYWla"
38
+ # Links to the download endpoint are generated by calling
39
+ # `UploadedFile#download_url` instead of the usual `UploadedFile#url`.
38
40
  #
39
- # :storages
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
- # :prefix
44
- # : The location where the download endpoint was mounted. If it was
45
- # mounted at the root level, this should be set to nil.
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
- # :disposition
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
- # Note that streaming the file through your app might impact the request
57
- # throughput of your app, depending on which web server is used. In any
58
- # case, it's recommended to use some kind of cache in front of the web
59
- # server.
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
- # If you want to authenticate the downloads, it's recommended you use the
62
- # `rack_response` plugin directly. With it you can return file responses
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
- raise Error, "The :storages option is required for download_endpoint plugin" if uploader.opts[:download_endpoint_storages].nil?
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 shrine_class.opts[:download_endpoint_storages].include?(storage_key.to_sym)
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
- stream_file(data)
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
- stream_file(data)
188
+ serve_file(data)
166
189
  end
167
190
  end
168
191
 
169
192
  private
170
193
 
171
- def stream_file(data)
194
+ # Streams or redirects to the uploaded file.
195
+ def serve_file(data)
172
196
  uploaded_file = get_uploaded_file(data)
173
- range = env["HTTP_RANGE"]
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