shrine 3.0.0.beta2 → 3.0.0.beta3

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.

Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +100 -106
  4. data/doc/advantages.md +90 -88
  5. data/doc/attacher.md +322 -152
  6. data/doc/carrierwave.md +105 -113
  7. data/doc/changing_derivatives.md +308 -0
  8. data/doc/changing_location.md +92 -21
  9. data/doc/changing_storage.md +107 -0
  10. data/doc/creating_plugins.md +1 -1
  11. data/doc/design.md +8 -9
  12. data/doc/direct_s3.md +3 -2
  13. data/doc/metadata.md +97 -78
  14. data/doc/multiple_files.md +3 -3
  15. data/doc/paperclip.md +89 -88
  16. data/doc/plugins/activerecord.md +3 -12
  17. data/doc/plugins/backgrounding.md +126 -100
  18. data/doc/plugins/derivation_endpoint.md +4 -5
  19. data/doc/plugins/derivatives.md +63 -32
  20. data/doc/plugins/download_endpoint.md +54 -1
  21. data/doc/plugins/entity.md +1 -0
  22. data/doc/plugins/form_assign.md +53 -0
  23. data/doc/plugins/mirroring.md +37 -16
  24. data/doc/plugins/multi_cache.md +22 -0
  25. data/doc/plugins/presign_endpoint.md +1 -1
  26. data/doc/plugins/remote_url.md +19 -4
  27. data/doc/plugins/validation.md +83 -0
  28. data/doc/processing.md +149 -133
  29. data/doc/refile.md +68 -63
  30. data/doc/release_notes/3.0.0.md +835 -0
  31. data/doc/securing_uploads.md +56 -36
  32. data/doc/storage/s3.md +2 -2
  33. data/doc/testing.md +104 -120
  34. data/doc/upgrading_to_3.md +538 -0
  35. data/doc/validation.md +48 -87
  36. data/lib/shrine.rb +7 -4
  37. data/lib/shrine/attacher.rb +16 -6
  38. data/lib/shrine/plugins/activerecord.rb +33 -14
  39. data/lib/shrine/plugins/atomic_helpers.rb +1 -1
  40. data/lib/shrine/plugins/backgrounding.rb +23 -89
  41. data/lib/shrine/plugins/data_uri.rb +13 -2
  42. data/lib/shrine/plugins/derivation_endpoint.rb +7 -11
  43. data/lib/shrine/plugins/derivatives.rb +44 -20
  44. data/lib/shrine/plugins/download_endpoint.rb +26 -0
  45. data/lib/shrine/plugins/form_assign.rb +6 -3
  46. data/lib/shrine/plugins/keep_files.rb +2 -2
  47. data/lib/shrine/plugins/mirroring.rb +62 -22
  48. data/lib/shrine/plugins/model.rb +2 -2
  49. data/lib/shrine/plugins/multi_cache.rb +27 -0
  50. data/lib/shrine/plugins/remote_url.rb +25 -10
  51. data/lib/shrine/plugins/remove_invalid.rb +1 -1
  52. data/lib/shrine/plugins/sequel.rb +39 -20
  53. data/lib/shrine/plugins/validation.rb +3 -0
  54. data/lib/shrine/storage/s3.rb +16 -1
  55. data/lib/shrine/uploaded_file.rb +1 -0
  56. data/lib/shrine/version.rb +1 -1
  57. data/shrine.gemspec +1 -1
  58. metadata +12 -7
  59. data/doc/migrating_storage.md +0 -76
  60. data/doc/regenerating_versions.md +0 -143
  61. data/lib/shrine/plugins/attacher_options.rb +0 -55
@@ -48,11 +48,26 @@ gem "http"
48
48
  ```rb
49
49
  require "down/http"
50
50
 
51
- down = Down::Http.new do |client|
52
- client.follow(max_hops: 2).timeout(connect: 2, read: 2)
53
- end
51
+ plugin :remote_url, downloader: -> (url, **options) {
52
+ Down::Http.download(url, **options) do |client|
53
+ client.follow(max_hops: 2).timeout(connect: 2, read: 2)
54
+ end
55
+ }
56
+ ```
57
+
58
+ Any `Down::NotFound` and `Down::TooLarge` exceptions will be rescued and
59
+ converted into validation errors. If you want to convert any other exceptions
60
+ into validation errors, you can raise them as
61
+ `Shrine::Plugins::RemoteUrl::DownloadError`:
54
62
 
55
- plugin :remote_url, downloader: down.method(:download), ...
63
+ ```rb
64
+ plugin :remote_url, downloader: -> (url, **options) {
65
+ begin
66
+ RestClient.get(url)
67
+ rescue RestClient::ExceptionWithResponse => error
68
+ raise Shrine::Plugins::RemoteUrl::DownloadError, "remote file not found"
69
+ end
70
+ }
56
71
  ```
57
72
 
58
73
  ## Uploader options
@@ -0,0 +1,83 @@
1
+ # Validation
2
+
3
+ The [`validation`][validation] plugin provides a framework for validating
4
+ attached files. For some useful validators, see the
5
+ [`validation_helpers`][validation_helpers] plugin.
6
+
7
+ ```rb
8
+ plugin :validation
9
+ ```
10
+
11
+ The `Attacher.validate` method is used to register a validation block, which
12
+ is called on attachment:
13
+
14
+ ```rb
15
+ class VideoUploader < Shrine
16
+ Attacher.validate do
17
+ if file.duration > 5*60*60
18
+ errors << "duration must not be longer than 5 hours"
19
+ end
20
+ end
21
+ end
22
+ ```
23
+ ```rb
24
+ attacher.assign(file)
25
+ attacher.errors #=> ["duration must not be longer than 5 hours"]
26
+ ```
27
+
28
+ The validation block is executed in context of a `Shrine::Attacher` instance:
29
+
30
+ ```rb
31
+ class VideoUploader < Shrine
32
+ Attacher.validate do
33
+ self #=> #<VideoUploader::Attacher>
34
+
35
+ record #=> #<Movie>
36
+ name #=> :video
37
+ file #=> #<Shrine::UploadedFile>
38
+ end
39
+ end
40
+ ```
41
+
42
+ ## Inheritance
43
+
44
+ If you're subclassing an uploader that has validations defined, you can call
45
+ those validations via `super()`:
46
+
47
+ ```rb
48
+ class ApplicationUploader < Shrine
49
+ Attacher.validate { validate_max_size 5.megabytes }
50
+ end
51
+ ```
52
+ ```rb
53
+ class ImageUploader < ApplicationUploader
54
+ Attacher.validate do
55
+ super() # empty parentheses are required
56
+ validate_mime_type %w[image/jpeg image/png image/webp]
57
+ end
58
+ end
59
+ ```
60
+
61
+ ## Validation options
62
+
63
+ You can pass options to the validator via the `:validate` option:
64
+
65
+ ```rb
66
+ attacher.assign(file, validate: { foo: "bar" })
67
+ ```
68
+ ```rb
69
+ class MyUploader < Shrine
70
+ Attacher.validate do |**options|
71
+ options #=> { foo: "bar" }
72
+ end
73
+ end
74
+ ```
75
+
76
+ You can also skip validation by passing `validate: false`:
77
+
78
+ ```rb
79
+ attacher.assign(file, validate: false) # skips validation
80
+ ```
81
+
82
+ [validation]: /lib/shrine/plugins/validation.rb
83
+ [validation_helpers]: /doc/plugins/validation_helpers.md#readme
@@ -1,19 +1,9 @@
1
1
  # File Processing
2
2
 
3
- Shrine allows you to process files in two ways. One is processing "[on
4
- upload](#processing-on-upload)", where the processing gets triggered when the file is
5
- attached to a record. The other is "[on-the-fly](#on-the-fly-processing)"
6
- processing, where the processing is performed lazily at the moment the file is
7
- requested.
8
-
9
- With both ways you need to define some kind of processing block, which accepts
10
- a source file and is expected to return the processed result file.
11
-
12
- ```rb
13
- some_process_block do |source_file|
14
- # process source file and return the result
15
- end
16
- ```
3
+ Shrine allows you to process attached files up front or on-the-fly. For
4
+ example, if your app is accepting image uploads, you can generate a predefined
5
+ set of of thumbnails when the image is attached to a record, or you can have
6
+ thumbnails generated dynamically as they're needed.
17
7
 
18
8
  How you're going to implement processing is entirely up to you. For images it's
19
9
  recommended to use the **[ImageProcessing]** gem, which provides wrappers for
@@ -24,12 +14,10 @@ Here is an example of generating a thumbnail with ImageProcessing:
24
14
  ```sh
25
15
  $ brew install imagemagick
26
16
  ```
27
-
28
17
  ```rb
29
18
  # Gemfile
30
- gem "image_processing", "~> 1.0"
19
+ gem "image_processing", "~> 1.8"
31
20
  ```
32
-
33
21
  ```rb
34
22
  require "image_processing/mini_magick"
35
23
 
@@ -40,107 +28,147 @@ thumbnail = ImageProcessing::MiniMagick
40
28
  thumbnail #=> #<Tempfile:...> (a 600x400 thumbnail of the source image)
41
29
  ```
42
30
 
43
- ## Processing on upload
44
-
45
- Shrine allows you to process files before they're uploaded to a storage. It's
46
- generally best to process cached files when they're being promoted to permanent
47
- storage, because (a) at that point the file has already been successfully
48
- [validated][validation], (b) the parent record has been saved and the database
49
- transaction has been committed, and (c) this can be delayed into a [background
50
- job][backgrounding].
31
+ ## Processing up front
51
32
 
52
- You can define processing using the `processing` plugin, which we'll use to
53
- hook into the `:store` phase (when cached file is uploaded to permanent
54
- storage).
33
+ Let's say we're handling images, and want to generate a predefined set of
34
+ thumbnails with various dimensions. We can use the `derivatives` plugin to
35
+ upload and save the processed files:
55
36
 
56
37
  ```rb
57
- class ImageUploader < Shrine
58
- plugin :processing
59
-
60
- process(:store) do |io, context|
61
- io #=> #<Shrine::UploadedFile ...>
62
- context #=> {:record=>#<Photo...>,:name=>:image,...}
38
+ Shrine.plugin :derivatives
39
+ ```
40
+ ```rb
41
+ require "image_processing/mini_magick"
63
42
 
64
- # ...
43
+ class ImageUploader < Shrine
44
+ Attacher.derivatives_processor do |original|
45
+ magick = ImageProcessing::MiniMagick.source(original)
46
+
47
+ {
48
+ large: magick.resize_to_limit!(800, 800),
49
+ medium: magick.resize_to_limit!(500, 500),
50
+ small: magick.resize_to_limit!(300, 300),
51
+ }
65
52
  end
66
53
  end
67
54
  ```
55
+ ```rb
56
+ photo = Photo.new
57
+ photo.image_derivatives! # calls derivatives processor
58
+ photo.save
59
+ ```
68
60
 
69
- The processing block yields two arguments: a [`Shrine::UploadedFile`] object
70
- representing the file uploaded to temporary storage, and a Hash containing
71
- additional data such as the model instance and attachment name. The block
72
- result should be file(s) that will be uploaded to permanent storage.
73
-
74
- ### Versions
75
-
76
- Let's say we're handling images, and want to generate thumbnails of various
77
- dimensions. In this case we can use the ImageProcessing gem to generate the
78
- thumbnails, and return a hash of processed files at the end of the block. We'll
79
- need to load the `versions` plugin which extends Shrine with the ability to
80
- handle collections of files inside the same attachment.
61
+ After the processed files are uploaded, their data is saved into the
62
+ `<attachment>_data` column. You can then retrieve the derivatives as
63
+ [`Shrine::UploadedFile`][uploaded file] objects:
81
64
 
82
65
  ```rb
83
- require "image_processing/mini_magick"
66
+ photo.image(:large) #=> #<Shrine::UploadedFile ...>
67
+ photo.image(:large).url #=> "/uploads/store/lg043.jpg"
68
+ photo.image(:large).size #=> 5825949
69
+ photo.image(:large).mime_type #=> "image/jpeg"
70
+ ```
84
71
 
85
- class ImageUploader < Shrine
86
- plugin :processing # allows hooking into promoting
87
- plugin :versions # enable Shrine to handle a hash of files
88
- plugin :delete_raw # delete processed files after uploading
72
+ ### Backgrounding
89
73
 
90
- process(:store) do |io, context|
91
- versions = { original: io } # retain original
74
+ Since file processing can be time consuming, it's recommended to move it into a
75
+ background job.
92
76
 
93
- # download the uploaded file from the temporary storage
94
- io.download do |original|
95
- pipeline = ImageProcessing::MiniMagick.source(original)
77
+ #### With promotion
96
78
 
97
- versions[:large] = pipeline.resize_to_limit!(800, 800)
98
- versions[:medium] = pipeline.resize_to_limit!(500, 500)
99
- versions[:small] = pipeline.resize_to_limit!(300, 300)
100
- end
79
+ The simplest way is to use the [`backgrounding`][backgrounding] plugin to move
80
+ promotion into a background job, and then create derivatives as part of
81
+ promotion:
101
82
 
102
- versions # return the hash of processed files
83
+ ```rb
84
+ Shrine.plugin :backgrounding
85
+ Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
86
+ ```
87
+ ```rb
88
+ class PromoteJob < ActiveJob::Base
89
+ def perform(attacher_class, record, name, file_data)
90
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
91
+ attacher.create_derivatives # calls derivatives processor
92
+ attacher.atomic_promote
93
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
94
+ # attachment has changed or the record has been deleted, nothing to do
103
95
  end
104
96
  end
105
97
  ```
106
98
 
107
- **NOTE: It's recommended to always keep the original file, just in case you'll
108
- ever need to reprocess it.**
109
-
110
- ### Conditional processing
111
-
112
- The process block yields the attached file uploaded to temporary storage, so we
113
- have information like file extension and MIME type available. Together with
114
- ImageProcessing's chainable API, it's easy to do conditional proccessing.
99
+ #### Separate from promotion
115
100
 
116
- For example, let's say we want our thumbnails to be either JPEGs or PNGs, and
117
- we also want to save JPEGs as progressive (interlaced). Here's how the code for
118
- this might look like:
101
+ Derivatives don't need to be created as part of the attachment flow, you can
102
+ create them at any point after promotion:
119
103
 
120
104
  ```rb
121
- process(:store) do |io, context|
122
- versions = { original: io }
105
+ DerivativesJob.perform_later(
106
+ attacher.class,
107
+ attacher.record,
108
+ attacher.name,
109
+ attacher.file_data,
110
+ )
111
+ ```
112
+ ```rb
113
+ class DerivativesJob < ActiveJob::Base
114
+ def perform(attacher_class, record, name, file_data)
115
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
116
+ attacher.create_derivatives # calls derivatives processor
117
+ attacher.atomic_persist
118
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
119
+ attacher&.destroy_attached # delete now orphaned derivatives
120
+ end
121
+ end
122
+ ```
123
123
 
124
- io.download do |original|
125
- pipeline = ImageProcessing::Vips.source(original)
124
+ #### Concurrent processing
126
125
 
127
- # Shrine::UploadedFile object contains information about the MIME type
128
- unless io.mime_type == "image/png"
129
- pipeline = pipeline
130
- .convert("jpeg")
131
- .saver(interlace: true)
132
- end
126
+ You can also generate derivatives concurrently:
133
127
 
134
- versions[:large] = pipeline.resize_to_limit!(800, 800)
135
- versions[:medium] = pipeline.resize_to_limit!(500, 500)
136
- versions[:small] = pipeline.resize_to_limit!(300, 300)
128
+ ```rb
129
+ class ImageUploader < Shrine
130
+ THUMBNAILS = {
131
+ large: [800, 800],
132
+ medium: [500, 500],
133
+ small: [300, 300],
134
+ }
135
+
136
+ Attacher.derivatives_processor do |original, name:|
137
+ thumbnail = ImageProcessing::MiniMagick
138
+ .source(original)
139
+ .resize_to_limit!(*THUMBNAILS.fetch(name))
140
+
141
+ { name => thumbnail }
142
+ end
143
+ end
144
+ ```
145
+ ```rb
146
+ ImageUploader::THUMBNAILS.each_key do |derivative_name|
147
+ DerivativeJob.perform_later(
148
+ attacher.class,
149
+ attacher.record,
150
+ attacher.name,
151
+ attacher.file_data,
152
+ derivative_name,
153
+ )
154
+ end
155
+ ```
156
+ ```rb
157
+ class DerivativeJob < ActiveJob::Base
158
+ def perform(attacher_class, record, name, file_data, derivative_name)
159
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
160
+ attacher.create_derivatives(name: derivative_name)
161
+ attacher.atomic_persist do |reloaded_attacher|
162
+ # make sure we don't override derivatives created in other jobs
163
+ attacher.merge_derivatives(reloaded_attacher.derivatives)
164
+ end
165
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
166
+ attacher.derivatives[derivative_name].delete # delete now orphaned derivative
137
167
  end
138
-
139
- versions
140
168
  end
141
169
  ```
142
170
 
143
- ### Processing other file types
171
+ ### Processing other filetypes
144
172
 
145
173
  So far we've only been talking about processing images. However, there is
146
174
  nothing image-specific in Shrine's processing API, you can just as well process
@@ -151,32 +179,23 @@ generic gem.
151
179
  To demonstrate, here is an example of transcoding videos using
152
180
  [streamio-ffmpeg]:
153
181
 
182
+ ```rb
183
+ # Gemfile
184
+ gem "streamio-ffmpeg"
185
+ ```
154
186
  ```rb
155
187
  require "streamio-ffmpeg"
156
- require "tempfile"
157
188
 
158
189
  class VideoUploader < Shrine
159
- plugin :processing
160
- plugin :versions
161
- plugin :delete_raw
162
-
163
- process(:store) do |io, context|
164
- versions = { original: io }
190
+ Attacher.derivatives_processor do |original|
191
+ transcoded = Tempfile.new ["transcoded", ".mp4"]
192
+ screenshot = Tempfile.new ["screenshot", ".jpg"]
165
193
 
166
- io.download do |original|
167
- transcoded = Tempfile.new(["transcoded", ".mp4"], binmode: true)
168
- screenshot = Tempfile.new(["screenshot", ".jpg"], binmode: true)
169
-
170
- movie = FFMPEG::Movie.new(original.path)
171
- movie.transcode(transcoded.path)
172
- movie.screenshot(screenshot.path)
173
-
174
- [transcoded, screenshot].each(&:open) # refresh file descriptors
175
-
176
- versions.merge!(transcoded: transcoded, screenshot: screenshot)
177
- end
194
+ movie = FFMPEG::Movie.new(original.path)
195
+ movie.transcode(transcoded.path)
196
+ movie.screenshot(screenshot.path)
178
197
 
179
- versions
198
+ { transcoded: transcoded, screenshot: screenshot }
180
199
  end
181
200
  end
182
201
  ```
@@ -185,13 +204,13 @@ end
185
204
 
186
205
  Generating image thumbnails on upload can be a pain to maintain, because
187
206
  whenever you need to add a new version or change an existing one, you need to
188
- retroactively apply it to all existing uploads (see the [Reprocessing Versions]
207
+ retroactively apply it to all existing uploads (see the [Managing Derivatives]
189
208
  guide for more details).
190
209
 
191
210
  As an alternative, it's very common to instead generate thumbnails dynamically
192
211
  as they're requested, and then cache them for future requests. This strategy is
193
- known as "on-the-fly processing", and it's suitable for generating thumbnails
194
- or document previews.
212
+ known as "on-the-fly processing", and it's suitable for short-running
213
+ processing such as creating image thumbnails or document previews.
195
214
 
196
215
  Shrine provides on-the-fly processing functionality via the
197
216
  [`derivation_endpoint`][derivation_endpoint] plugin. The basic setup is the
@@ -257,7 +276,7 @@ $ brew install vips
257
276
 
258
277
  ```rb
259
278
  # Gemfile
260
- gem "image_processing", "~> 1.0"
279
+ gem "image_processing", "~> 1.8"
261
280
  ```
262
281
 
263
282
  ```rb
@@ -271,30 +290,27 @@ thumbnail = ImageProcessing::Vips
271
290
  thumbnail #=> #<Tempfile:...> (a 600x400 thumbnail of the source image)
272
291
  ```
273
292
 
274
- ### Optimizing thumbnails
293
+ ### Parallelize uploading
275
294
 
276
- If you're generating image thumbnails, you can additionally use the
277
- [image_optim] gem to further reduce their filesize:
295
+ If you're generating derivatives, you can parallelize the uploads using the
296
+ [concurrent-ruby] gem:
278
297
 
279
298
  ```rb
280
299
  # Gemfile
281
- gem "image_processing", "~> 1.0"
282
- gem "image_optim"
283
- gem "image_optim_pack" # precompiled binaries
300
+ gem "concurrent-ruby"
284
301
  ```
285
-
286
302
  ```rb
287
- require "image_processing/mini_magick"
303
+ require "concurrent"
288
304
 
289
- thumbnail = ImageProcessing::MiniMagick
290
- .source(image)
291
- .resize_to_limit!(600, 400)
305
+ derivatives = attacher.process_derivatives
292
306
 
293
- image_optim = ImageOptim.new
294
- image_optim.optimize_image!(thumbnail.path)
307
+ tasks = derivatives.map do |name, file|
308
+ Concurrent::Promises.future(name, file) do |name, file|
309
+ attacher.add_derivative(name, file)
310
+ end
311
+ end
295
312
 
296
- thumbnail.open # refresh file descriptor
297
- thumbnail
313
+ Concurrent::Promises.zip(*tasks).wait!
298
314
  ```
299
315
 
300
316
  ### External processing
@@ -319,7 +335,7 @@ class ImageUploader < Shrine
319
335
  prefix: "derivations/image",
320
336
  download: false
321
337
 
322
- derivation :thumbnail do |source, width, height|
338
+ derivation :thumbnail do |width, height|
323
339
  # generate thumbnails using ImageOptim.com
324
340
  down = Down::Http.new(method: :post)
325
341
  down.download("https://im2.io/<USERNAME>/#{width}x#{height}/#{source.url}")
@@ -359,7 +375,7 @@ Now when we upload our images to Cloudinary, we can generate URLs with various
359
375
  processing parameters:
360
376
 
361
377
  ```rb
362
- photo.image.url(width: 100, height: 100, crop: :fit)
378
+ photo.image_url(width: 100, height: 100, crop: :fit)
363
379
  #=> "http://res.cloudinary.com/myapp/image/upload/w_100,h_100,c_fit/nature.jpg"
364
380
  ```
365
381
 
@@ -369,15 +385,15 @@ photo.image.url(width: 100, height: 100, crop: :fit)
369
385
  [GraphicsMagick]: http://www.graphicsmagick.org
370
386
  [libvips]: http://libvips.github.io/libvips/
371
387
  [Why is libvips quick]: https://github.com/libvips/libvips/wiki/Why-is-libvips-quick
372
- [image_optim]: https://github.com/toy/image_optim
373
388
  [ImageOptim.com]: https://imageoptim.com/api
374
389
  [streamio-ffmpeg]: https://github.com/streamio/streamio-ffmpeg
375
- [Reprocessing Versions]: /doc/regenerating_versions.md#readme
390
+ [Managing Derivatives]: /doc/changing_derivatives.md#readme
376
391
  [Cloudinary]: https://cloudinary.com
377
392
  [shrine-cloudinary]: https://github.com/shrinerb/shrine-cloudinary
378
393
  [backgrounding]: /doc/plugins/backgrounding.md#readme
379
- [validation]: /doc/validation.md#readme
380
394
  [ruby-vips]: https://github.com/libvips/ruby-vips
381
395
  [MiniMagick]: https://github.com/minimagick/minimagick
382
396
  [derivation_endpoint]: /doc/plugins/derivation_endpoint.md#readme
383
397
  [derivation_endpoint performance]: /doc/plugins/derivation_endpoint.md#performance
398
+ [derivatives]: /doc/plugins/derivatives.md#readme
399
+ [concurrent-ruby]: https://github.com/ruby-concurrency/concurrent-ruby