shrine 3.1.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +11 -4
  4. data/doc/advantages.md +4 -4
  5. data/doc/attacher.md +2 -2
  6. data/doc/carrierwave.md +24 -12
  7. data/doc/changing_derivatives.md +1 -1
  8. data/doc/changing_location.md +6 -5
  9. data/doc/design.md +134 -85
  10. data/doc/direct_s3.md +26 -0
  11. data/doc/external/articles.md +57 -45
  12. data/doc/external/extensions.md +41 -35
  13. data/doc/external/misc.md +23 -8
  14. data/doc/getting_started.md +156 -85
  15. data/doc/metadata.md +80 -44
  16. data/doc/multiple_files.md +1 -1
  17. data/doc/paperclip.md +28 -9
  18. data/doc/plugins/add_metadata.md +112 -35
  19. data/doc/plugins/atomic_helpers.md +41 -3
  20. data/doc/plugins/backgrounding.md +12 -2
  21. data/doc/plugins/column.md +36 -7
  22. data/doc/plugins/default_url.md +6 -3
  23. data/doc/plugins/derivatives.md +83 -44
  24. data/doc/plugins/download_endpoint.md +5 -5
  25. data/doc/plugins/dynamic_storage.md +1 -1
  26. data/doc/plugins/entity.md +12 -4
  27. data/doc/plugins/form_assign.md +5 -5
  28. data/doc/plugins/included.md +25 -5
  29. data/doc/plugins/infer_extension.md +9 -0
  30. data/doc/plugins/instrumentation.md +1 -1
  31. data/doc/plugins/metadata_attributes.md +1 -0
  32. data/doc/plugins/mirroring.md +1 -1
  33. data/doc/plugins/model.md +8 -3
  34. data/doc/plugins/persistence.md +10 -1
  35. data/doc/plugins/remote_url.md +6 -1
  36. data/doc/plugins/remove_invalid.md +9 -1
  37. data/doc/plugins/sequel.md +1 -1
  38. data/doc/plugins/store_dimensions.md +10 -0
  39. data/doc/plugins/type_predicates.md +96 -0
  40. data/doc/plugins/upload_endpoint.md +1 -1
  41. data/doc/plugins/upload_options.md +1 -1
  42. data/doc/plugins/url_options.md +4 -4
  43. data/doc/plugins/validation.md +14 -4
  44. data/doc/plugins/versions.md +7 -7
  45. data/doc/processing.md +287 -123
  46. data/doc/refile.md +9 -9
  47. data/doc/release_notes/2.8.0.md +1 -1
  48. data/doc/release_notes/3.0.0.md +1 -1
  49. data/doc/release_notes/3.2.0.md +96 -0
  50. data/doc/release_notes/3.2.1.md +31 -0
  51. data/doc/release_notes/3.2.2.md +14 -0
  52. data/doc/release_notes/3.3.0.md +105 -0
  53. data/doc/release_notes/3.4.0.md +35 -0
  54. data/doc/securing_uploads.md +2 -2
  55. data/doc/storage/memory.md +19 -0
  56. data/doc/storage/s3.md +104 -77
  57. data/doc/testing.md +12 -2
  58. data/doc/upgrading_to_3.md +99 -53
  59. data/lib/shrine.rb +9 -8
  60. data/lib/shrine/attacher.rb +20 -10
  61. data/lib/shrine/attachment.rb +2 -2
  62. data/lib/shrine/plugins.rb +22 -0
  63. data/lib/shrine/plugins/activerecord.rb +3 -3
  64. data/lib/shrine/plugins/add_metadata.rb +20 -5
  65. data/lib/shrine/plugins/backgrounding.rb +2 -2
  66. data/lib/shrine/plugins/default_url.rb +1 -1
  67. data/lib/shrine/plugins/derivation_endpoint.rb +13 -8
  68. data/lib/shrine/plugins/derivatives.rb +59 -30
  69. data/lib/shrine/plugins/determine_mime_type.rb +5 -3
  70. data/lib/shrine/plugins/entity.rb +12 -11
  71. data/lib/shrine/plugins/instrumentation.rb +12 -18
  72. data/lib/shrine/plugins/mirroring.rb +8 -8
  73. data/lib/shrine/plugins/model.rb +3 -3
  74. data/lib/shrine/plugins/presign_endpoint.rb +16 -4
  75. data/lib/shrine/plugins/pretty_location.rb +1 -1
  76. data/lib/shrine/plugins/processing.rb +1 -1
  77. data/lib/shrine/plugins/refresh_metadata.rb +2 -2
  78. data/lib/shrine/plugins/remote_url.rb +3 -3
  79. data/lib/shrine/plugins/remove_attachment.rb +5 -0
  80. data/lib/shrine/plugins/remove_invalid.rb +10 -5
  81. data/lib/shrine/plugins/sequel.rb +1 -1
  82. data/lib/shrine/plugins/store_dimensions.rb +4 -2
  83. data/lib/shrine/plugins/type_predicates.rb +113 -0
  84. data/lib/shrine/plugins/upload_endpoint.rb +10 -5
  85. data/lib/shrine/plugins/upload_options.rb +2 -2
  86. data/lib/shrine/plugins/url_options.rb +2 -2
  87. data/lib/shrine/plugins/validation.rb +9 -7
  88. data/lib/shrine/storage/linter.rb +4 -4
  89. data/lib/shrine/storage/memory.rb +5 -3
  90. data/lib/shrine/storage/s3.rb +117 -38
  91. data/lib/shrine/version.rb +1 -1
  92. data/shrine.gemspec +8 -8
  93. metadata +42 -34
data/doc/testing.md CHANGED
@@ -119,7 +119,7 @@ module TestData
119
119
  small: uploaded_image,
120
120
  )
121
121
 
122
- attacher.column_data
122
+ attacher.column_data # or attacher.data in case of postgres jsonb column
123
123
  end
124
124
 
125
125
  def uploaded_image
@@ -128,7 +128,7 @@ module TestData
128
128
  # for performance we skip metadata extraction and assign test metadata
129
129
  uploaded_file = Shrine.upload(file, :store, metadata: false)
130
130
  uploaded_file.metadata.merge!(
131
- "size" => file.size,
131
+ "size" => File.size(file.path),
132
132
  "mime_type" => "image/jpeg",
133
133
  "filename" => "test.jpg",
134
134
  )
@@ -251,6 +251,16 @@ TestMode.disable_processing(Photo.image_attacher) do
251
251
  end
252
252
  ```
253
253
 
254
+ ## Testing direct upload
255
+
256
+ If you'd like to unit-test direct upload on the server side, you can
257
+ emulate it by uploading a file to `cache` and then assigning it to the record.
258
+
259
+ ```rb
260
+ cached_file = Shrine.upload(some_file, :cache)
261
+ record.attachment = cached_file.to_json
262
+ ```
263
+
254
264
  [DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
255
265
  [`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
256
266
  [aws-sdk-ruby stubs]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html
@@ -237,7 +237,7 @@ class PromoteJob
237
237
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
238
238
  # attachment has changed or record has been deleted, nothing to do
239
239
  end
240
- and
240
+ end
241
241
  ```
242
242
  ```rb
243
243
  class DestroyJob
@@ -258,7 +258,7 @@ class DestroyJob
258
258
  attacher = attacher_class.from_data(data)
259
259
  attacher.destroy
260
260
  end
261
- and
261
+ end
262
262
  ```
263
263
 
264
264
  ### Attacher backgrounding
@@ -283,8 +283,9 @@ attacher.destroy_background # calls destroy block
283
283
  ## Versions
284
284
 
285
285
  The `versions`, `processing`, `recache`, and `delete_raw` plugins have been
286
- deprecated in favour of the new **[`derivatives`][derivatives]** plugin. Let's
287
- assume you have the following `versions` code:
286
+ deprecated in favour of the new **[`derivatives`][derivatives]** plugin.
287
+
288
+ Let's assume you have the following `versions` configuration:
288
289
 
289
290
  ```rb
290
291
  class ImageUploader < Shrine
@@ -307,26 +308,32 @@ class ImageUploader < Shrine
307
308
  end
308
309
  end
309
310
  ```
311
+
312
+ When an attached file is promoted to permanent storage, the versions would
313
+ automatically get generated:
314
+
310
315
  ```rb
311
316
  photo = Photo.new(photo_params)
312
317
 
313
318
  if photo.valid?
314
- photo.save # automatically calls processing block
319
+ photo.save # generates versions on promotion
315
320
  # ...
316
321
  else
317
322
  # ...
318
323
  end
319
324
  ```
320
325
 
321
- With `derivatives`, the original file is automatically downloaded and retained,
322
- so the code is now much simpler:
326
+ With `derivatives`, the original file is automatically downloaded and retained
327
+ during processing, so the setup is simpler:
323
328
 
324
329
  ```rb
325
- Shrine.plugin :derivatives, versions_compatibility: true # handle versions column format
330
+ Shrine.plugin :derivatives,
331
+ create_on_promote: true, # automatically create derivatives on promotion
332
+ versions_compatibility: true # handle versions column format
326
333
  ```
327
334
  ```rb
328
335
  class ImageUploader < Shrine
329
- Attacher.derivatives_processor do |original|
336
+ Attacher.derivatives do |original|
330
337
  magick = ImageProcessing::MiniMagick.source(original)
331
338
 
332
339
  # the :original file should NOT be included anymore
@@ -342,28 +349,13 @@ end
342
349
  photo = Photo.new(photo_params)
343
350
 
344
351
  if photo.valid?
345
- photo.image_derivatives! if photo.image_changed? # create derivatives
346
- photo.save # automatically calls processing block
352
+ photo.save # creates derivatives on promotion
347
353
  # ...
348
354
  else
349
355
  # ...
350
356
  end
351
357
  ```
352
358
 
353
- If you have multiple places where you need to generate derivatives, and want it
354
- to happen automatically like it did with the `versions` plugin, you can
355
- override `Attacher#promote` to call `Attacher#create_derivatives` before
356
- promotion:
357
-
358
- ```rb
359
- class Shrine::Attacher
360
- def promote(*)
361
- create_derivatives
362
- super
363
- end
364
- end
365
- ```
366
-
367
359
  ### Accessing derivatives
368
360
 
369
361
  The derivative URLs are accessed in the same way as versions:
@@ -372,7 +364,7 @@ The derivative URLs are accessed in the same way as versions:
372
364
  photo.image_url(:small)
373
365
  ```
374
366
 
375
- But the derivatives themselves are accessed differently:
367
+ But the files themselves are accessed differently:
376
368
 
377
369
  ```rb
378
370
  # versions
@@ -426,8 +418,8 @@ database column in different formats:
426
418
 
427
419
  The `:versions_compatibility` flag to the `derivatives` plugin enables it to
428
420
  read the `versions` format, which aids in transition. Once the `derivatives`
429
- plugin has been deployed to production, you can switch existing records to the
430
- new column format:
421
+ plugin has been deployed to production, you can update existing records with
422
+ the new column format:
431
423
 
432
424
  ```rb
433
425
  Photo.find_each do |photo|
@@ -447,7 +439,7 @@ creation in the `PromoteJob` instead of the controller:
447
439
  class PromoteJob
448
440
  include Sidekiq::Worker
449
441
 
450
- def perform(attacher_class, record_class, record.id, name, file_data)
442
+ def perform(attacher_class, record_class, record_id, name, file_data)
451
443
  attacher_class = Object.const_get(attacher_class)
452
444
  record = Object.const_get(record_class).find(record_id) # if using Active Record
453
445
 
@@ -467,11 +459,11 @@ creating another derivatives processor that you will trigger in the controller:
467
459
 
468
460
  ```rb
469
461
  class ImageUploader < Shrine
470
- Attacher.derivatives_processor do |original|
462
+ Attacher.derivatives do |original|
471
463
  # this will be triggered in the background job
472
464
  end
473
465
 
474
- Attacher.derivatives_processor :foreground do |original|
466
+ Attacher.derivatives :foreground do |original|
475
467
  # this will be triggered in the controller
476
468
  end
477
469
  end
@@ -488,48 +480,77 @@ else
488
480
  end
489
481
  ```
490
482
 
491
- #### Parallelize
483
+ ### Default URL
492
484
 
493
- The `parallelize` plugin has been removed. The `derivatives` plugin is
494
- thread-safe, so you can parallelize uploading processed files manually:
485
+ If you were using the `default_url` plugin, the `Attacher.default_url` now
486
+ receives a `:derivative` option:
495
487
 
496
488
  ```rb
497
- # Gemfile
498
- gem "concurrent-ruby"
489
+ Attacher.default_url do |derivative: nil, **|
490
+ "https://my-app.com/fallbacks/#{derivative}.jpg" if derivative
491
+ end
499
492
  ```
500
- ```rb
501
- require "concurrent"
502
493
 
503
- derivatives = attacher.process_derivatives
494
+ #### Fallback to original
504
495
 
505
- tasks = derivatives.map do |name, file|
506
- Concurrent::Promises.future(name, file) do |name, file|
507
- attacher.add_derivative(name, file)
508
- end
509
- end
496
+ With the `versions` plugin, a missing version URL would automatically fall back
497
+ to the original file. The `derivatives` plugin has no such fallback, but you
498
+ can configure it manually:
510
499
 
511
- Concurrent::Promises.zip(*tasks).wait!
500
+ ```rb
501
+ Attacher.default_url do |derivative: nil, **|
502
+ file&.url if derivative
503
+ end
512
504
  ```
513
505
 
514
- #### Default URL
506
+ #### Fallback to version
515
507
 
516
- The `derivatives` plugin integrates with the `default_url` plugin:
508
+ The `versions` plugin had the ability to fall back missing version URL to
509
+ another version that already exists. The `derivatives` plugin doesn't have this
510
+ built in, but you can implement it as follows:
517
511
 
518
512
  ```rb
513
+ DERIVATIVE_FALLBACKS = { foo: :bar, ... }
514
+
519
515
  Attacher.default_url do |derivative: nil, **|
520
- "https://my-app.com/fallbacks/#{derivative}.jpg" if derivative
516
+ derivatives[DERIVATIVE_FALLBACKS[derivative]]&.url if derivative
521
517
  end
522
518
  ```
523
519
 
524
- However, it doesn't implement any other URL fallbacks that the `versions`
525
- plugin has for missing derivatives.
520
+ ### Location
521
+
522
+ The `Shrine#generate_location` method will now receive a `:derivative`
523
+ parameter instead of `:version`:
524
+
525
+ ```rb
526
+ class MyUploader < Shrine
527
+ def generate_location(io, derivative: nil, **)
528
+ derivative #=> :large, :medium, :small, ...
529
+ # ...
530
+ end
531
+ end
532
+ ```
533
+
534
+ ### Overwriting original
535
+
536
+ With the `derivatives` plugin, saving processed files separately from the
537
+ original file, so the original file is automatically kept. This means it's not
538
+ possible anymore to overwrite the original file as part of processing.
539
+
540
+ However, **it's highly recommended to always keep the original file**, even if
541
+ you don't plan to use it. That way, if there is ever a need to reprocess
542
+ derivatives, you have the original file to use as a base.
543
+
544
+ That being said, if you still want to overwrite the original file, [this
545
+ thread][overwriting original] has some tips.
526
546
 
527
547
  ## Other
528
548
 
529
549
  ### Processing
530
550
 
531
551
  The `processing` plugin has been deprecated over the new
532
- [`derivatives`][derivatives] plugin. If you were modifying the original file:
552
+ [`derivatives`][derivatives] plugin. If you were previously replacing the
553
+ original file:
533
554
 
534
555
  ```rb
535
556
  class MyUploader < Shrine
@@ -549,7 +570,7 @@ you should now add the processed file as a derivative:
549
570
  class MyUploader < Shrine
550
571
  plugin :derivatives
551
572
 
552
- Attacher.derivatives_processor do |original|
573
+ Attacher.derivatives do |original|
553
574
  magick = ImageProcessing::MiniMagick.source(original)
554
575
 
555
576
  { normalized: magick.resize_to_limit!(1600, 1600) }
@@ -557,6 +578,30 @@ class MyUploader < Shrine
557
578
  end
558
579
  ```
559
580
 
581
+ ### Parallelize
582
+
583
+ The `parallelize` plugin has been removed. With `derivatives` plugin you can
584
+ parallelize uploading processed files manually:
585
+
586
+ ```rb
587
+ # Gemfile
588
+ gem "concurrent-ruby"
589
+ ```
590
+ ```rb
591
+ require "concurrent"
592
+
593
+ attacher = photo.image_attacher
594
+ derivatives = attacher.process_derivatives
595
+
596
+ tasks = derivatives.map do |name, file|
597
+ Concurrent::Promises.future(name, file) do |name, file|
598
+ attacher.add_derivative(name, file)
599
+ end
600
+ end
601
+
602
+ Concurrent::Promises.zip(*tasks).wait!
603
+ ```
604
+
560
605
  ### Logging
561
606
 
562
607
  The `logging` plugin has been removed in favour of the
@@ -604,7 +649,7 @@ attacher.copy(other_attacher)
604
649
  with
605
650
 
606
651
  ```rb
607
- attacher.attach other_attacher.file
652
+ attacher.set attacher.upload(other_attacher.file)
608
653
  attacher.add_derivatives other_attacher.derivatives # if using derivatives
609
654
  ```
610
655
 
@@ -662,3 +707,4 @@ end
662
707
  [derivatives]: https://shrinerb.com/docs/plugins/derivatives
663
708
  [instrumentation]: https://shrinerb.com/docs/plugins/instrumentation
664
709
  [mirroring]: https://shrinerb.com/docs/plugins/mirroring
710
+ [overwriting original]: https://discourse.shrinerb.com/t/keep-original-file-after-processing/50/4
data/lib/shrine.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "shrine/version"
4
3
  require "shrine/uploaded_file"
5
4
  require "shrine/attacher"
6
5
  require "shrine/attachment"
7
6
  require "shrine/plugins"
7
+ require "shrine/version"
8
8
 
9
9
  require "securerandom"
10
10
  require "json"
@@ -14,7 +14,8 @@ require "logger"
14
14
  # Core class that handles uploading files to specified storage.
15
15
  class Shrine
16
16
  # A generic exception used by Shrine.
17
- class Error < StandardError; end
17
+ class Error < StandardError
18
+ end
18
19
 
19
20
  # Raised when a file is not a valid IO.
20
21
  class InvalidFile < Error
@@ -67,9 +68,9 @@ class Shrine
67
68
  #
68
69
  # Shrine.plugin MyPlugin
69
70
  # Shrine.plugin :my_plugin
70
- def plugin(plugin, *args, &block)
71
+ def plugin(plugin, *args, **kwargs, &block)
71
72
  plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
72
- plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
73
+ Plugins.load_dependencies(plugin, self, *args, **kwargs, &block)
73
74
  self.include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
74
75
  self.extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
75
76
  self::UploadedFile.include(plugin::FileMethods) if defined?(plugin::FileMethods)
@@ -78,7 +79,7 @@ class Shrine
78
79
  self::Attachment.extend(plugin::AttachmentClassMethods) if defined?(plugin::AttachmentClassMethods)
79
80
  self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods)
80
81
  self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods)
81
- plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
82
+ Plugins.configure(plugin, self, *args, **kwargs, &block)
82
83
  plugin
83
84
  end
84
85
 
@@ -94,8 +95,8 @@ class Shrine
94
95
  # class Photo
95
96
  # include Shrine::Attachment(:image) # creates a Shrine::Attachment object
96
97
  # end
97
- def Attachment(name, *args)
98
- self::Attachment.new(name, *args)
98
+ def Attachment(name, **args)
99
+ self::Attachment.new(name, **args)
99
100
  end
100
101
  alias attachment Attachment
101
102
  alias [] Attachment
@@ -296,7 +297,7 @@ class Shrine
296
297
  # Retrieves the location for the given IO and context. First it looks
297
298
  # for the `:location` option, otherwise it calls #generate_location.
298
299
  def get_location(io, location: nil, **options)
299
- location ||= generate_location(io, options)
300
+ location ||= generate_location(io, **options)
300
301
  location or fail Error, "location generated for #{io.inspect} was nil"
301
302
  end
302
303
 
@@ -39,10 +39,11 @@ class Shrine
39
39
 
40
40
  # Initializes the attached file, temporary and permanent storage.
41
41
  def initialize(file: nil, cache: :cache, store: :store)
42
- @file = file
43
- @cache = cache
44
- @store = store
45
- @context = {}
42
+ @file = file
43
+ @cache = cache
44
+ @store = store
45
+ @context = {}
46
+ @previous = nil
46
47
  end
47
48
 
48
49
  # Returns the temporary storage identifier.
@@ -69,6 +70,10 @@ class Shrine
69
70
  def assign(value, **options)
70
71
  return if value == "" # skip empty hidden field
71
72
 
73
+ if value.is_a?(Hash) || value.is_a?(String)
74
+ return if uploaded_file(value) == file # skip assignment for current file
75
+ end
76
+
72
77
  attach_cached(value, **options)
73
78
  end
74
79
 
@@ -89,7 +94,7 @@ class Shrine
89
94
  # attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
90
95
  def attach_cached(value, **options)
91
96
  if value.is_a?(String) || value.is_a?(Hash)
92
- change(cached(value, **options), **options)
97
+ change(cached(value, **options))
93
98
  else
94
99
  attach(value, storage: cache_key, action: :cache, **options)
95
100
  end
@@ -111,7 +116,7 @@ class Shrine
111
116
  def attach(io, storage: store_key, **options)
112
117
  file = upload(io, storage, **options) if io
113
118
 
114
- change(file, **options)
119
+ change(file)
115
120
  end
116
121
 
117
122
  # Deletes any previous file and promotes newly attached cached file.
@@ -138,7 +143,7 @@ class Shrine
138
143
  def finalize
139
144
  destroy_previous
140
145
  promote_cached
141
- remove_instance_variable(:@previous) if changed?
146
+ @previous = nil
142
147
  end
143
148
 
144
149
  # Plugins can override this if they want something to be done in a
@@ -211,8 +216,8 @@ class Shrine
211
216
  # attacher.change(uploaded_file)
212
217
  # attacher.file #=> #<Shrine::UploadedFile>
213
218
  # attacher.changed? #=> true
214
- def change(file, **)
215
- @previous = dup unless @file == file
219
+ def change(file)
220
+ @previous = dup if change?(file)
216
221
  set(file)
217
222
  end
218
223
 
@@ -254,7 +259,7 @@ class Shrine
254
259
  # attacher.attach(file)
255
260
  # attacher.changed? #=> true
256
261
  def changed?
257
- instance_variable_defined?(:@previous)
262
+ !!@previous
258
263
  end
259
264
 
260
265
  # Returns whether a file is attached.
@@ -368,6 +373,11 @@ class Shrine
368
373
  attached? && !cached?
369
374
  end
370
375
 
376
+ # Whether assigning the given file is considered a change.
377
+ def change?(file)
378
+ @file != file
379
+ end
380
+
371
381
  # Returns whether the file is uploaded to specified storage.
372
382
  def uploaded?(file, storage_key)
373
383
  file&.storage_key == storage_key