shrine 0.9.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.

Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +663 -0
  4. data/doc/creating_plugins.md +100 -0
  5. data/doc/creating_storages.md +108 -0
  6. data/doc/direct_s3.md +97 -0
  7. data/doc/migrating_storage.md +79 -0
  8. data/doc/regenerating_versions.md +38 -0
  9. data/lib/shrine.rb +806 -0
  10. data/lib/shrine/plugins/activerecord.rb +89 -0
  11. data/lib/shrine/plugins/background_helpers.rb +148 -0
  12. data/lib/shrine/plugins/cached_attachment_data.rb +47 -0
  13. data/lib/shrine/plugins/data_uri.rb +93 -0
  14. data/lib/shrine/plugins/default_storage.rb +39 -0
  15. data/lib/shrine/plugins/delete_invalid.rb +25 -0
  16. data/lib/shrine/plugins/determine_mime_type.rb +119 -0
  17. data/lib/shrine/plugins/direct_upload.rb +274 -0
  18. data/lib/shrine/plugins/dynamic_storage.rb +57 -0
  19. data/lib/shrine/plugins/hooks.rb +123 -0
  20. data/lib/shrine/plugins/included.rb +48 -0
  21. data/lib/shrine/plugins/keep_files.rb +54 -0
  22. data/lib/shrine/plugins/logging.rb +158 -0
  23. data/lib/shrine/plugins/migration_helpers.rb +61 -0
  24. data/lib/shrine/plugins/moving.rb +75 -0
  25. data/lib/shrine/plugins/multi_delete.rb +47 -0
  26. data/lib/shrine/plugins/parallelize.rb +62 -0
  27. data/lib/shrine/plugins/pretty_location.rb +32 -0
  28. data/lib/shrine/plugins/recache.rb +36 -0
  29. data/lib/shrine/plugins/remote_url.rb +127 -0
  30. data/lib/shrine/plugins/remove_attachment.rb +59 -0
  31. data/lib/shrine/plugins/restore_cached.rb +36 -0
  32. data/lib/shrine/plugins/sequel.rb +94 -0
  33. data/lib/shrine/plugins/store_dimensions.rb +82 -0
  34. data/lib/shrine/plugins/validation_helpers.rb +168 -0
  35. data/lib/shrine/plugins/versions.rb +177 -0
  36. data/lib/shrine/storage/file_system.rb +165 -0
  37. data/lib/shrine/storage/linter.rb +94 -0
  38. data/lib/shrine/storage/s3.rb +118 -0
  39. data/lib/shrine/version.rb +14 -0
  40. data/shrine.gemspec +46 -0
  41. metadata +364 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 21c82440698540e29ee787915b4bbdf9c3289f72
4
+ data.tar.gz: 70e3d33cd286ed3911df5d130b3f72f67c16e571
5
+ SHA512:
6
+ metadata.gz: 349f0b0bfd9c3f96e752a99e1e126c15adac5071130a0804407bd6990d76a831cb68ea86f8dd72888cf04891b4623cf61d0bd4ec5ce5eaf9e1401cea1c7c4c72
7
+ data.tar.gz: 2ba7dca90b8e1730a8fa2d303ad2750bd2d96c0d5cf52770486c912b85c63ff0c04ec4c60c48ad780a97ff5711b1da5a24d7ad9e28e381decd24b546c66ebbbd
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Janko Marohnić
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,663 @@
1
+ # Shrine
2
+
3
+ Shrine is a toolkit for file uploads in Ruby applications.
4
+
5
+ ## Resources
6
+
7
+ * Documentation: [shrinerb.com](http://shrinerb.com)
8
+ * Source: [github.com/janko-m/shrine](https://github.com/janko-m/shrine)
9
+ * Bugs: [github.com/janko-m/shrine/issues](https://github.com/janko-m/shrine/issues)
10
+ * Discussion: [groups.google.com/group/ruby-shrine](https://groups.google.com/forum/#!forum/ruby-shrine)
11
+
12
+ ## Installation
13
+
14
+ ```rb
15
+ gem "shrine"
16
+ ```
17
+
18
+ Shrine has been tested on MRI 2.1, MRI 2.2 and JRuby.
19
+
20
+ ## Basics
21
+
22
+ Here's a basic example showing how the file upload works:
23
+
24
+ ```rb
25
+ require "shrine"
26
+ require "shrine/storage/file_system"
27
+
28
+ Shrine.storages[:file_system] = Shrine::Storage::FileSystem.new("uploads")
29
+
30
+ uploader = Shrine.new(:file_system)
31
+
32
+ uploaded_file = uploader.upload(File.open("avatar.jpg"))
33
+ uploaded_file #=> #<Shrine::UploadedFile>
34
+ uploaded_file.url #=> "uploads/9260ea09d8effd.jpg"
35
+ uploaded_file.data #=>
36
+ # {
37
+ # "storage" => "file_system",
38
+ # "id" => "9260ea09d8effd.jpg",
39
+ # "metadata" => {...},
40
+ # }
41
+ ```
42
+
43
+ First we add the storage we want to use to Shrine's registry. Storages are
44
+ simple Ruby classes which perform the actual uploads. We instantiate a `Shrine`
45
+ with the storage name, and when we call `Shrine#upload` the following happens:
46
+
47
+ * a unique location is generated for the file
48
+ * metadata is extracted from the file
49
+ * the underlying storage is called to store the file
50
+ * a `Shrine::UploadedFile` is returned with these data
51
+
52
+ The argument to `Shrine#upload` needs to be an IO-like object. So, `File`,
53
+ `Tempfile` and `StringIO` are all valid arguments. But the object doesn't have
54
+ to be an actual IO, it's enough that it responds to these 5 methods:
55
+ `#read(*args)`, `#size`, `#eof?`, `#rewind` and `#close`.
56
+ `ActionDispatch::Http::UploadedFile` is one such object.
57
+
58
+ Now that we've uploaded the file to the underlying storage, we can download it:
59
+
60
+ ```rb
61
+ file = uploaded_file.download
62
+ file #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20151004-74201-1t2jacf>
63
+ ```
64
+
65
+ When we're done, we can delete the file:
66
+
67
+ ```rb
68
+ uploader.delete(uploaded_file)
69
+ uploaded_file.exists? #=> false
70
+ ```
71
+
72
+ ## Attachment
73
+
74
+ In web applications, instead of managing files directly, we want to treat them
75
+ as "attachments" to models and to tie them to the lifecycle of records. Shrine
76
+ does this by generating and including "attachment" modules.
77
+
78
+ Firstly we need to assign the special `:cache` and `:store` storages:
79
+
80
+ ```rb
81
+ Shrine.storages = {
82
+ cache: Shrine::Storage::FileSystem.new(Dir.tmpdir),
83
+ store: Shrine::Storage::FileSystem.new("public", subdirectory: "uploads"),
84
+ }
85
+ ```
86
+
87
+ Next we should create an uploader specific to the type of files we're
88
+ uploading:
89
+
90
+ ```rb
91
+ class ImageUploader < Shrine
92
+ # here goes your uploading logic
93
+ end
94
+ ```
95
+
96
+ Now if we assume that we have a "User" model, and we want our users to have an
97
+ "avatar", we can generate and include an "attachment" module:
98
+
99
+ ```rb
100
+ class User
101
+ attr_accessor :avatar_data
102
+
103
+ include ImageUploader[:avatar]
104
+ end
105
+ ```
106
+
107
+ Now our model has gained special methods for attaching avatars:
108
+
109
+ ```rb
110
+ user = User.new
111
+ user.avatar = File.open("avatar.jpg") # uploads the file to `:cache`
112
+ user.avatar #=> #<Shrine::UploadedFile>
113
+ user.avatar_url #=> "/uploads/9260ea09d8effd.jpg"
114
+ user.avatar_data #=>
115
+ # {
116
+ # "storage" => "cache",
117
+ # "id" => "9260ea09d8effd.jpg",
118
+ # "metadata" => {...},
119
+ # }
120
+ ```
121
+
122
+ The attachment module has added `#avatar`, `#avatar=` and `#avatar_url`
123
+ methods to our User. This is what's happening:
124
+
125
+ ```rb
126
+ Shrine[:avatar] #=> #<Shrine::Attachment(avatar)>
127
+ Shrine[:avatar].class #=> Module
128
+ Shrine[:avatar].instance_methods #=> [:avatar=, :avatar, :avatar_url, ...]
129
+
130
+ Shrine[:document] #=> #<Shrine::Attachment(document)>
131
+ Shrine[:document].instance_methods #=> [:document=, :document, :document_url, ...]
132
+
133
+ # If you prefer to be more explicit, you can use the expanded forms
134
+ Shrine.attachment(:avatar)
135
+ Shrine::Attachment.new(:document)
136
+ ```
137
+
138
+ The setter (`#avatar=`) caches the assigned file and writes it to the "data"
139
+ column (`avatar_data`). The getter (`#avatar`) reads the "data" column and
140
+ returns a `Shrine::UploadedFile`. The url method (`#avatar_url`) calls
141
+ `avatar.url` if the attachment is present, otherwise returns nil.
142
+
143
+ ### ORM
144
+
145
+ Your models probably won't be POROs, so Shrine ships with plugins for
146
+ Sequel and ActiveRecord ORMs. Shrine uses the "\<attachment\>\_data" column
147
+ for storing attachments, so you'll need to add it in a migration:
148
+
149
+ ```rb
150
+ add_column :users, :avatar_data, :text
151
+ ```
152
+ ```rb
153
+ Shrine.plugin :sequel
154
+ ```
155
+ ```rb
156
+ class User < Sequel::Model
157
+ include ImageUploader[:avatar]
158
+ end
159
+ ```
160
+
161
+ In addition to getters and setters, the ORM plugins add the appropriate
162
+ callbacks:
163
+
164
+ ```rb
165
+ user.avatar = File.open("avatar.jpg")
166
+ user.avatar.storage_key #=> "cache"
167
+ user.save
168
+ user.avatar.storage_key #=> "store"
169
+ user.destroy
170
+ user.avatar.exists? #=> false
171
+ ```
172
+
173
+ This is how you would typically create the form for a `@user`:
174
+
175
+ ```erb
176
+ <form action="/users" method="post" enctype="multipart/form-data">
177
+ <input name="user[avatar]" type="hidden" value="<%= @user.avatar_data %>">
178
+ <input name="user[avatar]" type="file">
179
+ </form>
180
+ ```
181
+
182
+ The "file" field is for file upload, while the "hidden" field is to make the
183
+ file persist in case of validation errors, and for direct uploads.
184
+
185
+ ## Direct uploads
186
+
187
+ Shrine comes with a `direct_upload` plugin which provides an endpoint
188
+ (implemented in [Roda]) that can be used for AJAX uploads.
189
+
190
+ ```rb
191
+ Shrine.plugin :direct_upload # Exposes a Roda endpoint
192
+ ```
193
+ ```rb
194
+ Rails.application.routes.draw do
195
+ # adds `POST /attachments/images/:storage/:name`
196
+ mount ImageUploader.direct_endpoint => "/attachments/images"
197
+ end
198
+ ```
199
+ ```sh
200
+ $ curl -F "file=@/path/to/avatar.jpg" localhost:3000/attachments/images/cache/avatar
201
+ # {"id":"43kewit94.jpg","storage":"cache","metadata":{...}}
202
+ ```
203
+
204
+ There are many great JavaScript libraries for AJAX file uploads, for example
205
+ this is how we could hook up [jQuery-File-Upload] to our endpoint:
206
+
207
+ ```js
208
+ $('[type="file"]').fileupload({
209
+ url: '/attachments/images/cache/avatar',
210
+ paramName: 'file',
211
+ done: function(e, data) { $(this).prev().value(data.result) }
212
+ });
213
+ ```
214
+
215
+ This plugin also provides a route for direct S3 uploads. See the [example app]
216
+ for how you can do multiple uploads directly to S3.
217
+
218
+ ## Processing
219
+
220
+ Whenever a file is uploaded, `Shrine#process` is called, and this is where
221
+ you're expected to define your processing.
222
+
223
+ ```rb
224
+ class ImageUploader < Shrine
225
+ def process(io, context)
226
+ if context[:phase] == :store
227
+ # processing...
228
+ end
229
+ end
230
+ end
231
+ ```
232
+
233
+ The `io` is the file being uploaded, and `context` we'll leave for later. You
234
+ may be wondering why we need this conditional. Well, when an attachment is
235
+ assigned and saved, an "upload" actually happens two times. First the file is
236
+ "uploaded" to `:cache` on assignment, and then the cached file is reuploaded to
237
+ `:store` on save.
238
+
239
+ Ok, now how do we do the actual processing? Well, Shrine actually doesn't ship
240
+ with any image processing functionality, because that is a generic problem that
241
+ belongs in a separate gem. So, I created the [image_processing] gem which you
242
+ can use with Shrine:
243
+
244
+ ```rb
245
+ require "image_processing/mini_magick"
246
+
247
+ class ImageUploader < Shrine
248
+ include ImageProcessing::MiniMagick
249
+
250
+ def process(io, context)
251
+ if context[:phase] == :store
252
+ process_to_limit!(io.download, 700, 700)
253
+ end
254
+ end
255
+ end
256
+ ```
257
+
258
+ Notice that we needed to call `io.download`. This is because the original file
259
+ was already stored to `:cache`, and now this cached file is being uploaded to
260
+ `:store`. The cached file is an instance of `Shrine::UploadedFile`, but for
261
+ processing we need to work with actual files, so we first need to download it.
262
+
263
+ In general, processing works in a way that if `#process` returns a file, Shrine
264
+ continues storing that file, otherwise if nil is returned, Shrine continues
265
+ storing the original file.
266
+
267
+ ### Versions
268
+
269
+ Often you'll want to store various thumbnails alongside your original image.
270
+ For that you just need to load the `versions` plugin, and now in `#process`
271
+ you can return a Hash of versions:
272
+
273
+ ```rb
274
+ require "image_processing/mini_magick"
275
+
276
+ class ImageUploader < Shrine
277
+ include ImageProcessing::MiniMagick
278
+ plugin :versions, names: [:large, :medium, :small]
279
+
280
+ def process(io, context)
281
+ if context[:phase] == :store
282
+ size_700 = process_to_limit!(io.download, 700, 700)
283
+ size_500 = process_to_limit!(size_700, 500, 500)
284
+ size_300 = process_to_limit!(size_500, 300, 300)
285
+
286
+ {large: size_700, medium: size_500, small: size_300}
287
+ end
288
+ end
289
+ end
290
+ ```
291
+
292
+ As you see, instead of a complex class-level DSL, Shrine provides a very simple
293
+ instance-level interface where you're in complete control over processing. The
294
+ processed files are Ruby Tempfiles and they should eventually get deleted by
295
+ themselves, but you can also use the `moving` plugin to delete them immediately
296
+ after upload.
297
+
298
+ Now when you access the stored attachment, a Hash of versions will be returned
299
+ instead:
300
+
301
+ ```rb
302
+ user.avatar #=>
303
+ # {
304
+ # large: #<Shrine::UploadedFile>,
305
+ # medium: #<Shrine::UploadedFile>,
306
+ # small: #<Shrine::UploadedFile>,
307
+ # }
308
+ user.avatar.class #=> Hash
309
+
310
+ # With the store_dimensions plugin
311
+ user.avatar[:large].width #=> 700
312
+ user.avatar[:medium].width #=> 500
313
+ user.avatar[:small].width #=> 300
314
+
315
+ # The plugin expands this method to accept version names.
316
+ user.avatar_url(:large) #=> "..."
317
+ ```
318
+
319
+ ## Context
320
+
321
+ You may have noticed the `context` variable as the second argument to
322
+ `Shrine#process`. This variable contains information about the context in
323
+ which the file is uploaded.
324
+
325
+ ```rb
326
+ class ImageUploader < Shrine
327
+ def process(io, context)
328
+ puts context
329
+ end
330
+ end
331
+ ```
332
+ ```rb
333
+ user = User.new
334
+ user.avatar = File.open("avatar.jpg") # "cache"
335
+ user.save # "store"
336
+ ```
337
+ ```
338
+ {:name=>:avatar, :record=>#<User:0x007fe1627f1138>, :phase=>:cache}
339
+ {:name=>:avatar, :record=>#<User:0x007fe1627f1138>, :phase=>:store}
340
+ ```
341
+
342
+ The `:name` is the name of the attachment, in this case "avatar". The `:record`
343
+ is the model instance, in this case instance of `User`. As for `:phase`, in web
344
+ applications a file upload isn't an event that happens at once, it's a process
345
+ that happens in *phases*. By default there are only 2 phases, "cache" and
346
+ "store", other plugins add more of them.
347
+
348
+ Context is really useful for doing conditional processing and validation, since
349
+ we have access to the record and attachment name. In general the context is
350
+ used deeply in Shrine for various purposes.
351
+
352
+ ## Validations
353
+
354
+ Validations are registered by calling `Shrine::Attacher.validate`, and are best
355
+ done with the `validation_helpers` plugin:
356
+
357
+ ```rb
358
+ class ImageUploader < Shrine
359
+ plugin :validation_helpers
360
+
361
+ Attacher.validate do
362
+ # Evaluated inside an instance of Shrine::Attacher.
363
+ if record.guest?
364
+ validate_max_size 2*1024*1024, message: "is too large (max is 2 MB)"
365
+ end
366
+ end
367
+ end
368
+ ```
369
+
370
+ ```rb
371
+ user = User.new
372
+ user.avatar = File.open("big_image.jpg")
373
+ user.valid? #=> false
374
+ user.errors.to_hash #=> {avatar: ["is too large (max is 2 MB)"]}
375
+ ```
376
+
377
+ ## Metadata
378
+
379
+ By default Shrine extracts and stores general file metadata:
380
+
381
+ ```rb
382
+ class UsersController < ApplicationController
383
+ def create
384
+ user = User.create(params[:user])
385
+ user.avatar.metadata #=>
386
+ # {
387
+ # "filename" => "my_avatar.jpg",
388
+ # "mime_type" => "image/jpeg",
389
+ # "size" => 345993,
390
+ # }
391
+
392
+ user.avatar.original_filename #=> "my_avatar.jpg"
393
+ user.avatar.mime_type #=> "image/jpeg"
394
+ user.avatar.size #=> 345993
395
+ end
396
+ end
397
+ ```
398
+
399
+ ### MIME type
400
+
401
+ By default, "mime_type" is inherited from `#content_type` of the uploaded file.
402
+ In case of Rails, this value is set from the `Content-Type` header, which the
403
+ browser sets solely based on the extension of the uploaded file. This means
404
+ that by default Shrine's "mime_type" is *not* guaranteed to hold the actual
405
+ MIME type of the file.
406
+
407
+ To help with that Shrine provides the `extract_mime_type` plugin, which by
408
+ deafult uses the UNIX [file] utility to determine the actual MIME type:
409
+
410
+ ```rb
411
+ Shrine.plugin :extract_mime_type
412
+ ```
413
+ ```rb
414
+ user = User.create(avatar: File.open("image.mp4")) # image with a .mp4 extension
415
+ user.avatar.mime_type #=> "image/png"
416
+ ```
417
+
418
+ ### Dimensions
419
+
420
+ Shrine ships with the `store_dimensions` plugin which extracts dimensions
421
+ using the [fastimage] gem.
422
+
423
+ ```rb
424
+ ImageUploader.plugin :store_dimensions
425
+ ```
426
+ ```rb
427
+ user = User.create(avatar: File.open("image.jpg"))
428
+ user.avatar.width #=> 400
429
+ user.avatar.height #=> 500
430
+ ```
431
+
432
+ The fastimage gem has built-in protection against [image bombs].
433
+
434
+ ### Custom metadata
435
+
436
+ You can also extract and store custom metadata, by overriding
437
+ `Shrine#extract_metadata`:
438
+
439
+ ```rb
440
+ class ImageUploader < Shrine
441
+ def extract_metadata(io, context)
442
+ metadata = super
443
+ metadata["custom"] = extract_custom(io)
444
+ metadata
445
+ end
446
+ end
447
+ ```
448
+
449
+ ## Default URL
450
+
451
+ When attachment is missing, `user.avatar_url` by default returns nil. This
452
+ because it internally calls `Shrine#default_url`, which returns nil unless
453
+ overriden. For custom default URLs simply override the method:
454
+
455
+ ```rb
456
+ class ImageUploader < Shrine
457
+ def default_url(context)
458
+ "/images/fallback/#{context[:name]}.png"
459
+ end
460
+ end
461
+ ```
462
+
463
+ ## Locations
464
+
465
+ By default files will all be put in the same folder. If you want that each
466
+ record has its own directory, you can use the `pretty_location` plugin:
467
+
468
+ ```rb
469
+ Shrine.plugin :pretty_location
470
+ ```
471
+ ```rb
472
+ user = User.create(avatar: File.open("avatar.jpg"))
473
+ user.avatar.id #=> "user/34/avatar/34krtreds2df.jpg"
474
+ ```
475
+
476
+ If you want to generate your own locations, simply override
477
+ `Shrine#generate_location`:
478
+
479
+ ```rb
480
+ class ImageUploader < Shrine
481
+ def generate_location(io, context)
482
+ # your custom logic
483
+ end
484
+ end
485
+ ```
486
+
487
+ Note that in this case should be careful to make the locations unique,
488
+ otherwise dirty tracking won't be detected properly (you can use
489
+ `Shrine#generate_uid`).
490
+
491
+ When using `Shrine` directly you can bypass `#generate_location` by passing in
492
+ `:location`
493
+
494
+ ```rb
495
+ file = File.open("avatar.jpg")
496
+ Shrine.new(:store).upload(file, location: "a/specific/location.jpg")
497
+ ```
498
+
499
+ ## Amazon S3
500
+
501
+ So far in the examples we've only used the FileSystem storage. However, Shrine
502
+ also ships with S3 storage (which internally uses the [aws-sdk] gem).
503
+
504
+ ```
505
+ gem "aws-sdk", "~> 2.1"
506
+ ```
507
+
508
+ It's typically good to use FileSystem for `:cache`, and S3 for `:store`:
509
+
510
+ ```rb
511
+ require "shrine"
512
+ require "shrine/storage/file_system"
513
+ require "shrine/storage/s3"
514
+
515
+ s3_options = {
516
+ access_key_id: "<ACCESS_KEY_ID>", # "xyz"
517
+ secret_access_key: "<SECRET_ACCESS_KEY>", # "abc"
518
+ region: "<REGION>", # "eu-west-1"
519
+ bucket: "<BUCKET>", # "my-app"
520
+ }
521
+
522
+ Shrine.storages = {
523
+ cache: Shrine::Storage::FileSystem.new("public", subdirectory: "uploads"),
524
+ store: Shrine::Storage::S3.new(s3_options),
525
+ }
526
+ ```
527
+
528
+ ```rb
529
+ user = User.new(avatar: File.open(:avatar))
530
+ user.avatar.url #=> "/uploads/j4k343ui12ls9.jpg"
531
+ user.save
532
+ user.avatar.url #=> "https://s3-sa-east-1.amazonaws.com/my-bucket/0943sf8gfk13.jpg"
533
+ ```
534
+
535
+ If you're using S3 for both `:cache` and `:store`, saving the record will
536
+ execute an S3 COPY command if possible, which avoids reuploading the file.
537
+ Also, the `versions` plugin takes advantage of S3's MULTI DELETE capabilities,
538
+ so versions are deleted with a single HTTP request.
539
+
540
+ ## Background jobs
541
+
542
+ Unlike other uploading libraries, Shrine embraces that putting phases of file
543
+ upload into background jobs is essential for scaling and good user experience,
544
+ so it ships with `background_helpers` plugin which makes backgrounding really
545
+ easy:
546
+
547
+ ```rb
548
+ Shrine.plugin :background_helpers
549
+ Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
550
+ Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
551
+ ```
552
+ ```rb
553
+ class UploadJob
554
+ include Sidekiq::Worker
555
+ def perform(data)
556
+ Shrine::Attacher.promote(data)
557
+ end
558
+ end
559
+ ```
560
+ ```rb
561
+ class DeleteJob
562
+ include Sidekiq::Worker
563
+ def perform(data)
564
+ Shrine::Attacher.delete(data)
565
+ end
566
+ end
567
+ ```
568
+
569
+ The above puts all promoting (moving to store) and deleting of files into a
570
+ background Sidekiq job. Obviously instead of Sidekiq you can just as well use
571
+ any other backgrounding library.
572
+
573
+ ### Seamless user experience
574
+
575
+ In combination with direct upload for caching, this provides a completely
576
+ seamless user experience. First the user ansynchronosuly caches the file and
577
+ hopefully sees a nice progress bar. After this is finishes and user submits the
578
+ form, promoting will be kicked off into a background job, and the record will
579
+ be saved with the cached image. If your cache is public (e.g. in the "public"
580
+ folder), the end user will immediately see their uploaded file, because the URL
581
+ will point to the cached version.
582
+
583
+ In the meanwhile, what `#promote` does is it uploads the cached file `:store`,
584
+ and writes the stored file to the column. When the record gets saved, the URL
585
+ will switch from filesystem to S3, but the user won't even notice that
586
+ something happened, because they will still see the same image.
587
+
588
+ ### Generality
589
+
590
+ This solution is completely agnostic about what kind of attachment it is
591
+ uploading/deleting, and for which model. This means that all attachments can
592
+ use this same worker. Also, there is no need for any extra columns.
593
+
594
+ ### Safety
595
+
596
+ It is possible that the user changes their mind and reuploads a new file before
597
+ the background job finished promoting. With a naive implementation, this means
598
+ that after uploading a new file, there can happen a brief moment where the user
599
+ sees the old file again, which can be upsetting.
600
+
601
+ Shrine handles this gracefully. After `#promote` uploads the cached file to
602
+ `:store`, it checks if the cached file still matches the file in the record
603
+ column. If the files are different, that means the user uploaded a new
604
+ attachment, and Shrine won't do the replacement. Additionally, this job is
605
+ idempotent, meaning it can be safely repeated in case of failure.
606
+
607
+ ## Clearing cache
608
+
609
+ Your `:cache` storage will grow over time, so you'll want to periodically clean
610
+ it. If you're using FileSystem as your `:cache`, you can put this in a
611
+ scheduled job:
612
+
613
+ ```rb
614
+ file_system = Shrine.storages[:cache]
615
+ file_system.clear!(older_than: 1.week.ago) # adjust the time
616
+ ```
617
+
618
+ If your `:cache` is S3, Amazon provides settings for automatic cache clearing,
619
+ see [this article](http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html).
620
+
621
+ ## Plugins
622
+
623
+ Shrine comes with a small core which provides only the essential functionality.
624
+ However, it comes with a lot of additional features which can be loaded via
625
+ plugins. This way you can choose exactly how much Shrine does for you. Shrine
626
+ itself [ships with over 25 plugins], most of them I haven't managed to cover
627
+ here.
628
+
629
+ The plugin system respects inheritance, so you can choose which plugins will
630
+ be applied to which uploaders:
631
+
632
+ ```rb
633
+ Shrine.plugin :logging # enables logging for all uploaders
634
+
635
+ class ImageUploader < Shrine
636
+ plugin :store_dimensions # stores dimensions only for this uploader
637
+ end
638
+ ```
639
+
640
+ ## Inspiration
641
+
642
+ Shrine was heavily inspired by [Refile] and [Roda]. From Refile it borrows the
643
+ idea of "backends" (here named "storages"), attachment interface, and direct
644
+ uploads. From Roda it borrows the implementation of an extensible [plugin
645
+ system].
646
+
647
+ ## License
648
+
649
+ The gem is available as open source under the terms of the [MIT License].
650
+
651
+ [Contributor Covenant]: http://contributor-covenant.org
652
+ [image_processing]: https://github.com/janko-m/image_processing
653
+ [fastimage]: https://github.com/sdsykes/fastimage
654
+ [file]: http://linux.die.net/man/1/file
655
+ [image bombs]: https://www.bamsoftware.com/hacks/deflate.html
656
+ [aws-sdk]: https://github.com/aws/aws-sdk-ruby
657
+ [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
658
+ [Roda]: https://github.com/jeremyevans/roda
659
+ [Refile]: https://github.com/refile/refile
660
+ [plugin system]: http://twin.github.io/the-plugin-system-of-sequel-and-roda/
661
+ [MIT License]: http://opensource.org/licenses/MIT
662
+ [example app]: https://github.com/janko-m/shrine-example
663
+ [ships with over 25 plugins]: http://shrinerb.com#plugins