shrine 2.19.4 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +485 -43
  3. data/LICENSE.txt +1 -1
  4. data/README.md +81 -977
  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 +102 -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 +1115 -0
  20. data/doc/metadata.md +190 -109
  21. data/doc/multiple_files.md +62 -34
  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 +162 -101
  34. data/doc/plugins/derivatives.md +829 -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 +14 -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 +185 -167
  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 +4 -0
  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/retrieving_uploads.md +4 -1
  116. data/doc/securing_uploads.md +60 -37
  117. data/doc/storage/file_system.md +20 -3
  118. data/doc/storage/memory.md +19 -0
  119. data/doc/storage/s3.md +117 -83
  120. data/doc/testing.md +124 -144
  121. data/doc/upgrading_to_3.md +710 -0
  122. data/doc/validation.md +54 -90
  123. data/lib/shrine/attacher.rb +287 -171
  124. data/lib/shrine/attachment.rb +13 -46
  125. data/lib/shrine/plugins/_persistence.rb +93 -0
  126. data/lib/shrine/plugins/activerecord.rb +77 -34
  127. data/lib/shrine/plugins/add_metadata.rb +25 -17
  128. data/lib/shrine/plugins/atomic_helpers.rb +119 -0
  129. data/lib/shrine/plugins/backgrounding.rb +77 -113
  130. data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
  131. data/lib/shrine/plugins/column.rb +102 -0
  132. data/lib/shrine/plugins/data_uri.rb +38 -36
  133. data/lib/shrine/plugins/default_storage.rb +45 -15
  134. data/lib/shrine/plugins/default_url.rb +12 -24
  135. data/lib/shrine/plugins/default_url_options.rb +3 -30
  136. data/lib/shrine/plugins/delete_raw.rb +10 -16
  137. data/lib/shrine/plugins/derivation_endpoint.rb +89 -134
  138. data/lib/shrine/plugins/derivatives.rb +637 -0
  139. data/lib/shrine/plugins/determine_mime_type.rb +9 -21
  140. data/lib/shrine/plugins/download_endpoint.rb +109 -133
  141. data/lib/shrine/plugins/dynamic_storage.rb +5 -11
  142. data/lib/shrine/plugins/entity.rb +152 -0
  143. data/lib/shrine/plugins/form_assign.rb +108 -0
  144. data/lib/shrine/plugins/included.rb +6 -6
  145. data/lib/shrine/plugins/infer_extension.rb +13 -20
  146. data/lib/shrine/plugins/instrumentation.rb +54 -42
  147. data/lib/shrine/plugins/keep_files.rb +3 -15
  148. data/lib/shrine/plugins/metadata_attributes.rb +28 -19
  149. data/lib/shrine/plugins/mirroring.rb +142 -0
  150. data/lib/shrine/plugins/model.rb +158 -0
  151. data/lib/shrine/plugins/module_include.rb +3 -3
  152. data/lib/shrine/plugins/multi_cache.rb +27 -0
  153. data/lib/shrine/plugins/presign_endpoint.rb +18 -22
  154. data/lib/shrine/plugins/pretty_location.rb +15 -9
  155. data/lib/shrine/plugins/processing.rb +22 -9
  156. data/lib/shrine/plugins/rack_file.rb +2 -42
  157. data/lib/shrine/plugins/rack_response.rb +15 -10
  158. data/lib/shrine/plugins/recache.rb +6 -5
  159. data/lib/shrine/plugins/refresh_metadata.rb +13 -11
  160. data/lib/shrine/plugins/remote_url.rb +49 -49
  161. data/lib/shrine/plugins/remove_attachment.rb +10 -6
  162. data/lib/shrine/plugins/remove_invalid.rb +19 -8
  163. data/lib/shrine/plugins/restore_cached_data.rb +13 -7
  164. data/lib/shrine/plugins/sequel.rb +86 -36
  165. data/lib/shrine/plugins/signature.rb +10 -16
  166. data/lib/shrine/plugins/store_dimensions.rb +35 -40
  167. data/lib/shrine/plugins/tempfile.rb +1 -3
  168. data/lib/shrine/plugins/type_predicates.rb +113 -0
  169. data/lib/shrine/plugins/upload_endpoint.rb +25 -23
  170. data/lib/shrine/plugins/upload_options.rb +14 -15
  171. data/lib/shrine/plugins/url_options.rb +31 -0
  172. data/lib/shrine/plugins/validation.rb +80 -0
  173. data/lib/shrine/plugins/validation_helpers.rb +34 -57
  174. data/lib/shrine/plugins/versions.rb +107 -87
  175. data/lib/shrine/plugins.rb +22 -0
  176. data/lib/shrine/storage/file_system.rb +46 -64
  177. data/lib/shrine/storage/linter.rb +42 -7
  178. data/lib/shrine/storage/memory.rb +49 -0
  179. data/lib/shrine/storage/s3.rb +154 -158
  180. data/lib/shrine/uploaded_file.rb +28 -30
  181. data/lib/shrine/version.rb +3 -3
  182. data/lib/shrine.rb +86 -149
  183. data/shrine.gemspec +9 -10
  184. metadata +79 -83
  185. data/doc/migrating_storage.md +0 -76
  186. data/doc/plugins/backup.md +0 -31
  187. data/doc/plugins/copy.md +0 -24
  188. data/doc/plugins/delete_promoted.md +0 -12
  189. data/doc/plugins/direct_upload.md +0 -172
  190. data/doc/plugins/hooks.md +0 -58
  191. data/doc/plugins/logging.md +0 -42
  192. data/doc/plugins/migration_helpers.md +0 -60
  193. data/doc/plugins/moving.md +0 -19
  194. data/doc/plugins/multi_delete.md +0 -20
  195. data/doc/plugins/parallelize.md +0 -16
  196. data/doc/plugins/parsed_json.md +0 -23
  197. data/doc/regenerating_versions.md +0 -143
  198. data/lib/shrine/plugins/background_helpers.rb +0 -5
  199. data/lib/shrine/plugins/backup.rb +0 -90
  200. data/lib/shrine/plugins/copy.rb +0 -50
  201. data/lib/shrine/plugins/delete_promoted.rb +0 -20
  202. data/lib/shrine/plugins/direct_upload.rb +0 -217
  203. data/lib/shrine/plugins/hooks.rb +0 -90
  204. data/lib/shrine/plugins/logging.rb +0 -142
  205. data/lib/shrine/plugins/migration_helpers.rb +0 -70
  206. data/lib/shrine/plugins/moving.rb +0 -57
  207. data/lib/shrine/plugins/multi_delete.rb +0 -32
  208. data/lib/shrine/plugins/parallelize.rb +0 -78
  209. 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
231
+ end
232
+
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
151
242
  end
152
243
 
153
- # Returns the URL to the attached file if it's present. It forwards any
154
- # given URL options to the storage.
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,46 @@ class Shrine
217
341
 
218
342
  private
219
343
 
220
- # Assigns a cached file.
221
- def assign_cached(cached_file)
222
- set(cached_file)
223
- end
224
-
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
344
+ # Converts a String or Hash value into an UploadedFile object and ensures
345
+ # it's uploaded to temporary storage.
346
+ #
347
+ # # from JSON data
348
+ # attacher.cached('{"id":"...","storage":"cache","metadata":{...}}')
349
+ # #=> #<Shrine::UploadedFile>
350
+ #
351
+ # # from Hash data
352
+ # attacher.cached({ "id" => "...", "storage" => "cache", "metadata" => { ... } })
353
+ # #=> #<Shrine::UploadedFile>
354
+ def cached(value, **)
355
+ uploaded_file = uploaded_file(value)
356
+
357
+ # reject files not uploaded to temporary storage, because otherwise
358
+ # attackers could hijack other users' attachments
359
+ unless cached?(uploaded_file)
360
+ fail Shrine::Error, "expected cached file, got #{uploaded_file.inspect}"
361
+ end
242
362
 
243
- # Writes to the `<attachment>_data` attribute on the model instance.
244
- def write(value)
245
- record.send(:"#{data_attribute}=", value)
363
+ uploaded_file
246
364
  end
247
365
 
248
- # Returns the data hash of the given UploadedFile.
249
- def convert_to_data(uploaded_file)
250
- uploaded_file.data
366
+ # Whether attached file should be uploaded to permanent storage.
367
+ def promote?
368
+ changed? && cached?
251
369
  end
252
370
 
253
- # Returns the hash value dumped to JSON.
254
- def convert_before_write(value)
255
- value.to_json
371
+ # Whether attached file should be deleted.
372
+ def destroy?
373
+ attached? && !cached?
256
374
  end
257
375
 
258
- # Returns the read value unchanged.
259
- def convert_after_read(value)
260
- value
376
+ # Whether assigning the given file is considered a change.
377
+ def change?(file)
378
+ @file != file
261
379
  end
262
380
 
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
381
+ # Returns whether the file is uploaded to specified storage.
382
+ def uploaded?(file, storage_key)
383
+ file&.storage_key == storage_key
268
384
  end
269
385
  end
270
386
 
@@ -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.