shrine 3.1.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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