shrine 2.19.4 → 3.0.0.alpha

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -11
  3. data/README.md +9 -3
  4. data/doc/advantages.md +1 -1
  5. data/doc/carrierwave.md +4 -4
  6. data/doc/creating_persistence_plugins.md +172 -0
  7. data/doc/creating_plugins.md +1 -1
  8. data/doc/creating_storages.md +3 -1
  9. data/doc/design.md +2 -2
  10. data/doc/direct_s3.md +0 -22
  11. data/doc/paperclip.md +3 -3
  12. data/doc/plugins/activerecord.md +211 -42
  13. data/doc/plugins/atomic_helpers.md +153 -0
  14. data/doc/plugins/column.md +90 -0
  15. data/doc/plugins/derivation_endpoint.md +54 -62
  16. data/doc/plugins/derivatives.md +752 -0
  17. data/doc/plugins/entity.md +204 -0
  18. data/doc/plugins/infer_extension.md +8 -8
  19. data/doc/plugins/instrumentation.md +33 -13
  20. data/doc/plugins/keep_files.md +5 -15
  21. data/doc/plugins/model.md +157 -0
  22. data/doc/plugins/presign_endpoint.md +2 -1
  23. data/doc/plugins/refresh_metadata.md +44 -7
  24. data/doc/plugins/sequel.md +190 -33
  25. data/doc/plugins/{default_url_options.md → url_options.md} +5 -5
  26. data/doc/processing.md +1 -1
  27. data/doc/release_notes/1.1.0.md +2 -2
  28. data/doc/release_notes/2.15.0.md +1 -1
  29. data/doc/storage/s3.md +2 -2
  30. data/doc/testing.md +1 -1
  31. data/lib/shrine.rb +72 -138
  32. data/lib/shrine/attacher.rb +272 -176
  33. data/lib/shrine/attachment.rb +2 -42
  34. data/lib/shrine/plugins/activerecord.rb +103 -26
  35. data/lib/shrine/plugins/add_metadata.rb +9 -10
  36. data/lib/shrine/plugins/atomic_helpers.rb +111 -0
  37. data/lib/shrine/plugins/attacher_options.rb +55 -0
  38. data/lib/shrine/plugins/backgrounding.rb +147 -115
  39. data/lib/shrine/plugins/cached_attachment_data.rb +6 -9
  40. data/lib/shrine/plugins/column.rb +104 -0
  41. data/lib/shrine/plugins/data_uri.rb +35 -38
  42. data/lib/shrine/plugins/default_storage.rb +18 -12
  43. data/lib/shrine/plugins/default_url.rb +11 -21
  44. data/lib/shrine/plugins/default_url_options.rb +3 -30
  45. data/lib/shrine/plugins/delete_raw.rb +9 -13
  46. data/lib/shrine/plugins/derivation_endpoint.rb +75 -114
  47. data/lib/shrine/plugins/derivatives.rb +576 -0
  48. data/lib/shrine/plugins/determine_mime_type.rb +3 -15
  49. data/lib/shrine/plugins/download_endpoint.rb +83 -131
  50. data/lib/shrine/plugins/dynamic_storage.rb +4 -8
  51. data/lib/shrine/plugins/entity.rb +128 -0
  52. data/lib/shrine/plugins/form_assign.rb +107 -0
  53. data/lib/shrine/plugins/included.rb +4 -3
  54. data/lib/shrine/plugins/infer_extension.rb +10 -17
  55. data/lib/shrine/plugins/instrumentation.rb +45 -25
  56. data/lib/shrine/plugins/keep_files.rb +2 -12
  57. data/lib/shrine/plugins/metadata_attributes.rb +15 -14
  58. data/lib/shrine/plugins/model.rb +137 -0
  59. data/lib/shrine/plugins/module_include.rb +2 -0
  60. data/lib/shrine/plugins/presign_endpoint.rb +1 -15
  61. data/lib/shrine/plugins/pretty_location.rb +5 -5
  62. data/lib/shrine/plugins/processing.rb +21 -6
  63. data/lib/shrine/plugins/rack_file.rb +1 -39
  64. data/lib/shrine/plugins/rack_response.rb +14 -7
  65. data/lib/shrine/plugins/recache.rb +5 -2
  66. data/lib/shrine/plugins/refresh_metadata.rb +12 -8
  67. data/lib/shrine/plugins/remote_url.rb +44 -53
  68. data/lib/shrine/plugins/remove_attachment.rb +7 -2
  69. data/lib/shrine/plugins/remove_invalid.rb +8 -4
  70. data/lib/shrine/plugins/restore_cached_data.rb +12 -4
  71. data/lib/shrine/plugins/sequel.rb +115 -27
  72. data/lib/shrine/plugins/signature.rb +2 -7
  73. data/lib/shrine/plugins/store_dimensions.rb +13 -27
  74. data/lib/shrine/plugins/upload_endpoint.rb +14 -15
  75. data/lib/shrine/plugins/upload_options.rb +9 -8
  76. data/lib/shrine/plugins/url_options.rb +33 -0
  77. data/lib/shrine/plugins/validation.rb +87 -0
  78. data/lib/shrine/plugins/validation_helpers.rb +33 -54
  79. data/lib/shrine/plugins/versions.rb +106 -84
  80. data/lib/shrine/storage/file_system.rb +32 -57
  81. data/lib/shrine/storage/linter.rb +9 -1
  82. data/lib/shrine/storage/memory.rb +42 -0
  83. data/lib/shrine/storage/s3.rb +38 -146
  84. data/lib/shrine/uploaded_file.rb +22 -29
  85. data/lib/shrine/version.rb +4 -4
  86. data/shrine.gemspec +2 -3
  87. metadata +27 -54
  88. data/doc/plugins/backup.md +0 -31
  89. data/doc/plugins/copy.md +0 -24
  90. data/doc/plugins/delete_promoted.md +0 -12
  91. data/doc/plugins/direct_upload.md +0 -172
  92. data/doc/plugins/hooks.md +0 -58
  93. data/doc/plugins/logging.md +0 -42
  94. data/doc/plugins/migration_helpers.md +0 -60
  95. data/doc/plugins/moving.md +0 -19
  96. data/doc/plugins/multi_delete.md +0 -20
  97. data/doc/plugins/parallelize.md +0 -16
  98. data/doc/plugins/parsed_json.md +0 -23
  99. data/lib/shrine/plugins/background_helpers.rb +0 -5
  100. data/lib/shrine/plugins/backup.rb +0 -90
  101. data/lib/shrine/plugins/copy.rb +0 -50
  102. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  103. data/lib/shrine/plugins/direct_upload.rb +0 -217
  104. data/lib/shrine/plugins/hooks.rb +0 -90
  105. data/lib/shrine/plugins/logging.rb +0 -142
  106. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  107. data/lib/shrine/plugins/moving.rb +0 -57
  108. data/lib/shrine/plugins/multi_delete.rb +0 -32
  109. data/lib/shrine/plugins/parallelize.rb +0 -78
  110. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -27,40 +27,6 @@ class Shrine
27
27
  def initialize(name, **options)
28
28
  @name = name.to_sym
29
29
  @options = options
30
-
31
- define_attachment_methods!
32
- end
33
-
34
- # Defines attachment methods for the specified attachment name. These
35
- # methods will be added to any model that includes this module.
36
- def define_attachment_methods!
37
- attachment = self
38
- name = attachment_name
39
-
40
- define_method :"#{name}_attacher" do |**options|
41
- if !instance_variable_get(:"@#{name}_attacher") || options.any?
42
- instance_variable_set(:"@#{name}_attacher", attachment.build_attacher(self, options))
43
- else
44
- instance_variable_get(:"@#{name}_attacher")
45
- end
46
- end
47
-
48
- define_method :"#{name}=" do |value|
49
- send(:"#{name}_attacher").assign(value)
50
- end
51
-
52
- define_method :"#{name}" do
53
- send(:"#{name}_attacher").get
54
- end
55
-
56
- define_method :"#{name}_url" do |*args|
57
- send(:"#{name}_attacher").url(*args)
58
- end
59
- end
60
-
61
- # Creates an instance of the corresponding Attacher subclass.
62
- def build_attacher(object, options)
63
- shrine_class::Attacher.new(object, @name, @options.merge(options))
64
30
  end
65
31
 
66
32
  # Returns name of the attachment this module provides.
@@ -75,17 +41,11 @@ class Shrine
75
41
 
76
42
  # Returns class name with attachment name included.
77
43
  #
78
- # Shrine[:image].to_s #=> "#<Shrine::Attachment(image)>"
79
- def to_s
80
- "#<#{self.class.inspect}(#{attachment_name})>"
81
- end
82
-
83
- # Returns class name with attachment name included.
84
- #
85
- # Shrine[:image].inspect #=> "#<Shrine::Attachment(image)>"
44
+ # Shrine::Attachment.new(:image).to_s #=> "#<Shrine::Attachment(image)>"
86
45
  def inspect
87
46
  "#<#{self.class.inspect}(#{attachment_name})>"
88
47
  end
48
+ alias to_s inspect
89
49
 
90
50
  # Returns the Shrine class that this attachment's class is namespaced
91
51
  # under.
@@ -8,9 +8,14 @@ class Shrine
8
8
  #
9
9
  # [doc/plugins/activerecord.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/activerecord.md
10
10
  module Activerecord
11
- def self.configure(uploader, opts = {})
12
- uploader.opts[:activerecord_callbacks] = opts.fetch(:callbacks, uploader.opts.fetch(:activerecord_callbacks, true))
13
- uploader.opts[:activerecord_validations] = opts.fetch(:validations, uploader.opts.fetch(:activerecord_validations, true))
11
+ def self.load_dependencies(uploader, **)
12
+ uploader.plugin :model
13
+ uploader.plugin :atomic_helpers
14
+ end
15
+
16
+ def self.configure(uploader, **opts)
17
+ uploader.opts[:activerecord] ||= { callbacks: true, validations: true }
18
+ uploader.opts[:activerecord].merge!(opts)
14
19
  end
15
20
 
16
21
  module AttachmentMethods
@@ -21,60 +26,132 @@ class Shrine
21
26
 
22
27
  name = attachment_name
23
28
 
24
- if shrine_class.opts[:activerecord_validations]
29
+ if shrine_class.opts[:activerecord][:validations]
25
30
  model.validate do
26
- send("#{name}_attacher").errors.each do |message|
27
- errors.add(name, *message)
31
+ # validation plugin integration
32
+ if send(:"#{name}_attacher").respond_to?(:errors)
33
+ send(:"#{name}_attacher").errors.each do |message|
34
+ errors.add(name, *message)
35
+ end
28
36
  end
29
37
  end
30
38
  end
31
39
 
32
- if shrine_class.opts[:activerecord_callbacks]
40
+ if shrine_class.opts[:activerecord][:callbacks]
33
41
  model.before_save do
34
- attacher = send("#{name}_attacher")
35
- attacher.save if attacher.changed?
42
+ if send(:"#{name}_attacher").changed?
43
+ send(:"#{name}_attacher").save
44
+ end
36
45
  end
37
46
 
38
47
  [:create, :update].each do |action|
39
48
  model.after_commit on: action do
40
- attacher = send("#{name}_attacher")
41
- attacher.finalize if attacher.changed?
49
+ if send(:"#{name}_attacher").changed?
50
+ send(:"#{name}_attacher").finalize
51
+ send(:"#{name}_attacher").activerecord_persist
52
+ end
42
53
  end
43
54
  end
44
55
 
45
56
  model.after_commit on: :destroy do
46
- send("#{name}_attacher").destroy
57
+ send(:"#{name}_attacher").destroy_attached
47
58
  end
48
59
  end
49
- end
50
- end
51
60
 
52
- module AttacherClassMethods
53
- # Needed by the `backgrounding` plugin.
54
- def find_record(record_class, record_id)
55
- record_class.where(id: record_id).first
61
+ # reload the attacher on record reload
62
+ define_method :reload do |*args|
63
+ result = super(*args)
64
+ instance_variable_set(:"@#{name}_attacher", nil)
65
+ result
66
+ end
56
67
  end
57
68
  end
58
69
 
59
70
  module AttacherMethods
71
+ # Promotes cached file to permanent storage in an atomic way. It's
72
+ # intended to be called from a background job.
73
+ #
74
+ # attacher.assign(file)
75
+ # attacher.cached? #=> true
76
+ #
77
+ # # ... in background job ...
78
+ #
79
+ # attacher.atomic_promote
80
+ # attacher.stored? #=> true
81
+ #
82
+ # It accepts `:reload` and `:persist` strategies:
83
+ #
84
+ # attacher.atomic_promote(reload: :lock) # uses database locking (default)
85
+ # attacher.atomic_promote(reload: :fetch) # reloads with no locking
86
+ # attacher.atomic_promote(reload: ->(&b){}) # custom reloader
87
+ # attacher.atomic_promote(reload: false) # skips reloading
88
+ #
89
+ # attacher.atomic_promote(persist: :save) # persists stored file (default)
90
+ # attacher.atomic_promote(persist: ->{}) # custom persister
91
+ # attacher.atomic_promote(persist: false) # skips persistence
92
+ def activerecord_atomic_promote(**options, &block)
93
+ abstract_atomic_promote(activerecord_strategies(**options), &block)
94
+ end
95
+ alias atomic_promote activerecord_atomic_promote
96
+
97
+ # Persist the the record only if the attachment hasn't changed.
98
+ # Optionally yields reloaded attacher to the block before persisting.
99
+ # It's intended to be called from a background job.
100
+ #
101
+ # # ... in background job ...
102
+ #
103
+ # attacher.file.metadata["foo"] = "bar"
104
+ # attacher.write
105
+ #
106
+ # attacher.atomic_persist
107
+ def activerecord_atomic_persist(*args, **options, &block)
108
+ abstract_atomic_persist(*args, activerecord_strategies(**options), &block)
109
+ end
110
+ alias atomic_persist activerecord_atomic_persist
111
+
112
+ # Called in the `after_commit` callback after finalization.
113
+ def activerecord_persist
114
+ activerecord_save
115
+ end
116
+ alias persist activerecord_persist
117
+
60
118
  private
61
119
 
62
- # Saves the record after assignment, skipping validations.
63
- def update(uploaded_file)
64
- super
120
+ # Resolves strategies for atomic promotion and persistence.
121
+ def activerecord_strategies(reload: :lock, persist: :save, **options)
122
+ reload = method(:"activerecord_#{reload}") if reload.is_a?(Symbol)
123
+ persist = method(:"activerecord_#{persist}") if persist.is_a?(Symbol)
124
+
125
+ { reload: reload, persist: persist, **options }
126
+ end
127
+
128
+ # Implements the "fetch" reload strategy for #atomic_promote and
129
+ # #atomic_persist.
130
+ def activerecord_fetch
131
+ yield record.clone.reload
132
+ end
133
+
134
+ # Implements the "lock" reload strategy for #atomic_promote and
135
+ # #atomic_persist.
136
+ def activerecord_lock
137
+ record.transaction { yield record.clone.reload(lock: true) }
138
+ end
139
+
140
+ # Implements the "save" persist strategy for #atomic_promote and
141
+ # #atomic_persist.
142
+ def activerecord_save
65
143
  record.save(validate: false)
66
144
  end
67
145
 
68
- # If the data attribute represents a JSON column, it needs to receive a
69
- # Hash.
70
- def convert_before_write(value)
71
- activerecord_json_column? ? value : super
146
+ # ActiveRecord JSON column attribute needs to be assigned with a Hash.
147
+ def serialize_column(data)
148
+ activerecord_json_column? ? data : super
72
149
  end
73
150
 
74
151
  # Returns true if the data attribute represents a JSON or JSONB column.
75
152
  def activerecord_json_column?
76
153
  return false unless record.is_a?(ActiveRecord::Base)
77
- return false unless column = record.class.columns_hash[data_attribute.to_s]
154
+ return false unless column = record.class.columns_hash[attribute.to_s]
78
155
 
79
156
  [:json, :jsonb].include?(column.type)
80
157
  end
@@ -7,12 +7,12 @@ class Shrine
7
7
  # [doc/plugins/add_metadata.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/add_metadata.md
8
8
  module AddMetadata
9
9
  def self.configure(uploader)
10
- uploader.opts[:add_metadata_definitions] ||= []
10
+ uploader.opts[:add_metadata] ||= { definitions: [] }
11
11
  end
12
12
 
13
13
  module ClassMethods
14
14
  def add_metadata(name = nil, &block)
15
- opts[:add_metadata_definitions] << [name, block]
15
+ opts[:add_metadata][:definitions] << [name, block]
16
16
 
17
17
  metadata_method(name) if name
18
18
  end
@@ -31,25 +31,24 @@ class Shrine
31
31
  end
32
32
 
33
33
  module InstanceMethods
34
- def extract_metadata(io, context = {})
34
+ def extract_metadata(io, **options)
35
35
  metadata = super
36
- context = context.merge(metadata: metadata)
37
36
 
38
- extract_custom_metadata(io, context)
37
+ extract_custom_metadata(io, **options, metadata: metadata)
39
38
 
40
39
  metadata
41
40
  end
42
41
 
43
42
  private
44
43
 
45
- def extract_custom_metadata(io, context)
46
- opts[:add_metadata_definitions].each do |name, block|
47
- result = instance_exec(io, context, &block)
44
+ def extract_custom_metadata(io, **options)
45
+ opts[:add_metadata][:definitions].each do |name, block|
46
+ result = instance_exec(io, options, &block)
48
47
 
49
48
  if name
50
- context[:metadata].merge! name.to_s => result
49
+ options[:metadata].merge! name.to_s => result
51
50
  else
52
- context[:metadata].merge! result.transform_keys(&:to_s) if result
51
+ options[:metadata].merge! result.transform_keys(&:to_s) if result
53
52
  end
54
53
 
55
54
  # rewind between metadata blocks
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ class AttachmentChanged < Error
5
+ end
6
+
7
+ module Plugins
8
+ # Documentation lives in [doc/plugins/atomic_helpers.md] on GitHub.
9
+ #
10
+ # [doc/plugins/atomic_helpers.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/atomic_helpers.md
11
+ class AtomicHelpers
12
+ module AttacherClassMethods
13
+ # Retrieves the attacher from the given entity/model and verifies that
14
+ # the attachment hasn't changed. It raises `Shrine::AttachmentChanged`
15
+ # exception if the attached file doesn't match.
16
+ #
17
+ # Shrine::Attacher.retrieve(model: photo, name: :image, data: data)
18
+ # #=> #<ImageUploader::Attacher>
19
+ def retrieve(model: nil, entity: nil, name:, data:, **options)
20
+ fail ArgumentError, "either :model or :entity is required" unless model || entity
21
+
22
+ record = model || entity
23
+
24
+ attacher = record.send(:"#{name}_attacher", **options) if record.respond_to?(:"#{name}_attacher")
25
+ attacher ||= from_model(record, name, **options) if model
26
+ attacher ||= from_entity(record, name, **options) if entity
27
+
28
+ if attacher.file != attacher.class.from_data(data).file
29
+ fail Shrine::AttachmentChanged, "attachment has changed"
30
+ end
31
+
32
+ attacher
33
+ end
34
+ end
35
+
36
+ module AttacherMethods
37
+ # Like #promote, but additionally persists the promoted file
38
+ # atomically. You need to specify `:reload` and `:persist` strategies
39
+ # when calling the method:
40
+ #
41
+ # attacher.abstract_atomic_promote(
42
+ # reload: reload_strategy,
43
+ # persist: persist_strategy,
44
+ # )
45
+ #
46
+ # This more convenient to use with ORM plugins, which provide defaults
47
+ # for reloading and persistence.
48
+ def abstract_atomic_promote(reload:, persist:, **options, &block)
49
+ original_file = file
50
+
51
+ result = promote(**options)
52
+
53
+ begin
54
+ abstract_atomic_persist(original_file, reload: reload, persist: persist, &block)
55
+ result
56
+ rescue Shrine::AttachmentChanged
57
+ destroy(background: true)
58
+ raise
59
+ end
60
+ end
61
+
62
+ # Reloads the record to check whether the attachment has changed. If it
63
+ # hasn't, it persists the record. Otherwise it raises
64
+ # `Shrine::AttachmentChanged` exception.
65
+ #
66
+ # attacher.abstract_atomic_persist(
67
+ # reload: reload_strategy,
68
+ # persist: persist_strategy,
69
+ # )
70
+ #
71
+ # This more convenient to use with ORM plugins, which provide defaults
72
+ # for reloading and persistence.
73
+ def abstract_atomic_persist(original_file = file, reload:, persist:)
74
+ abstract_reload(reload) do |attacher|
75
+ if attacher && attacher.file != original_file
76
+ fail Shrine::AttachmentChanged, "attachment has changed"
77
+ end
78
+
79
+ yield attacher if block_given?
80
+
81
+ abstract_persist(persist)
82
+ end
83
+ end
84
+
85
+ protected
86
+
87
+ # Calls the reload strategy and yields a reloaded attacher from the
88
+ # reloaded record.
89
+ def abstract_reload(strategy)
90
+ return yield if strategy == false
91
+
92
+ strategy.call do |record|
93
+ reloaded_attacher = dup
94
+ reloaded_attacher.load_entity(record, name)
95
+
96
+ yield reloaded_attacher
97
+ end
98
+ end
99
+
100
+ # Calls the persist strategy.
101
+ def abstract_persist(strategy)
102
+ return if strategy == false
103
+
104
+ strategy.call
105
+ end
106
+ end
107
+ end
108
+
109
+ register_plugin(:atomic_helpers, AtomicHelpers)
110
+ end
111
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ module AttacherOptions
6
+ module AttacherMethods
7
+ def initialize(**options)
8
+ super
9
+ @options = {}
10
+ end
11
+
12
+ def attach_options(options = nil)
13
+ handle_option(:attach, options)
14
+ end
15
+
16
+ def promote_options(options = nil)
17
+ handle_option(:promote, options)
18
+ end
19
+
20
+ def destroy_options(options = nil)
21
+ handle_option(:destroy, options)
22
+ end
23
+
24
+ def attach_cached(io, **options)
25
+ super(io, **attach_options, **options)
26
+ end
27
+
28
+ def attach(io, **options)
29
+ super(io, **attach_options, **options)
30
+ end
31
+
32
+ def promote_cached(**options)
33
+ super(**promote_options, **options)
34
+ end
35
+
36
+ def destroy_attached(**options)
37
+ super(**destroy_options, **options)
38
+ end
39
+
40
+ private
41
+
42
+ def handle_option(name, options)
43
+ if options
44
+ @options[name] ||= {}
45
+ @options[name].merge!(options)
46
+ else
47
+ @options[name] || {}
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ register_plugin(:attacher_options, AttacherOptions)
54
+ end
55
+ end