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
@@ -104,12 +104,12 @@ In the Photo model, create a Shrine attachment attribute named `image`
104
104
  ```rb
105
105
  # with Sequel:
106
106
  class Photo < Sequel::Model
107
- include ImageUploader::Attachment.new(:image)
107
+ include ImageUploader::Attachment(:image)
108
108
  end
109
109
 
110
110
  # with Active Record:
111
111
  class Photo < ActiveRecord::Base
112
- include ImageUploader::Attachment.new(:image)
112
+ include ImageUploader::Attachment(:image)
113
113
  end
114
114
  ```
115
115
 
@@ -257,7 +257,7 @@ class ImageUploader < Shrine
257
257
 
258
258
  Attacher.validate do
259
259
  validate_max_size 10*1024*1024
260
- validate_mime_type_inclusion %w[image/jpeg image/png]
260
+ validate_mime_type %w[image/jpeg image/png image/webp]
261
261
  end
262
262
  end
263
263
  ```
@@ -65,7 +65,7 @@ class ImageUploader < Shrine
65
65
  end
66
66
 
67
67
  class Photo < ActiveRecord::Base
68
- include ImageUploader::Attachment.new(:image)
68
+ include ImageUploader::Attachment(:image)
69
69
  end
70
70
  ```
71
71
 
@@ -81,8 +81,9 @@ uploaded_file.url #=> "https://my-bucket.s3.amazonaws.com/store/kfds0lg9rer.jpg"
81
81
  ### Processing
82
82
 
83
83
  In contrast to Paperclip's static options, in Shrine you define and perform
84
- processing on instance-level. The result of processing can be a single file
85
- or a hash of versions:
84
+ processing on instance-level. You also have the descriptive method names
85
+ provided by the [image_processing] gem.
86
+
86
87
 
87
88
  ```rb
88
89
  class Photo < ActiveRecord::Base
@@ -99,34 +100,26 @@ end
99
100
  require "image_processing/mini_magick"
100
101
 
101
102
  class ImageUploader < Shrine
102
- plugin :processing
103
- plugin :versions
104
-
105
- process(:store) do |io, context|
106
- versions = { original: io } # retain original
107
-
108
- io.download do |original|
109
- pipeline = ImageProcessing::MiniMagick.source(original)
103
+ plugin :derivatives
110
104
 
111
- versions[:large] = pipeline.resize_to_limit!(800, 800)
112
- versions[:medium] = pipeline.resize_to_limit!(500, 500)
113
- versions[:small] = pipeline.resize_to_limit!(300, 300)
114
- end
105
+ Attacher.derivatives_processor do |original|
106
+ magick = ImageProcessing::MiniMagick.source(original)
115
107
 
116
- versions # return the hash of processed files
108
+ {
109
+ large: magick.resize_to_limit!(800, 800),
110
+ medium: magick.resize_to_limit!(500, 500),
111
+ small: magick.resize_to_limit!(300, 300),
112
+ }
117
113
  end
118
114
  end
119
115
  ```
120
116
 
121
- This allows you to fully optimize processing, because you can easily specify
122
- which files are processed from which, and even add parallelization.
123
-
124
117
  #### Reprocessing versions
125
118
 
126
119
  Shrine doesn't have a built-in way of regenerating versions, because that has
127
120
  to be written and optimized differently depending on whether you're adding or
128
121
  removing a version, what ORM are you using, how many records there are in the
129
- database etc. The [Reprocessing versions] guide provides some useful tips on
122
+ database etc. The [Managing Derivatives] guide provides some useful tips on
130
123
  this task.
131
124
 
132
125
  ### Validations
@@ -148,8 +141,8 @@ class ImageUploader < Shrine
148
141
  plugin :validation_helpers
149
142
 
150
143
  Attacher.validate do
151
- validate_mime_type_inclusion %w[image/jpeg image/gif image/png]
152
- validate_max_size 10*1024*1024 unless record.admin?
144
+ validate_mime_type %w[image/jpeg image/gif image/png]
145
+ validate_max_size 10*1024*1024
153
146
  end
154
147
  end
155
148
  ```
@@ -212,7 +205,7 @@ gives your models similar set of methods that Paperclip gives:
212
205
 
213
206
  ```rb
214
207
  class Photo < Sequel::Model
215
- include ImageUploader::Attachment.new(:image)
208
+ include ImageUploader::Attachment(:image)
216
209
  end
217
210
  ```
218
211
 
@@ -243,11 +236,11 @@ Unlike Paperclip, Shrine will store this information for each processed
243
236
  version, making them first-class citizens:
244
237
 
245
238
  ```rb
246
- photo.image[:original] #=> #<Shrine::UploadedFile>
247
- photo.image[:original].width #=> 800
239
+ photo.image #=> #<Shrine::UploadedFile>
240
+ photo.image.width #=> 800
248
241
 
249
- photo.image[:thumb] #=> #<Shrine::UploadedFile>
250
- photo.image[:thumb].width #=> 300
242
+ photo.image(:thumb) #=> #<Shrine::UploadedFile>
243
+ photo.image(:thumb).width #=> 300
251
244
  ```
252
245
 
253
246
  Also, since Paperclip stores only the filename, it has to recalculate the full
@@ -256,28 +249,6 @@ to move files to a new location, because changing how the location is generated
256
249
  will now cause incorrect URLs to be generated for all existing files. Shrine
257
250
  calculates the whole location only once and saves it to the column.
258
251
 
259
- ### Hooks/Callbacks
260
-
261
- Shrine's `hooks` plugin provides callbacks for Shrine, so to get Paperclip's
262
- `(before|after)_post_process`, you can override `#before_process` and
263
- `#after_process` methods:
264
-
265
- ```rb
266
- class ImageUploader < Shrine
267
- plugin :hooks
268
-
269
- def before_process(io, context)
270
- # ...
271
- super
272
- end
273
-
274
- def after_process(io, context)
275
- super
276
- # ...
277
- end
278
- end
279
- ```
280
-
281
252
  ## Migrating from Paperclip
282
253
 
283
254
  You have an existing app using Paperclip and you want to transfer it to Shrine.
@@ -292,6 +263,17 @@ Afterwards we need to make new uploads write to the `image_data` column. This
292
263
  can be done by including the below module to all models that have Paperclip
293
264
  attachments:
294
265
 
266
+ ```rb
267
+ require "shrine"
268
+
269
+ Shrine.storages = {
270
+ cache: ...,
271
+ store: ...,
272
+ }
273
+
274
+ Shrine.plugin :model
275
+ Shrine.plugin :derivatives
276
+ ```
295
277
  ```rb
296
278
  module PaperclipShrineSynchronization
297
279
  def self.included(model)
@@ -304,49 +286,53 @@ module PaperclipShrineSynchronization
304
286
 
305
287
  def write_shrine_data(name)
306
288
  attachment = send(name)
289
+ attacher = Shrine::Attacher.from_model(self, name)
307
290
 
308
291
  if attachment.size.present?
309
- data = attachment_to_shrine_data(attachment)
292
+ attacher.set shrine_file(attachment)
310
293
 
311
- if attachment.styles.any?
312
- data = {original: data}
313
- attachment.styles.each do |name, style|
314
- data[name] = style_to_shrine_data(style)
315
- end
294
+ attachment.styles.each do |name, style|
295
+ attacher.merge_derivatives(name => shrine_file(style))
316
296
  end
317
-
318
- write_attribute(:"#{name}_data", data.to_json)
319
297
  else
320
- write_attribute(:"#{name}_data", nil)
298
+ attacher.set nil
321
299
  end
322
300
  end
323
301
 
324
302
  private
325
303
 
304
+ def shrine_file(object)
305
+ if object.is_a?(Paperclip::Attachment)
306
+ shrine_attachment_file(object)
307
+ else
308
+ shrine_style_file(object)
309
+ end
310
+ end
311
+
326
312
  # If you'll be using a `:prefix` on your Shrine storage, or you're storing
327
313
  # files on the filesystem, make sure to subtract the appropriate part
328
314
  # from the path assigned to `:id`.
329
- def attachment_to_shrine_data(attachment)
330
- {
331
- storage: :store,
332
- id: attachment.path,
315
+ def shrine_attachment_file(attachment)
316
+ Shrine.uploaded_file(
317
+ storage: :store,
318
+ id: attachment.path,
333
319
  metadata: {
334
- size: attachment.size,
335
- filename: attachment.original_filename,
336
- mime_type: attachment.content_type,
320
+ "size" => attachment.size,
321
+ "filename" => attachment.original_filename,
322
+ "mime_type" => attachment.content_type,
337
323
  }
338
- }
324
+ )
339
325
  end
340
326
 
341
327
  # If you'll be using a `:prefix` on your Shrine storage, or you're storing
342
328
  # files on the filesystem, make sure to subtract the appropriate part
343
329
  # from the path assigned to `:id`.
344
330
  def style_to_shrine_data(style)
345
- {
346
- storage: :store,
347
- id: style.attachment.path(style.name),
348
- metadata: {}
349
- }
331
+ Shrine.uploaded_file(
332
+ storage: :store,
333
+ id: style.attachment.path(style.name),
334
+ metadata: {},
335
+ )
350
336
  end
351
337
  end
352
338
  ```
@@ -383,8 +369,8 @@ metadata defined in your Shrine uploader:
383
369
  Shrine.plugin :refresh_metadata
384
370
 
385
371
  Photo.find_each do |photo|
386
- attachment = ImageUploader.uploaded_file(photo.image, &:refresh_metadata!)
387
- photo.update(image_data: attachment.to_json)
372
+ photo.image_attacher.refresh_metadata!
373
+ photo.save
388
374
  end
389
375
  ```
390
376
 
@@ -396,8 +382,8 @@ As mentioned above, Shrine's equivalent of `has_attached_file` is including
396
382
  an attachment module:
397
383
 
398
384
  ```rb
399
- class User < Sequel::Model
400
- include ImageUploader::Attachment.new(:avatar) # adds `avatar`, `avatar=` and `avatar_url` methods
385
+ class Photo < Sequel::Model
386
+ include ImageUploader::Attachment(:image) # adds `image`, `image=` and `image_url` methods
401
387
  end
402
388
  ```
403
389
 
@@ -420,8 +406,23 @@ You can change that for a specific uploader with the `default_storage` plugin.
420
406
 
421
407
  #### `:styles`, `:processors`, `:convert_options`
422
408
 
423
- As explained in the "Processing" section, processing is done by overriding the
424
- `Shrine#process` method.
409
+ Processing is defined by using the `derivatives` plugin:
410
+
411
+ ```rb
412
+ class ImageUploader < Shrine
413
+ plugin :derivatives
414
+
415
+ Attacher.derivatives_processor do |original|
416
+ magick = ImageProcessing::MiniMagick.source(image)
417
+
418
+ {
419
+ large: magick.resize_to_limit!(800, 800),
420
+ medium: magick.resize_to_limit!(500, 500),
421
+ small: magick.resize_to_limit!(300, 300),
422
+ }
423
+ end
424
+ end
425
+ ```
425
426
 
426
427
  #### `:default_url`
427
428
 
@@ -443,7 +444,7 @@ Shrine provides a `keep_files` plugin which allows you to keep files that would
443
444
  otherwise be deleted:
444
445
 
445
446
  ```rb
446
- Shrine.plugin :keep_files, destroyed: true
447
+ Shrine.plugin :keep_files
447
448
  ```
448
449
 
449
450
  #### `:path`, `:url`, `:interpolator`, `:url_generator`
@@ -460,7 +461,7 @@ Alternatively, if you want to generate locations yourself you can override the
460
461
 
461
462
  ```rb
462
463
  class ImageUploader < Shrine
463
- def generate_location(io, context)
464
+ def generate_location(io, **options)
464
465
  # ...
465
466
  end
466
467
  end
@@ -481,17 +482,16 @@ If you're generating versions in Shrine, the attachment will be a hash of
481
482
  uploaded files:
482
483
 
483
484
  ```rb
484
- user.avatar.class #=> Hash
485
- user.avatar #=>
485
+ photo.image_derivatives #=>
486
486
  # {
487
487
  # small: #<Shrine::UploadedFile>,
488
488
  # medium: #<Shrine::UploadedFile>,
489
489
  # large: #<Shrine::UploadedFile>,
490
490
  # }
491
491
 
492
- user.avatar[:small].url #=> "..."
492
+ photo.image(:small).url #=> "..."
493
493
  # or
494
- user.avatar_url(:small) #=> "..."
494
+ photo.image_url(:small) #=> "..."
495
495
  ```
496
496
 
497
497
  #### `#path`
@@ -500,12 +500,12 @@ Shrine doesn't have this because storages are abstract and this would be
500
500
  specific to the filesystem, but the closest is probably `#id`:
501
501
 
502
502
  ```rb
503
- user.avatar.id #=> "users/342/avatar/398543qjfdsf.jpg"
503
+ photo.image.id #=> "photo/342/image/398543qjfdsf.jpg"
504
504
  ```
505
505
 
506
506
  #### `#reprocess!`
507
507
 
508
- Shrine doesn't have an equivalent to this, but the [Reprocessing versions]
508
+ Shrine doesn't have an equivalent to this, but the [Managing Derivatives]
509
509
  guide provides some useful tips on how to do this.
510
510
 
511
511
  ### `Paperclip::Storage::S3`
@@ -532,7 +532,7 @@ Shrine::Storage::S3.new(
532
532
  The object data can be configured via the `:upload_options` hash:
533
533
 
534
534
  ```rb
535
- Shrine::Storage::S3.new(upload_options: {content_disposition: "attachment"}, **options)
535
+ Shrine::Storage::S3.new(upload_options: { content_disposition: "attachment" }, **options)
536
536
  ```
537
537
 
538
538
  You can use the `upload_options` plugin to set upload options dynamically.
@@ -542,7 +542,7 @@ You can use the `upload_options` plugin to set upload options dynamically.
542
542
  The object permissions can be configured with the `:acl` upload option:
543
543
 
544
544
  ```rb
545
- Shrine::Storage::S3.new(upload_options: {acl: "private"}, **options)
545
+ Shrine::Storage::S3.new(upload_options: { acl: "private" }, **options)
546
546
  ```
547
547
 
548
548
  You can use the `upload_options` plugin to set upload options dynamically.
@@ -581,6 +581,7 @@ The Shrine storage has no replacement for the `:url` Paperclip option, and it
581
581
  isn't needed.
582
582
 
583
583
  [file]: http://linux.die.net/man/1/file
584
- [Reprocessing versions]: /doc/regenerating_versions.md#readme
584
+ [Managing Derivatives]: /doc/changing_derivatives.md#readme
585
585
  [direct S3 uploads]: /doc/direct_s3.md#readme
586
586
  [`Shrine::Storage::S3`]: /doc/storage/s3.md#readme
587
+ [image_processing]: https://github.com/janko/image_processing
@@ -74,18 +74,9 @@ photo.image.exists? #=> false
74
74
 
75
75
  #### Caveats
76
76
 
77
- Active Record versions prior to 5.x silence errors that occur in callbacks,
78
- which can make debugging more difficult, so it's recommended that you disable
79
- this behaviour:
80
-
81
- ```rb
82
- # This is the default in ActiveRecord 5
83
- ActiveRecord::Base.raise_in_transactional_callbacks = true
84
- ```
85
-
86
- Active Record also currently has a [bug with transaction callbacks], so if
87
- you have any "after commit" callbacks, make sure to include Shrine's attachment
88
- module *after* they have all been defined.
77
+ Active Record currently has a [bug with transaction callbacks], so if you have
78
+ any "after commit" callbacks, make sure to include Shrine's attachment module
79
+ *after* they have all been defined.
89
80
 
90
81
  #### Skipping Callbacks
91
82
 
@@ -1,150 +1,176 @@
1
1
  # Backgrounding
2
2
 
3
3
  The [`backgrounding`][backgrounding] plugin enables you to move promoting and
4
- deleting of files from record's lifecycle into background jobs. This is
5
- especially useful if you're doing processing and/or you're storing files on an
6
- external storage service.
7
-
8
- The plugin provides `Attacher.promote` and `Attacher.delete` methods, which
9
- allow you to hook up to promoting and deleting and spawn background jobs, by
10
- passing a block.
4
+ deleting of files into background jobs. This is especially useful if you're
5
+ processing [derivatives] and storing files to a remote storage service.
11
6
 
12
7
  ```rb
13
- Shrine.plugin :backgrounding
14
-
15
- # makes all uploaders use background jobs
16
- Shrine::Attacher.promote { |data| PromoteJob.perform_async(data) }
17
- Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
8
+ Shrine.plugin :backgrounding # load the plugin globally
18
9
  ```
19
10
 
20
- If you don't want to apply backgrounding for all uploaders, you can declare the
21
- hooks only for specific uploaders (in this case it's still recommended to keep
22
- the plugin loaded globally).
11
+ The plugin provides `Attacher.promote_block` and `Attacher.destroy_block`
12
+ methods, which allow you to register blocks that will get executed in place of
13
+ synchronous promotion and deletion. Inside them you can spawn your background
14
+ jobs:
23
15
 
24
16
  ```rb
25
- class MyUploader < Shrine
26
- # makes this uploader use background jobs
27
- Attacher.promote { |data| PromoteJob.perform_async(data) }
28
- Attacher.delete { |data| DeleteJob.perform_async(data) }
29
- end
17
+ # register backgrounding blocks for all uploaders
18
+ Shrine::Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
19
+ Shrine::Attacher.destroy_block { DestroyJob.perform_later(self.class, data) }
30
20
  ```
31
-
32
- The yielded `data` variable is a serializable hash containing all context
33
- needed for promotion/deletion. Now you just need to declare the job classes,
34
- and inside them call `Attacher.promote` or `Attacher.delete`, this time with
35
- the received data.
36
-
37
21
  ```rb
38
- class PromoteJob
39
- include Sidekiq::Worker
40
- def perform(data)
41
- Shrine::Attacher.promote(data)
22
+ class PromoteJob < ActiveJob::Base
23
+ def perform(attacher_class, record, name, file_data)
24
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
25
+ attacher.atomic_promote
26
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
27
+ # attachment has changed or record has been deleted, nothing to do
42
28
  end
43
29
  end
44
-
45
- class DeleteJob
46
- include Sidekiq::Worker
47
- def perform(data)
48
- Shrine::Attacher.delete(data)
30
+ ```
31
+ ```rb
32
+ class DestroyJob < ActiveJob::Base
33
+ def perform(attacher_class, data)
34
+ attacher = attacher_class.from_data(data)
35
+ attacher.destroy
49
36
  end
50
37
  end
51
38
  ```
52
39
 
53
- This example used Sidekiq, but obviously you could just as well use any other
54
- backgrounding library. This setup will be applied globally for all uploaders.
40
+ If you don't want to apply backgrounding for all uploaders, you can register
41
+ backgrounding blocks only for a specific uploader:
55
42
 
56
- If you're generating versions, and you want to process some versions in the
57
- foreground before kicking off a background job, you can use the `recache`
58
- plugin.
43
+ ```rb
44
+ class MyUploader < Shrine
45
+ # register backgrounding blocks only for this uploader
46
+ Attacher.promote_block { PromoteJob.perform_later(self.class, record, name, file_data) }
47
+ Attacher.destroy_block { DestroyJob.perform_later(self.class, data) }
48
+ end
49
+ ```
59
50
 
60
- In your application you can use `Attacher#cached?` and `Attacher#stored?`
61
- to differentiate between your background job being in progress and
62
- having completed.
51
+ Backgrounding will automatically get triggered as part of your attachment flow
52
+ if you're using `Shrine::Attachment` with a persistence plugin such as
53
+ `activerecord` or `sequel`:
63
54
 
64
55
  ```rb
65
- if user.avatar_attacher.cached? # background job is still in progress
66
- # ...
67
- elsif user.avatar_attacher.stored? # background job has finished
68
- # ...
69
- end
56
+ photo = Photo.new
57
+ photo.image = file
58
+ photo.save # spawns promote job
59
+ photo.destroy # spawns destroy job
70
60
  ```
71
61
 
72
- ## `Attacher.promote` and `Attacher.delete`
62
+ In terms of `Shrine::Attacher`, the background jobs are spawned on
63
+ `Attacher#promote_cached` (called on `Attacher#finalize`) and
64
+ `Attacher#destroy_attached`:
73
65
 
74
- In background jobs, `Attacher.promote` and `Attacher.delete` will resolve all
75
- necessary objects, and do the promotion/deletion. If `Attacher.find_record` is
76
- defined (which comes with ORM plugins), model instances will be treated as
77
- database records, with the `#id` attribute assumed to represent the primary
78
- key. Then promotion will have the following behaviour:
66
+ ```rb
67
+ attacher.assign(file)
68
+ attacher.finalize # spawns promote job
69
+ attacher.destroy_attached # spawns destroy job
70
+ ```
79
71
 
80
- 1. retrieves the database record
81
- * if record is not found, it finishes
82
- * if record is found but attachment has changed, it finishes
83
- 2. uploads cached file to permanent storage
84
- 3. reloads the database record
85
- * if record is not found, it deletes the promoted files and finishes
86
- * if record is found but attachment has changed, it deletes the promoted files and finishes
87
- 4. updates the record with the promoted files
72
+ ## Promotion
88
73
 
89
- Both `Attacher.promote` and `Attacher.delete` return a `Shrine::Attacher`
90
- instance (if the action hasn't aborted), so you can use it to perform
91
- additional tasks:
74
+ While background deletion acts only on file data, background promotion is more
75
+ complex as it deals with persistence and concurrency safety:
92
76
 
93
77
  ```rb
94
- def perform(data)
95
- attacher = Shrine::Attacher.promote(data)
96
- attacher.record.update(published: true) if attacher && attacher.record.is_a?(Post)
97
- end
78
+ attacher = Shrine::Attacher.retrieve(model: record, name: name, file: file_data)
79
+ attacher.atomic_promote
98
80
  ```
99
81
 
100
- ### Plain models
82
+ The `Attacher.retrieve` and `Attacher#atomic_promote` methods are provided by
83
+ the [`atomic_helpers`][atomic_helpers] plugin, which is automatically loaded by
84
+ your persistence plugin (`activerecord`, `sequel`). They add concurrency safety
85
+ by verifying that the attachment hasn't changed on the outside before or after
86
+ promotion.
101
87
 
102
- You can also do backgrounding with plain models which don't represent database
103
- records; the plugin will use that mode if `Attacher.find_record` is not
104
- defined. In that case promotion will have the following behaviour:
88
+ When we remove the concurrency safety, promotion would look like this:
105
89
 
106
- 1. instantiates the model
107
- 2. uploads cached file to permanent storage
108
- 3. writes promoted files to the model instance
90
+ ```rb
91
+ attacher = record.send(:"#{name}_attacher")
92
+ attacher.promote
93
+ attacher.persist
94
+ ```
95
+
96
+ ## Backgrounding blocks
109
97
 
110
- You can then retrieve the promoted files via the attacher object that
111
- `Attacher.promote` returns, and do any additional tasks if you need to.
98
+ The blocks registered by `Attacher.promote_block` and `Attacher#destroy_block`
99
+ are by default evaluated in context of a `Shrine::Attacher` instance. You can
100
+ also use the explicit version by declaring an attacher argument:
101
+
102
+ ```rb
103
+ Shrine::Attacher.promote_block do |attacher|
104
+ PromoteJob.perform_later(
105
+ attacher.class,
106
+ attacher.record,
107
+ attacher.name,
108
+ attacher.file_data,
109
+ )
110
+ end
112
111
 
113
- ## `Attacher#_promote` and `Attacher#_delete`
112
+ Shrine::Attacher.destroy_block do |attacher|
113
+ DestroyJob.perform_later(
114
+ attacher.class,
115
+ attacher.data,
116
+ )
117
+ end
118
+ ```
114
119
 
115
- The plugin modifies `Attacher#_promote` and `Attacher#_delete` to call the
116
- registered blocks with serializable attacher data, and these methods are
117
- internally called by the attacher. `Attacher#promote` and `Attacher#delete!`
118
- remain synchronous.
120
+ You can also register backgrounding blocks on attacher *instances* for more
121
+ flexibility:
119
122
 
120
123
  ```rb
121
- # asynchronous (spawn background jobs)
122
- attacher._promote
123
- attacher._delete(attachment)
124
+ photo.image_attacher.promote_block do |attacher|
125
+ PromoteJob.perform_later(
126
+ attacher.class,
127
+ attacher.record,
128
+ attacher.name,
129
+ attacher.file_data,
130
+ current_user.id, # pass arguments known at the controller level
131
+ )
132
+ end
124
133
 
125
- # synchronous
126
- attacher.promote
127
- attacher.delete!(attachment)
134
+ photo.image = file
135
+ photo.save # executes the promote block above
128
136
  ```
129
137
 
130
- ## `Attacher.dump` and `Attacher.load`
138
+ ## Other backgrounding libraries
131
139
 
132
- The plugin adds `Attacher.dump` and `Attacher.load` methods for serializing
133
- attacher object and loading it back up. You can use them to spawn background
134
- jobs for custom tasks.
140
+ If you're not using Active Job for backgrounding, you might need to retrieve
141
+ records and resolve constants from job arguments manually:
135
142
 
136
143
  ```rb
137
- data = Shrine::Attacher.dump(attacher)
138
- SomethingJob.perform_async(data)
144
+ # register backgrounding blocks for all uploaders
145
+ Shrine::Attacher.promote_block { PromoteJob.perform_async(self.class, record.class, record.id, name, file_data) }
146
+ Shrine::Attacher.destroy_block { DestroyJob.perform_async(self.class, data) }
147
+ ```
148
+ ```rb
149
+ class PromoteJob
150
+ include Sidekiq::Worker
151
+
152
+ def perform(attacher_class, record_class, record_id, name, file_data)
153
+ attacher_class = Object.const_get(attacher_class)
154
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
155
+
156
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
157
+ attacher.atomic_promote
158
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
159
+ end
160
+ end
161
+
162
+ class DestroyJob
163
+ include Sidekiq::Worker
139
164
 
140
- # ...
165
+ def perform(attacher_class, data)
166
+ attacher_class = Object.const_get(attacher_class)
141
167
 
142
- class SomethingJob
143
- def perform(data)
144
- attacher = Shrine::Attacher.load(data)
145
- # ...
168
+ attacher = attacher_class.from_data(data)
169
+ attacher.destroy
146
170
  end
147
171
  end
148
172
  ```
149
173
 
150
174
  [backgrounding]: /lib/shrine/plugins/backgrounding.rb
175
+ [derivatives]: /doc/plugins/derivatives.md#readme
176
+ [atomic_helpers]: /doc/plugins/atomic_helpers.md#readme