shrine 2.19.3 → 3.6.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 (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +83 -979
  5. data/doc/advantages.md +231 -204
  6. data/doc/attacher.md +304 -153
  7. data/doc/carrierwave.md +297 -226
  8. data/doc/changing_derivatives.md +308 -0
  9. data/doc/changing_location.md +103 -21
  10. data/doc/changing_storage.md +110 -0
  11. data/doc/creating_persistence_plugins.md +132 -0
  12. data/doc/creating_plugins.md +43 -23
  13. data/doc/creating_storages.md +19 -5
  14. data/doc/design.md +147 -97
  15. data/doc/direct_s3.md +38 -28
  16. data/doc/external/articles.md +63 -0
  17. data/doc/external/extensions.md +53 -0
  18. data/doc/external/misc.md +32 -0
  19. data/doc/getting_started.md +1156 -0
  20. data/doc/metadata.md +190 -109
  21. data/doc/multiple_files.md +93 -30
  22. data/doc/paperclip.md +384 -262
  23. data/doc/plugins/activerecord.md +177 -46
  24. data/doc/plugins/add_metadata.md +139 -38
  25. data/doc/plugins/atomic_helpers.md +217 -0
  26. data/doc/plugins/backgrounding.md +156 -98
  27. data/doc/plugins/cached_attachment_data.md +7 -5
  28. data/doc/plugins/column.md +121 -0
  29. data/doc/plugins/data_uri.md +23 -22
  30. data/doc/plugins/default_storage.md +36 -10
  31. data/doc/plugins/default_url.md +30 -13
  32. data/doc/plugins/delete_raw.md +4 -2
  33. data/doc/plugins/derivation_endpoint.md +186 -101
  34. data/doc/plugins/derivatives.md +839 -0
  35. data/doc/plugins/determine_mime_type.md +4 -2
  36. data/doc/plugins/download_endpoint.md +64 -8
  37. data/doc/plugins/dynamic_storage.md +5 -3
  38. data/doc/plugins/entity.md +263 -0
  39. data/doc/plugins/form_assign.md +55 -0
  40. data/doc/plugins/included.md +31 -8
  41. data/doc/plugins/infer_extension.md +21 -10
  42. data/doc/plugins/instrumentation.md +38 -16
  43. data/doc/plugins/keep_files.md +16 -17
  44. data/doc/plugins/metadata_attributes.md +42 -13
  45. data/doc/plugins/mirroring.md +118 -0
  46. data/doc/plugins/model.md +210 -0
  47. data/doc/plugins/module_include.md +4 -2
  48. data/doc/plugins/multi_cache.md +24 -0
  49. data/doc/plugins/persistence.md +101 -0
  50. data/doc/plugins/presign_endpoint.md +9 -4
  51. data/doc/plugins/pretty_location.md +16 -3
  52. data/doc/plugins/processing.md +4 -2
  53. data/doc/plugins/rack_file.md +8 -2
  54. data/doc/plugins/rack_response.md +6 -2
  55. data/doc/plugins/recache.md +4 -2
  56. data/doc/plugins/refresh_metadata.md +49 -9
  57. data/doc/plugins/remote_url.md +84 -47
  58. data/doc/plugins/remove_attachment.md +27 -6
  59. data/doc/plugins/remove_invalid.md +21 -6
  60. data/doc/plugins/restore_cached_data.md +11 -3
  61. data/doc/plugins/sequel.md +159 -35
  62. data/doc/plugins/signature.md +16 -5
  63. data/doc/plugins/store_dimensions.md +14 -2
  64. data/doc/plugins/tempfile.md +4 -2
  65. data/doc/plugins/type_predicates.md +96 -0
  66. data/doc/plugins/upload_endpoint.md +13 -13
  67. data/doc/plugins/upload_options.md +6 -4
  68. data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
  69. data/doc/plugins/validation.md +97 -0
  70. data/doc/plugins/validation_helpers.md +16 -13
  71. data/doc/plugins/versions.md +15 -19
  72. data/doc/processing.md +438 -221
  73. data/doc/refile.md +188 -170
  74. data/doc/release_notes/1.0.0.md +4 -0
  75. data/doc/release_notes/1.1.0.md +6 -2
  76. data/doc/release_notes/1.2.0.md +4 -0
  77. data/doc/release_notes/1.3.0.md +4 -0
  78. data/doc/release_notes/1.4.0.md +4 -0
  79. data/doc/release_notes/1.4.1.md +4 -0
  80. data/doc/release_notes/1.4.2.md +4 -0
  81. data/doc/release_notes/2.0.0.md +4 -0
  82. data/doc/release_notes/2.0.1.md +4 -0
  83. data/doc/release_notes/2.1.0.md +5 -1
  84. data/doc/release_notes/2.1.1.md +4 -0
  85. data/doc/release_notes/2.10.0.md +4 -0
  86. data/doc/release_notes/2.10.1.md +4 -0
  87. data/doc/release_notes/2.11.0.md +4 -0
  88. data/doc/release_notes/2.12.0.md +4 -0
  89. data/doc/release_notes/2.13.0.md +4 -0
  90. data/doc/release_notes/2.14.0.md +5 -1
  91. data/doc/release_notes/2.15.0.md +11 -7
  92. data/doc/release_notes/2.16.0.md +4 -0
  93. data/doc/release_notes/2.17.0.md +4 -0
  94. data/doc/release_notes/2.18.0.md +4 -0
  95. data/doc/release_notes/2.19.0.md +6 -3
  96. data/doc/release_notes/2.2.0.md +4 -0
  97. data/doc/release_notes/2.3.0.md +4 -0
  98. data/doc/release_notes/2.3.1.md +4 -0
  99. data/doc/release_notes/2.4.0.md +4 -0
  100. data/doc/release_notes/2.4.1.md +4 -0
  101. data/doc/release_notes/2.5.0.md +4 -0
  102. data/doc/release_notes/2.6.0.md +4 -0
  103. data/doc/release_notes/2.6.1.md +4 -0
  104. data/doc/release_notes/2.7.0.md +4 -0
  105. data/doc/release_notes/2.8.0.md +4 -0
  106. data/doc/release_notes/2.9.0.md +4 -0
  107. data/doc/release_notes/3.0.0.md +981 -0
  108. data/doc/release_notes/3.0.1.md +22 -0
  109. data/doc/release_notes/3.1.0.md +73 -0
  110. data/doc/release_notes/3.2.0.md +96 -0
  111. data/doc/release_notes/3.2.1.md +31 -0
  112. data/doc/release_notes/3.2.2.md +14 -0
  113. data/doc/release_notes/3.3.0.md +105 -0
  114. data/doc/release_notes/3.4.0.md +35 -0
  115. data/doc/release_notes/3.5.0.md +63 -0
  116. data/doc/release_notes/3.6.0.md +23 -0
  117. data/doc/retrieving_uploads.md +5 -2
  118. data/doc/securing_uploads.md +60 -37
  119. data/doc/storage/file_system.md +20 -3
  120. data/doc/storage/memory.md +19 -0
  121. data/doc/storage/s3.md +122 -78
  122. data/doc/testing.md +141 -133
  123. data/doc/upgrading_to_3.md +708 -0
  124. data/doc/validation.md +54 -90
  125. data/lib/shrine/attacher.rb +292 -169
  126. data/lib/shrine/attachment.rb +13 -46
  127. data/lib/shrine/plugins/_persistence.rb +93 -0
  128. data/lib/shrine/plugins/activerecord.rb +77 -34
  129. data/lib/shrine/plugins/add_metadata.rb +25 -17
  130. data/lib/shrine/plugins/atomic_helpers.rb +119 -0
  131. data/lib/shrine/plugins/backgrounding.rb +77 -113
  132. data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
  133. data/lib/shrine/plugins/column.rb +102 -0
  134. data/lib/shrine/plugins/data_uri.rb +38 -36
  135. data/lib/shrine/plugins/default_storage.rb +45 -15
  136. data/lib/shrine/plugins/default_url.rb +12 -24
  137. data/lib/shrine/plugins/default_url_options.rb +3 -30
  138. data/lib/shrine/plugins/delete_raw.rb +10 -16
  139. data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
  140. data/lib/shrine/plugins/derivatives.rb +645 -0
  141. data/lib/shrine/plugins/determine_mime_type.rb +9 -21
  142. data/lib/shrine/plugins/download_endpoint.rb +118 -133
  143. data/lib/shrine/plugins/dynamic_storage.rb +5 -11
  144. data/lib/shrine/plugins/entity.rb +158 -0
  145. data/lib/shrine/plugins/form_assign.rb +108 -0
  146. data/lib/shrine/plugins/included.rb +6 -6
  147. data/lib/shrine/plugins/infer_extension.rb +17 -20
  148. data/lib/shrine/plugins/instrumentation.rb +59 -43
  149. data/lib/shrine/plugins/keep_files.rb +3 -15
  150. data/lib/shrine/plugins/metadata_attributes.rb +28 -19
  151. data/lib/shrine/plugins/mirroring.rb +142 -0
  152. data/lib/shrine/plugins/model.rb +160 -0
  153. data/lib/shrine/plugins/module_include.rb +3 -3
  154. data/lib/shrine/plugins/multi_cache.rb +27 -0
  155. data/lib/shrine/plugins/presign_endpoint.rb +27 -28
  156. data/lib/shrine/plugins/pretty_location.rb +15 -9
  157. data/lib/shrine/plugins/processing.rb +22 -9
  158. data/lib/shrine/plugins/rack_file.rb +2 -42
  159. data/lib/shrine/plugins/rack_response.rb +21 -10
  160. data/lib/shrine/plugins/recache.rb +6 -5
  161. data/lib/shrine/plugins/refresh_metadata.rb +13 -11
  162. data/lib/shrine/plugins/remote_url.rb +49 -49
  163. data/lib/shrine/plugins/remove_attachment.rb +12 -6
  164. data/lib/shrine/plugins/remove_invalid.rb +19 -8
  165. data/lib/shrine/plugins/restore_cached_data.rb +13 -7
  166. data/lib/shrine/plugins/sequel.rb +86 -36
  167. data/lib/shrine/plugins/signature.rb +10 -16
  168. data/lib/shrine/plugins/store_dimensions.rb +35 -40
  169. data/lib/shrine/plugins/tempfile.rb +1 -3
  170. data/lib/shrine/plugins/type_predicates.rb +113 -0
  171. data/lib/shrine/plugins/upload_endpoint.rb +28 -24
  172. data/lib/shrine/plugins/upload_options.rb +14 -15
  173. data/lib/shrine/plugins/url_options.rb +31 -0
  174. data/lib/shrine/plugins/validation.rb +80 -0
  175. data/lib/shrine/plugins/validation_helpers.rb +35 -58
  176. data/lib/shrine/plugins/versions.rb +107 -87
  177. data/lib/shrine/plugins.rb +22 -0
  178. data/lib/shrine/storage/file_system.rb +46 -64
  179. data/lib/shrine/storage/linter.rb +42 -7
  180. data/lib/shrine/storage/memory.rb +49 -0
  181. data/lib/shrine/storage/s3.rb +173 -160
  182. data/lib/shrine/uploaded_file.rb +32 -32
  183. data/lib/shrine/version.rb +3 -3
  184. data/lib/shrine.rb +87 -150
  185. data/shrine.gemspec +11 -12
  186. metadata +92 -82
  187. data/doc/migrating_storage.md +0 -76
  188. data/doc/plugins/backup.md +0 -31
  189. data/doc/plugins/copy.md +0 -24
  190. data/doc/plugins/delete_promoted.md +0 -12
  191. data/doc/plugins/direct_upload.md +0 -172
  192. data/doc/plugins/hooks.md +0 -58
  193. data/doc/plugins/logging.md +0 -42
  194. data/doc/plugins/migration_helpers.md +0 -60
  195. data/doc/plugins/moving.md +0 -19
  196. data/doc/plugins/multi_delete.md +0 -20
  197. data/doc/plugins/parallelize.md +0 -16
  198. data/doc/plugins/parsed_json.md +0 -23
  199. data/doc/regenerating_versions.md +0 -143
  200. data/lib/shrine/plugins/background_helpers.rb +0 -5
  201. data/lib/shrine/plugins/backup.rb +0 -90
  202. data/lib/shrine/plugins/copy.rb +0 -50
  203. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  204. data/lib/shrine/plugins/direct_upload.rb +0 -217
  205. data/lib/shrine/plugins/hooks.rb +0 -90
  206. data/lib/shrine/plugins/logging.rb +0 -142
  207. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  208. data/lib/shrine/plugins/moving.rb +0 -57
  209. data/lib/shrine/plugins/multi_delete.rb +0 -32
  210. data/lib/shrine/plugins/parallelize.rb +0 -78
  211. data/lib/shrine/plugins/parsed_json.rb +0 -29
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Shrine
4
- # Core class which handles attaching files to model instances.
5
- # Base implementation is defined in InstanceMethods and ClassMethods.
4
+ # Core class that handles attaching files. It uses Shrine and
5
+ # Shrine::UploadedFile objects internally.
6
6
  class Attacher
7
7
  @shrine_class = ::Shrine
8
8
 
@@ -18,195 +18,319 @@ class Shrine
18
18
  "#{shrine_class.inspect}::Attacher"
19
19
  end
20
20
 
21
- # Block that is executed in context of Shrine::Attacher during
22
- # validation. Example:
21
+ # Initializes the attacher from a data hash generated from `Attacher#data`.
23
22
  #
24
- # Shrine::Attacher.validate do
25
- # if get.size > 5*1024*1024
26
- # errors << "is too big (max is 5 MB)"
27
- # end
28
- # end
29
- def validate(&block)
30
- define_method(:validate_block, &block)
31
- private :validate_block
23
+ # attacher = Attacher.from_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
24
+ # attacher.file #=> #<Shrine::UploadedFile>
25
+ def from_data(data, **options)
26
+ attacher = new(**options)
27
+ attacher.load_data(data)
28
+ attacher
32
29
  end
33
30
  end
34
31
 
35
32
  module InstanceMethods
36
- # Returns the uploader that is used for the temporary storage.
37
- attr_reader :cache
38
-
39
- # Returns the uploader that is used for the permanent storage.
40
- attr_reader :store
33
+ # Returns the attached uploaded file.
34
+ attr_reader :file
41
35
 
42
- # Returns the context that will be sent to the uploader when uploading
43
- # and deleting. Can be modified with additional data to be sent to the
44
- # uploader.
36
+ # Returns options that are automatically forwarded to the uploader.
37
+ # Can be modified with additional data.
45
38
  attr_reader :context
46
39
 
47
- # Returns an array of validation errors created on file assignment in
48
- # the `Attacher.validate` block.
49
- attr_reader :errors
50
-
51
- # Initializes the necessary attributes.
52
- def initialize(record, name, cache: :cache, store: :store)
53
- @cache = shrine_class.new(cache)
54
- @store = shrine_class.new(store)
55
- @context = { record: record, name: name }
56
- @errors = []
40
+ # Initializes the attached file, temporary and permanent storage.
41
+ def initialize(file: nil, cache: :cache, store: :store)
42
+ @file = file
43
+ @cache = cache
44
+ @store = store
45
+ @context = {}
46
+ @previous = nil
57
47
  end
58
48
 
59
- # Returns the model instance associated with the attacher.
60
- def record; context[:record]; end
49
+ # Returns the temporary storage identifier.
50
+ def cache_key; @cache.to_sym; end
51
+ # Returns the permanent storage identifier.
52
+ def store_key; @store.to_sym; end
61
53
 
62
- # Returns the attachment name associated with the attacher.
63
- def name; context[:name]; end
54
+ # Returns the uploader that is used for the temporary storage.
55
+ def cache; shrine_class.new(cache_key); end
56
+ # Returns the uploader that is used for the permanent storage.
57
+ def store; shrine_class.new(store_key); end
64
58
 
65
- # Receives the attachment value from the form. It can receive an
66
- # already cached file as a JSON string, otherwise it assumes that it's
67
- # an IO object and uploads it to the temporary storage. The cached file
68
- # is then written to the attachment attribute in the JSON format.
59
+ # Calls #attach_cached, but skips if value is an empty string (this is
60
+ # useful when the uploaded file comes from form fields). Forwards any
61
+ # additional options to #attach_cached.
62
+ #
63
+ # attacher.assign(File.open(...))
64
+ # attacher.assign(File.open(...), metadata: { "foo" => "bar" })
65
+ # attacher.assign('{"id":"...","storage":"cache","metadata":{...}}')
66
+ # attacher.assign({ "id" => "...", "storage" => "cache", "metadata" => {} })
67
+ #
68
+ # # ignores the assignment when a blank string is given
69
+ # attacher.assign("")
69
70
  def assign(value, **options)
70
- if value.is_a?(String)
71
- return if value == "" || !cached?(uploaded_file(value))
72
- assign_cached(uploaded_file(value))
73
- else
74
- uploaded_file = cache!(value, action: :cache, **options) if value
75
- set(uploaded_file)
71
+ return if value == "" # skip empty hidden field
72
+
73
+ if value.is_a?(Hash) || value.is_a?(String)
74
+ return if uploaded_file(value) == file # skip assignment for current file
76
75
  end
76
+
77
+ attach_cached(value, **options)
77
78
  end
78
79
 
79
- # Accepts a Shrine::UploadedFile object and writes it to the attachment
80
- # attribute. It then runs file validations, and records that the
81
- # attachment has changed.
82
- def set(uploaded_file)
83
- file = get
84
- @old = file unless uploaded_file == file
85
- _set(uploaded_file)
86
- validate
80
+ # Sets an existing cached file, or uploads an IO object to temporary
81
+ # storage and sets it via #attach. Forwards any additional options to
82
+ # #attach.
83
+ #
84
+ # # upload file to temporary storage and set the uploaded file.
85
+ # attacher.attach_cached(File.open(...))
86
+ #
87
+ # # foward additional options to the uploader
88
+ # attacher.attach_cached(File.open(...), metadata: { "foo" => "bar" })
89
+ #
90
+ # # sets an existing cached file from JSON data
91
+ # attacher.attach_cached('{"id":"...","storage":"cache","metadata":{...}}')
92
+ #
93
+ # # sets an existing cached file from Hash data
94
+ # attacher.attach_cached({ "id" => "...", "storage" => "cache", "metadata" => {} })
95
+ def attach_cached(value, **options)
96
+ if value.is_a?(String) || value.is_a?(Hash)
97
+ change(cached(value, **options))
98
+ else
99
+ attach(value, storage: cache_key, action: :cache, **options)
100
+ end
87
101
  end
88
102
 
89
- # Runs the validations defined by `Attacher.validate`.
90
- def validate
91
- errors.clear
92
- validate_block if get
103
+ # Uploads given IO object and changes the uploaded file.
104
+ #
105
+ # # uploads the file to permanent storage
106
+ # attacher.attach(io)
107
+ #
108
+ # # uploads the file to specified storage
109
+ # attacher.attach(io, storage: :other_store)
110
+ #
111
+ # # forwards additional options to the uploader
112
+ # attacher.attach(io, upload_options: { acl: "public-read" }, metadata: { "foo" => "bar" })
113
+ #
114
+ # # removes the attachment
115
+ # attacher.attach(nil)
116
+ def attach(io, storage: store_key, **options)
117
+ file = upload(io, storage, **options) if io
118
+
119
+ change(file)
93
120
  end
94
121
 
95
- # Returns true if a new file has been attached.
96
- def changed?
97
- instance_variable_defined?(:@old)
122
+ # Deletes any previous file and promotes newly attached cached file.
123
+ # It also clears any dirty tracking.
124
+ #
125
+ # # promoting cached file
126
+ # attacher.assign(io)
127
+ # attacher.cached? #=> true
128
+ # attacher.finalize
129
+ # attacher.stored?
130
+ #
131
+ # # deleting previous file
132
+ # previous_file = attacher.file
133
+ # previous_file.exists? #=> true
134
+ # attacher.assign(io)
135
+ # attacher.finalize
136
+ # previous_file.exists? #=> false
137
+ #
138
+ # # clearing dirty tracking
139
+ # attacher.assign(io)
140
+ # attacher.changed? #=> true
141
+ # attacher.finalize
142
+ # attacher.changed? #=> false
143
+ def finalize
144
+ destroy_previous
145
+ promote_cached
146
+ @previous = nil
98
147
  end
99
- alias attached? changed?
100
148
 
101
- # Plugins can override this if they want something to be done before
102
- # save.
149
+ # Plugins can override this if they want something to be done in a
150
+ # "before save" callback.
103
151
  def save
104
152
  end
105
153
 
106
- # Deletes the old file and promotes the new one. Typically this should
107
- # be called after saving the model instance.
108
- def finalize
109
- return if !instance_variable_defined?(:@old)
110
- replace
111
- remove_instance_variable(:@old)
112
- _promote(action: :store) if cached?
154
+ # If a new cached file has been attached, uploads it to permanent storage.
155
+ # Any additional options are forwarded to #promote.
156
+ #
157
+ # attacher.assign(io)
158
+ # attacher.cached? #=> true
159
+ # attacher.promote_cached
160
+ # attacher.stored? #=> true
161
+ def promote_cached(**options)
162
+ promote(**options) if promote?
113
163
  end
114
164
 
115
- # Delegates to #promote, overriden for backgrounding.
116
- def _promote(uploaded_file = get, **options)
117
- promote(uploaded_file, **options)
165
+ # Uploads current file to permanent storage and sets the stored file.
166
+ #
167
+ # attacher.cached? #=> true
168
+ # attacher.promote
169
+ # attacher.stored? #=> true
170
+ def promote(storage: store_key, **options)
171
+ set upload(file, storage, action: :store, **options)
118
172
  end
119
173
 
120
- # Uploads the cached file to store, and writes the stored file to the
121
- # attachment attribute.
122
- def promote(uploaded_file = get, **options)
123
- stored_file = store!(uploaded_file, **options)
124
- result = swap(stored_file) or _delete(stored_file, action: :abort)
125
- result
174
+ # Delegates to `Shrine.upload`, passing the #context.
175
+ #
176
+ # # upload file to specified storage
177
+ # attacher.upload(io, :store) #=> #<Shrine::UploadedFile>
178
+ #
179
+ # # pass additional options for the uploader
180
+ # attacher.upload(io, :store, metadata: { "foo" => "bar" })
181
+ def upload(io, storage = store_key, **options)
182
+ shrine_class.upload(io, storage, **context, **options)
126
183
  end
127
184
 
128
- # Calls #update, overriden in ORM plugins, and returns true if the
129
- # attachment was successfully updated.
130
- def swap(uploaded_file)
131
- update(uploaded_file)
132
- uploaded_file if uploaded_file == get
185
+ # If a new file was attached, deletes previously attached file if any.
186
+ #
187
+ # previous_file = attacher.file
188
+ # attacher.attach(file)
189
+ # attacher.destroy_previous
190
+ # previous_file.exists? #=> false
191
+ def destroy_previous
192
+ @previous.destroy_attached if changed?
133
193
  end
134
194
 
135
- # Deletes the previous attachment that was replaced, typically called
136
- # after the model instance is saved with the new attachment.
137
- def replace
138
- _delete(@old, action: :replace) if @old && !cached?(@old)
195
+ # Destroys the attached file if it exists and is uploaded to permanent
196
+ # storage.
197
+ #
198
+ # attacher.file.exists? #=> true
199
+ # attacher.destroy_attached
200
+ # attacher.file.exists? #=> false
201
+ def destroy_attached
202
+ destroy if destroy?
139
203
  end
140
204
 
141
- # Deletes the current attachment, typically called after destroying the
142
- # record.
205
+ # Destroys the attachment.
206
+ #
207
+ # attacher.file.exists? #=> true
208
+ # attacher.destroy
209
+ # attacher.file.exists? #=> false
143
210
  def destroy
144
- file = get
145
- _delete(file, action: :destroy) if file && !cached?(file)
211
+ file&.delete
146
212
  end
147
213
 
148
- # Delegates to #delete!, overriden for backgrounding.
149
- def _delete(uploaded_file, **options)
150
- delete!(uploaded_file, **options)
214
+ # Sets the uploaded file with dirty tracking, and runs validations.
215
+ #
216
+ # attacher.change(uploaded_file)
217
+ # attacher.file #=> #<Shrine::UploadedFile>
218
+ # attacher.changed? #=> true
219
+ def change(file)
220
+ @previous = dup if change?(file)
221
+ set(file)
222
+ end
223
+
224
+ # Sets the uploaded file.
225
+ #
226
+ # attacher.set(uploaded_file)
227
+ # attacher.file #=> #<Shrine::UploadedFile>
228
+ # attacher.changed? #=> false
229
+ def set(file)
230
+ self.file = file
151
231
  end
152
232
 
153
- # Returns the URL to the attached file if it's present. It forwards any
154
- # given URL options to the storage.
233
+ # Returns the attached file.
234
+ #
235
+ # # when a file is attached
236
+ # attacher.get #=> #<Shrine::UploadedFile>
237
+ #
238
+ # # when no file is attached
239
+ # attacher.get #=> nil
240
+ def get
241
+ file
242
+ end
243
+
244
+ # If a file is attached, returns the uploaded file URL, otherwise returns
245
+ # nil. Any options are forwarded to the storage.
246
+ #
247
+ # attacher.file = file
248
+ # attacher.url #=> "https://..."
249
+ #
250
+ # attacher.file = nil
251
+ # attacher.url #=> nil
155
252
  def url(**options)
156
- get.url(**options) if read
253
+ file&.url(**options)
157
254
  end
158
255
 
159
- # Returns true if attachment is present and cached.
160
- def cached?(file = get)
161
- file && cache.uploaded?(file)
256
+ # Returns whether the attachment has changed.
257
+ #
258
+ # attacher.changed? #=> false
259
+ # attacher.attach(file)
260
+ # attacher.changed? #=> true
261
+ def changed?
262
+ !!@previous
162
263
  end
163
264
 
164
- # Returns true if attachment is present and stored.
165
- def stored?(file = get)
166
- file && store.uploaded?(file)
265
+ # Returns whether a file is attached.
266
+ #
267
+ # attacher.attach(io)
268
+ # attacher.attached? #=> true
269
+ #
270
+ # attacher.attach(nil)
271
+ # attacher.attached? #=> false
272
+ def attached?
273
+ !!file
167
274
  end
168
275
 
169
- # Returns a Shrine::UploadedFile instantiated from the data written to
170
- # the attachment attribute.
171
- def get
172
- uploaded_file(read) if read
276
+ # Returns whether the file is uploaded to temporary storage.
277
+ #
278
+ # attacher.cached? # checks current file
279
+ # attacher.cached?(file) # checks given file
280
+ def cached?(file = self.file)
281
+ uploaded?(file, cache_key)
173
282
  end
174
283
 
175
- # Reads from the `<attachment>_data` attribute on the model instance.
176
- # It returns nil if the value is blank.
177
- def read
178
- value = record.send(data_attribute)
179
- convert_after_read(value) unless value.nil? || value.empty?
284
+ # Returns whether the file is uploaded to permanent storage.
285
+ #
286
+ # attacher.stored? # checks current file
287
+ # attacher.stored?(file) # checks given file
288
+ def stored?(file = self.file)
289
+ uploaded?(file, store_key)
180
290
  end
181
291
 
182
- # Uploads the file using the #cache uploader, passing the #context.
183
- def cache!(io, **options)
184
- Shrine.deprecation("Sending :phase to Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
185
- cache.upload(io, context.merge(_equalize_phase_and_action(options)))
292
+ # Generates serializable data for the attachment.
293
+ #
294
+ # attacher.data #=> { "id" => "...", "storage" => "...", "metadata": { ... } }
295
+ def data
296
+ file&.data
186
297
  end
187
298
 
188
- # Uploads the file using the #store uploader, passing the #context.
189
- def store!(io, **options)
190
- Shrine.deprecation("Sending :phase to Attacher#store! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
191
- store.upload(io, context.merge(_equalize_phase_and_action(options)))
299
+ # Loads the uploaded file from data generated by `Attacher#data`.
300
+ #
301
+ # attacher.file #=> nil
302
+ # attacher.load_data({ "id" => "...", "storage" => "...", "metadata" => { ... } })
303
+ # attacher.file #=> #<Shrine::UploadedFile>
304
+ def load_data(data)
305
+ @file = data && uploaded_file(data)
192
306
  end
193
307
 
194
- # Deletes the file using the uploader, passing the #context.
195
- def delete!(uploaded_file, **options)
196
- Shrine.deprecation("Sending :phase to Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
197
- store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
308
+ # Saves the given uploaded file to an instance variable.
309
+ #
310
+ # attacher.file = uploaded_file
311
+ # attacher.file #=> #<Shrine::UploadedFile>
312
+ def file=(file)
313
+ unless file.is_a?(Shrine::UploadedFile) || file.nil?
314
+ fail ArgumentError, "expected file to be a Shrine::UploadedFile or nil, got #{file.inspect}"
315
+ end
316
+
317
+ @file = file
198
318
  end
199
319
 
200
- # Enhances `Shrine.uploaded_file` with the ability to recognize uploaded
201
- # files as JSON strings.
202
- def uploaded_file(object, &block)
203
- shrine_class.uploaded_file(object, &block)
320
+ # Returns attached file or raises an exception if no file is attached.
321
+ def file!
322
+ file or fail Error, "no file is attached"
204
323
  end
205
324
 
206
- # The name of the attribute on the model instance that is used to store
207
- # the attachment data. Defaults to `<attachment>_data`.
208
- def data_attribute
209
- :"#{name}_data"
325
+ # Converts JSON or Hash data into a Shrine::UploadedFile object.
326
+ #
327
+ # attacher.uploaded_file('{"id":"...","storage":"...","metadata":{...}}')
328
+ # #=> #<Shrine::UploadedFile ...>
329
+ #
330
+ # attacher.uploaded_file({ "id" => "...", "storage" => "...", "metadata" => {} })
331
+ # #=> #<Shrine::UploadedFile ...>
332
+ def uploaded_file(value)
333
+ shrine_class.uploaded_file(value)
210
334
  end
211
335
 
212
336
  # Returns the Shrine class that this attacher's class is namespaced
@@ -217,54 +341,53 @@ class Shrine
217
341
 
218
342
  private
219
343
 
220
- # Assigns a cached file.
221
- def assign_cached(cached_file)
222
- set(cached_file)
344
+ # The copy constructor that's called on #dup and #clone
345
+ # We need to duplicate the context to prevent it from being shared
346
+ def initialize_copy(other)
347
+ super
348
+ @context = @context.dup
223
349
  end
224
350
 
225
- # Writes the uploaded file to the attachment attribute. Overriden in ORM
226
- # plugins to additionally save the model instance.
227
- def update(uploaded_file)
228
- _set(uploaded_file)
229
- end
230
-
231
- # Performs validation actually.
232
- # This method is redefined with `Attacher.validate`.
233
- def validate_block
234
- end
235
-
236
- # Converts the UploadedFile to a data hash and writes it to the
237
- # attribute.
238
- def _set(uploaded_file)
239
- data = convert_to_data(uploaded_file) if uploaded_file
240
- write(data ? convert_before_write(data) : nil)
241
- end
351
+ # Converts a String or Hash value into an UploadedFile object and ensures
352
+ # it's uploaded to temporary storage.
353
+ #
354
+ # # from JSON data
355
+ # attacher.cached('{"id":"...","storage":"cache","metadata":{...}}')
356
+ # #=> #<Shrine::UploadedFile>
357
+ #
358
+ # # from Hash data
359
+ # attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => { ... } })
360
+ # #=> #<Shrine::UploadedFile>
361
+ def cached(value, **)
362
+ uploaded_file = uploaded_file(value)
363
+
364
+ # reject files not uploaded to temporary storage, because otherwise
365
+ # attackers could hijack other users' attachments
366
+ unless cached?(uploaded_file)
367
+ fail Shrine::Error, "expected cached file, got #{uploaded_file.inspect}"
368
+ end
242
369
 
243
- # Writes to the `<attachment>_data` attribute on the model instance.
244
- def write(value)
245
- record.send(:"#{data_attribute}=", value)
370
+ uploaded_file
246
371
  end
247
372
 
248
- # Returns the data hash of the given UploadedFile.
249
- def convert_to_data(uploaded_file)
250
- uploaded_file.data
373
+ # Whether attached file should be uploaded to permanent storage.
374
+ def promote?
375
+ changed? && cached?
251
376
  end
252
377
 
253
- # Returns the hash value dumped to JSON.
254
- def convert_before_write(value)
255
- value.to_json
378
+ # Whether attached file should be deleted.
379
+ def destroy?
380
+ attached? && !cached?
256
381
  end
257
382
 
258
- # Returns the read value unchanged.
259
- def convert_after_read(value)
260
- value
383
+ # Whether assigning the given file is considered a change.
384
+ def change?(file)
385
+ @file != file
261
386
  end
262
387
 
263
- # Temporary method used for transitioning from :phase to :action.
264
- def _equalize_phase_and_action(options)
265
- options[:phase] = options[:action] if options.key?(:action)
266
- options[:action] = options[:phase] if options.key?(:phase)
267
- options
388
+ # Returns whether the file is uploaded to specified storage.
389
+ def uploaded?(file, storage_key)
390
+ file&.storage_key == storage_key
268
391
  end
269
392
  end
270
393
 
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Shrine
4
- # Core class which creates attachment modules for specified attribute names
5
- # that are included into model classes.
6
- # Base implementation is defined in InstanceMethods and ClassMethods.
4
+ # Core class that provides an attachment interface for a specified attribute
5
+ # name, which can be added to model/entity classes. The model/entity plugins
6
+ # define the main interface, which delegates to a Shrine::Attacher object.
7
7
  class Attachment < Module
8
8
  @shrine_class = ::Shrine
9
9
 
@@ -18,6 +18,13 @@ class Shrine
18
18
  def inspect
19
19
  "#{shrine_class.inspect}::Attachment"
20
20
  end
21
+
22
+ # Shorthand for `Attachment.new`.
23
+ #
24
+ # Shrine::Attachment[:image]
25
+ def [](*args, **options)
26
+ new(*args, **options)
27
+ end
21
28
  end
22
29
 
23
30
  module InstanceMethods
@@ -27,40 +34,6 @@ class Shrine
27
34
  def initialize(name, **options)
28
35
  @name = name.to_sym
29
36
  @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
37
  end
65
38
 
66
39
  # Returns name of the attachment this module provides.
@@ -75,17 +48,11 @@ class Shrine
75
48
 
76
49
  # Returns class name with attachment name included.
77
50
  #
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)>"
51
+ # Shrine::Attachment.new(:image).to_s #=> "#<Shrine::Attachment(image)>"
86
52
  def inspect
87
- "#<#{self.class.inspect}(#{attachment_name})>"
53
+ "#<#{self.class.inspect}(#{@name})>"
88
54
  end
55
+ alias to_s inspect
89
56
 
90
57
  # Returns the Shrine class that this attachment's class is namespaced
91
58
  # under.