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
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ module FormAssign
6
+ def self.load_dependencies(uploader)
7
+ uploader.plugin :entity
8
+ end
9
+
10
+ def self.configure(uploader, **opts)
11
+ uploader.opts[:form_assign] ||= { result: :params }
12
+ uploader.opts[:form_assign].merge!(opts)
13
+ end
14
+
15
+ module AttacherMethods
16
+ # Helper for setting the attachment from form fields. Returns normalized
17
+ # fields.
18
+ #
19
+ # attacher = Shrine::Attacher.from_entity(photo, :image)
20
+ #
21
+ # attacher.form_assign({ image: file, title: "Title" })
22
+ # #=> { image: '{...}', title: "Title" }
23
+ #
24
+ # attacher.form_assign({ image: "", image_remote_url: "...", title: "Title" })
25
+ # #=> { image: '{...}', title: "Title" }
26
+ #
27
+ # attacher.form_assign({ image: "", title: "Title" })
28
+ # #=> { title: "Title" }
29
+ #
30
+ # You can also return the result in form of attributes to be used for
31
+ # database record creation.
32
+ #
33
+ # attacher.form_assign({ image: file, title: "Title" }, result: :attributes)
34
+ # #=> { image_data: '{...}', title: "Title" }
35
+ def form_assign(fields, result: shrine_class.opts[:form_assign][:result])
36
+ form = create_form_object
37
+ fields = form_write(form, fields)
38
+
39
+ form_attach(form)
40
+
41
+ form_result(fields, result)
42
+ end
43
+
44
+ private
45
+
46
+ # Assigns form params to the form object using Shrine's attachment
47
+ # writers.
48
+ def form_write(form, fields)
49
+ result = fields.dup
50
+
51
+ fields.each do |key, value|
52
+ if form.respond_to?(:"#{key}=")
53
+ form.send(:"#{key}=", value)
54
+
55
+ result.delete(key)
56
+ end
57
+ end
58
+
59
+ result
60
+ end
61
+
62
+ # Attaches the file from the form object if atachment has changed.
63
+ def form_attach(form)
64
+ return unless form.send(:"#{name}_attacher").changed?
65
+
66
+ file = form.send(:"#{name}_attacher").file
67
+
68
+ if file
69
+ change uploaded_file(file.data) # use our UploadedFile class
70
+ else
71
+ change nil
72
+ end
73
+ end
74
+
75
+ # Adds attached file data to the fields if attachment has changed.
76
+ def form_result(fields, result_type)
77
+ return fields unless changed?
78
+
79
+ case result_type
80
+ when :params then fields[name] = file&.to_json
81
+ when :attributes then fields[:"#{name}_data"] = column_data
82
+ else
83
+ fail ArgumentError, "unrecognized result type: #{result_type.inspect}"
84
+ end
85
+
86
+ fields
87
+ end
88
+
89
+ # Creates a disposable form object with model plugin loaded.
90
+ def create_form_object
91
+ # load the model plugin into a disposable Shrine subclass
92
+ shrine_subclass = Class.new(shrine_class)
93
+ shrine_subclass.plugin :model
94
+
95
+ # create a model class with attachment methods
96
+ form_class = Struct.new(:"#{name}_data")
97
+ form_class.include shrine_subclass::Attachment(name)
98
+
99
+ # instantiate form object
100
+ form_class.new(column_data)
101
+ end
102
+ end
103
+ end
104
+
105
+ register_plugin(:form_assign, FormAssign)
106
+ end
107
+ end
@@ -7,13 +7,14 @@ class Shrine
7
7
  # [doc/plugins/included.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/included.md
8
8
  module Included
9
9
  def self.configure(uploader, &block)
10
- uploader.opts[:included_block] = block
10
+ uploader.opts[:included] ||= {}
11
+ uploader.opts[:included][:block] = block
11
12
  end
12
13
 
13
14
  module AttachmentMethods
14
- def included(model)
15
+ def included(klass)
15
16
  super
16
- model.instance_exec(@name, &shrine_class.opts[:included_block])
17
+ klass.instance_exec(@name, &shrine_class.opts[:included][:block])
17
18
  end
18
19
  end
19
20
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
4
+
3
5
  class Shrine
4
6
  module Plugins
5
7
  # Documentation lives in [doc/plugins/infer_extension.md] on GitHub.
@@ -13,14 +15,12 @@ class Shrine
13
15
  }.inspect}"
14
16
  end
15
17
 
16
- def self.configure(uploader, opts = {})
17
- uploader.opts[:infer_extension] ||= { inferrer: :mime_types, force: false, log_subscriber: LOG_SUBSCRIBER }
18
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
19
+ uploader.opts[:infer_extension] ||= { inferrer: :mini_mime }
18
20
  uploader.opts[:infer_extension].merge!(opts)
19
21
 
20
22
  # instrumentation plugin integration
21
- if uploader.respond_to?(:subscribe)
22
- uploader.subscribe(:extension, &uploader.opts[:infer_extension][:log_subscriber])
23
- end
23
+ uploader.subscribe(:extension, &log_subscriber) if uploader.respond_to?(:subscribe)
24
24
  end
25
25
 
26
26
  module ClassMethods
@@ -54,21 +54,14 @@ class Shrine
54
54
 
55
55
  module InstanceMethods
56
56
  def basic_location(io, metadata:)
57
- location = super
58
- current_extension = File.extname(location)
57
+ location = Pathname(super)
59
58
 
60
- if current_extension.empty? || opts[:infer_extension][:force]
61
- inferred_extension = infer_extension(metadata["mime_type"])
62
- location = location.chomp(current_extension) << inferred_extension unless inferred_extension.empty?
59
+ if location.extname.empty? || opts[:infer_extension][:force]
60
+ inferred_extension = self.class.infer_extension(metadata["mime_type"])
61
+ location = location.sub_ext(inferred_extension) if inferred_extension
63
62
  end
64
63
 
65
- location
66
- end
67
-
68
- private
69
-
70
- def infer_extension(mime_type)
71
- self.class.infer_extension(mime_type).to_s
64
+ location.to_s
72
65
  end
73
66
  end
74
67
 
@@ -6,21 +6,18 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/instrumentation.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/instrumentation.md
8
8
  module Instrumentation
9
- EVENTS = %i[upload download exists delete metadata].freeze
9
+ EVENTS = %i[upload download open exists delete metadata].freeze
10
10
 
11
11
  # We use a proc in order to be able identify listeners.
12
12
  LOG_SUBSCRIBER = -> (event) { LogSubscriber.call(event) }
13
13
 
14
- def self.configure(uploader, opts = {})
15
- uploader.opts[:instrumentation] ||= { log_subscriber: LOG_SUBSCRIBER, log_events: EVENTS }
14
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
15
+ uploader.opts[:instrumentation] ||= { log_events: EVENTS, subscribers: {} }
16
16
  uploader.opts[:instrumentation].merge!(opts)
17
17
  uploader.opts[:instrumentation][:notifications] ||= ::ActiveSupport::Notifications
18
18
 
19
- # we assign it to the top-level so that it's duplicated on subclassing
20
- uploader.opts[:instrumentation_subscribers] ||= Hash.new { |h, k| h[k] = [] }
21
-
22
19
  uploader.opts[:instrumentation][:log_events].each do |event_name|
23
- uploader.subscribe(event_name, &uploader.opts[:instrumentation][:log_subscriber])
20
+ uploader.subscribe(event_name, &log_subscriber)
24
21
  end
25
22
  end
26
23
 
@@ -48,12 +45,13 @@ class Shrine
48
45
  # end
49
46
  def subscribe(event_name, &subscriber)
50
47
  return if subscriber.nil?
51
- return if subscribers[event_name].include?(subscriber)
48
+ return if subscribers[event_name]&.include?(subscriber)
52
49
 
53
50
  notifications.subscribe("#{event_name}.shrine") do |event|
54
51
  subscriber.call(event) if event[:uploader] <= self
55
52
  end
56
53
 
54
+ subscribers[event_name] ||= []
57
55
  subscribers[event_name] << subscriber
58
56
  end
59
57
 
@@ -63,12 +61,8 @@ class Shrine
63
61
  Notifications.new(opts[:instrumentation][:notifications])
64
62
  end
65
63
 
66
- def log_subscriber
67
- opts[:instrumentation][:log_subscriber]
68
- end
69
-
70
64
  def subscribers
71
- opts[:instrumentation_subscribers]
65
+ opts[:instrumentation][:subscribers]
72
66
  end
73
67
  end
74
68
 
@@ -76,46 +70,49 @@ class Shrine
76
70
  private
77
71
 
78
72
  # Sends a `upload.shrine` event.
79
- def copy(io, context)
73
+ def _upload(io, location:, metadata:, upload_options: {}, **options)
80
74
  self.class.instrument(
81
75
  :upload,
82
76
  storage: storage_key,
83
- location: context[:location],
77
+ location: location,
84
78
  io: io,
85
- upload_options: context[:upload_options] || {},
86
- options: context,
79
+ upload_options: upload_options,
80
+ metadata: metadata,
81
+ options: options,
87
82
  ) { super }
88
83
  end
89
84
 
90
85
  # Sends a `metadata.shrine` event.
91
- def get_metadata(io, context)
92
- return super if io.is_a?(UploadedFile) && context[:metadata] != true || context[:metadata] == false
86
+ def get_metadata(io, metadata: nil, **options)
87
+ return super if io.is_a?(UploadedFile) && metadata != true || metadata == false
93
88
 
94
89
  self.class.instrument(
95
90
  :metadata,
96
91
  storage: storage_key,
97
92
  io: io,
98
- options: context,
93
+ options: options,
99
94
  ) { super }
100
95
  end
101
96
  end
102
97
 
103
98
  module FileMethods
104
99
  # Sends a `download.shrine` event.
105
- def open(**options)
100
+ def stream(destination, **options)
101
+ return super if opened?
102
+
106
103
  shrine_class.instrument(
107
104
  :download,
108
- storage: storage_key.to_sym,
105
+ storage: storage_key,
109
106
  location: id,
110
107
  download_options: options,
111
- ) { super }
108
+ ) { super(destination, **options, instrument: false) }
112
109
  end
113
110
 
114
111
  # Sends a `exists.shrine` event.
115
112
  def exists?
116
113
  shrine_class.instrument(
117
114
  :exists,
118
- storage: storage_key.to_sym,
115
+ storage: storage_key,
119
116
  location: id,
120
117
  ) { super }
121
118
  end
@@ -124,10 +121,24 @@ class Shrine
124
121
  def delete
125
122
  shrine_class.instrument(
126
123
  :delete,
127
- storage: storage_key.to_sym,
124
+ storage: storage_key,
128
125
  location: id,
129
126
  ) { super }
130
127
  end
128
+
129
+ private
130
+
131
+ # Sends an `open.shrine` event.
132
+ def _open(instrument: true, **options)
133
+ return super(**options) unless instrument
134
+
135
+ shrine_class.instrument(
136
+ :open,
137
+ storage: storage_key,
138
+ location: id,
139
+ download_options: options,
140
+ ) { super(**options) }
141
+ end
131
142
  end
132
143
 
133
144
  # Abstracts away different types of notifications objects
@@ -255,6 +266,15 @@ class Shrine
255
266
  )}"
256
267
  end
257
268
 
269
+ def on_open(event)
270
+ log "Open (#{event.duration}ms) – #{format(
271
+ storage: event[:storage],
272
+ location: event[:location],
273
+ download_options: event[:download_options],
274
+ uploader: event[:uploader],
275
+ )}"
276
+ end
277
+
258
278
  def on_exists(event)
259
279
  log "Exists (#{event.duration}ms) – #{format(
260
280
  storage: event[:storage],
@@ -6,19 +6,9 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/keep_files.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/keep_files.md
8
8
  module KeepFiles
9
- def self.configure(uploader, opts = {})
10
- keep_files = (uploader.opts[:keep_files] ||= [])
11
- opts[:destroyed] ? keep_files << :destroyed : keep_files.delete(:destroyed) if opts.key?(:destroyed)
12
- opts[:replaced] ? keep_files << :replaced : keep_files.delete(:replaced) if opts.key?(:replaced)
13
- end
14
-
15
9
  module AttacherMethods
16
- def replace
17
- super unless shrine_class.opts[:keep_files].include?(:replaced)
18
- end
19
-
20
- def destroy
21
- super unless shrine_class.opts[:keep_files].include?(:destroyed)
10
+ def destroy_attached(*)
11
+ # don't delete files
22
12
  end
23
13
  end
24
14
  end
@@ -6,33 +6,34 @@ class Shrine
6
6
  #
7
7
  # [doc/plugins/metadata_attributes.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/metadata_attributes.md
8
8
  module MetadataAttributes
9
+ def self.load_dependencies(uploader, *)
10
+ uploader.plugin :entity
11
+ end
12
+
9
13
  def self.configure(uploader, mappings = {})
10
- uploader.opts[:metadata_attributes_mappings] ||= {}
11
- uploader.opts[:metadata_attributes_mappings].merge!(mappings)
14
+ uploader.opts[:metadata_attributes] ||= { mappings: {} }
15
+ uploader.opts[:metadata_attributes][:mappings].merge!(mappings)
12
16
  end
13
17
 
14
18
  module AttacherClassMethods
15
19
  def metadata_attributes(mappings)
16
- shrine_class.opts[:metadata_attributes_mappings].merge!(mappings)
20
+ shrine_class.opts[:metadata_attributes][:mappings].merge!(mappings)
17
21
  end
18
22
  end
19
23
 
20
24
  module AttacherMethods
21
- def assign(value, **options)
22
- super
23
- cached_file = get
25
+ def column_values
26
+ values = super
24
27
 
25
- shrine_class.opts[:metadata_attributes_mappings].each do |source, destination|
26
- attribute_name = destination.is_a?(Symbol) ? :"#{name}_#{destination}" : :"#{destination}"
28
+ shrine_class.opts[:metadata_attributes][:mappings].each do |source, destination|
29
+ metadata_attribute = destination.is_a?(Symbol) ? :"#{name}_#{destination}" : :"#{destination}"
27
30
 
28
- next unless record.respond_to?(:"#{attribute_name}=")
31
+ next unless record.respond_to?(metadata_attribute)
29
32
 
30
- if cached_file
31
- record.send(:"#{attribute_name}=", cached_file.metadata[source.to_s])
32
- else
33
- record.send(:"#{attribute_name}=", nil)
34
- end
33
+ values[metadata_attribute] = file && file.metadata[source.to_s]
35
34
  end
35
+
36
+ values
36
37
  end
37
38
  end
38
39
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation lives in [doc/plugins/model.md] on GitHub.
6
+ #
7
+ # [doc/plugins/model.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/model.md
8
+ module Model
9
+ def self.load_dependencies(uploader, **)
10
+ uploader.plugin :entity
11
+ end
12
+
13
+ def self.configure(uploader, **opts)
14
+ uploader.opts[:model] ||= { cache: true }
15
+ uploader.opts[:model].merge!(opts)
16
+ end
17
+
18
+ module AttachmentMethods
19
+ # Allows specifying whether the attachment should be for a model
20
+ # (default) or an entity.
21
+ #
22
+ # Shrine::Attachment.new(:image) # model (default)
23
+ # Shrine::Attachment.new(:image, type: :model) # model
24
+ # Shrine::Attachment.new(:image, type: :entity) # entity
25
+ def initialize(name, **options)
26
+ super(name, type: :model, **options)
27
+ end
28
+
29
+ # We define the setter dynamically on inclusion to allow other plugins
30
+ # to still have time to override attachment type on inclusion.
31
+ def included(klass)
32
+ super
33
+
34
+ return unless options[:type] == :model
35
+
36
+ name = attachment_name
37
+
38
+ define_method :"#{name}=" do |value|
39
+ send(:"#{name}_attacher").model_assign(value)
40
+ end
41
+
42
+ define_method :initialize_copy do |other|
43
+ super(other)
44
+ instance_variable_set(:"@#{name}_attacher", instance_variable_get(:"@#{name}_attacher")&.dup)
45
+ self
46
+ end
47
+ private :initialize_copy
48
+ end
49
+
50
+ # Memoizes the attacher instance into an instance variable.
51
+ def attacher(record, options)
52
+ return super unless @options[:type] == :model
53
+
54
+ name = attachment_name
55
+
56
+ if !record.instance_variable_get(:"@#{name}_attacher") || options.any?
57
+ record.instance_variable_set(:"@#{name}_attacher", super)
58
+ else
59
+ record.instance_variable_get(:"@#{name}_attacher")
60
+ end
61
+ end
62
+ end
63
+
64
+ module AttacherClassMethods
65
+ # Initializes itself from a model instance and attachment name.
66
+ #
67
+ # photo.image_data #=> "{...}" # a file is attached
68
+ #
69
+ # attacher = Attacher.from_model(photo, :image)
70
+ # attacher.file #=> #<Shrine::UploadedFile>
71
+ def from_model(record, name, type: :model, **options)
72
+ attacher = new(**options)
73
+ attacher.load_model(record, name, type: type)
74
+ attacher
75
+ end
76
+ end
77
+
78
+ module AttacherMethods
79
+ def initialize(model_cache: shrine_class.opts[:model][:cache], **options)
80
+ super(**options)
81
+ @model_cache = model_cache
82
+ end
83
+
84
+ # Saves record and name and initializes attachment from the model
85
+ # attribute. Called from `Attacher.from_model`.
86
+ def load_model(record, name, type: :model)
87
+ load_entity(record, name, type: type)
88
+ end
89
+
90
+ # Called by the attachment attribute setter on the model.
91
+ def model_assign(value, **options)
92
+ if model_cache?
93
+ assign(value, **options)
94
+ else
95
+ attach(value, **options)
96
+ end
97
+ end
98
+
99
+ # Writes uploaded file data into the model.
100
+ def set(*args)
101
+ result = super
102
+ write if model?
103
+ result
104
+ end
105
+
106
+ # Writes the attachment data into the model attribute.
107
+ def write
108
+ column_values.each do |name, value|
109
+ write_attribute(name, value)
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # Writes given value into the model attribute.
116
+ def write_attribute(name = attribute, value)
117
+ record.public_send(:"#{name}=", value)
118
+ end
119
+
120
+ # Returns whether assigned files should be uploaded to/loaded from
121
+ # temporary storage.
122
+ def model_cache?
123
+ @model_cache
124
+ end
125
+
126
+ # Returns whether the attacher is being backed by a model instance.
127
+ # This allows users to still use the attacher with an entity instance
128
+ # or without any record instance.
129
+ def model?
130
+ type == :model
131
+ end
132
+ end
133
+ end
134
+
135
+ register_plugin(:model, Model)
136
+ end
137
+ end