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
@@ -0,0 +1,217 @@
1
+ ---
2
+ title: Atomic Helpers
3
+ ---
4
+
5
+ The [`atomic_helpers`][atomic_helpers] plugin provides API for retrieving and
6
+ persisting attachments in a concurrency-safe way, which is especially useful
7
+ when using the `backgrounding` plugin. The database plugins (`activerecord`
8
+ and `sequel`) implement atomic promotion and atomic persistence on top of this
9
+ plugin.
10
+
11
+ ```rb
12
+ plugin :atomic_helpers
13
+ ```
14
+
15
+ ## Problem Statement
16
+
17
+ What happens if two different processors (web workers, background jobs,
18
+ command-line executions, whatever) try to edit a shrine attachment
19
+ concurrently? The kinds of edits typically made include: "promoting a file",
20
+ moving it to a different storage and persisting that change in the model;
21
+ adding or changing a derivative; adding or changing a metadata element.
22
+
23
+ There are two main categories of "race condition":
24
+
25
+ 1. The file could be switched out from under you. If you were promoting a file,
26
+ but some other process has *changed* the attachment, you don't want to
27
+ overwrite it with the promomoted version of the *prior* attacchment. Likewise,
28
+ if you were adding metadata or a derivative, they would be corresponding to a
29
+ certain attachment, and you don't want to accidentally add them to a now changed
30
+ attacchment for which they are inappropriate.
31
+
32
+ 2. Overwriting each other's edits. Since all shrine (meta)data is stored in a
33
+ single JSON hash, standard implementations will write the entire JSON hash at
34
+ once to a rdbms column or other store. If two processes both read in the hash,
35
+ make a change to different keys in it, and then write it back out, the second
36
+ process to write will 'win' and overwrite changes made by the first.
37
+
38
+ The atomic helpers give you tools to avoid both of these sorts of race
39
+ conditions, under conditions of concurrent editing.
40
+
41
+ ## High-level ORM helpers
42
+
43
+ If you are using the `sequel` or `activerecord` plugins, they give you two
44
+ higher-level helpers: `atomic_persist` and `atomic_promote`. See the
45
+ [persistence] documentation for more.
46
+
47
+
48
+ ## Retrieving
49
+
50
+ The `Attacher.retrieve` method provided by the plugin instantiates an attacher
51
+ from a record instance, attachment name and attachment data, asserting that the
52
+ given attachment data matches the attached file on the record.
53
+
54
+ ```rb
55
+ # with a model instance
56
+ Shrine::Attacher.retrieve(
57
+ model: photo,
58
+ name: :image,
59
+ file: { "id" => "abc123", "storage" => "cache" },
60
+ )
61
+ #=> #<Shrine::Attacher ...>
62
+
63
+ # with an entity instance
64
+ Shrine::Attacher.retrieve(
65
+ entity: photo,
66
+ name: :image,
67
+ file: { "id" => "abc123", "storage" => "cache" },
68
+ )
69
+ #=> #<Shrine::Attacher ...>
70
+ ```
71
+
72
+ If the record has `Shrine::Attachment` included, the `#<name>_attacher` method
73
+ will be called on the record, which will return the correct attacher class.
74
+
75
+ ```rb
76
+ class Photo
77
+ include ImageUploader::Attachment(:image)
78
+ end
79
+ ```
80
+ ```rb
81
+ Shrine::Attacher.retrieve(model: photo, name: :image, file: { ... })
82
+ #=> #<ImageUploader::Attacher ...>
83
+ ```
84
+
85
+ Otherwise it will call `Attacher.from_model`/`Attacher.from_entity` from the
86
+ `model`/`entity` plugin, in which case you need to make sure to call
87
+ `Attacher.retrieve` on the appropriate attacher class.
88
+
89
+ ```rb
90
+ ImageUploader::Attacher.retrieve(entity: photo, name: :image, file: { ... })
91
+ #=> #<ImageUploader::Attacher ...>
92
+ ```
93
+
94
+ If the attached file on the record doesn't match the provided attachment data,
95
+ a `Shrine::AttachmentChanged` exception is raised. Note that metadata is
96
+ allowed to differ, Shrine will only compare location and storage of the file.
97
+
98
+ ```rb
99
+ photo.image_data #=> '{"id":"foo","storage":"store","metadata":{...}}'
100
+
101
+ Shrine::Attacher.retrieve(
102
+ model: photo,
103
+ name: :image,
104
+ file: { "id" => "bar", "storage" => "store" },
105
+ )
106
+ # ~> Shrine::AttachmentChanged: attachment has changed
107
+ ```
108
+
109
+ ### File data
110
+
111
+ The `Attacher#file_data` method can be used for sending the attached file data
112
+ into a background job. It returns only location and storage of the attached
113
+ file, leaving out any metadata or derivatives data that `Attacher#data` would
114
+ return. This way the background job payload is kept light.
115
+
116
+ ```rb
117
+ attacher.file_data #=> { "id" => "abc123", "storage" => "store" }
118
+ ```
119
+
120
+ This value can then be passed as the `:file` argument to
121
+ `Shrine::Attacher.retrieve`.
122
+
123
+ ## Promoting
124
+
125
+ The `Attacher#abstract_atomic_promote` method provided by the plugin promotes
126
+ the cached file to permanent storage, reloads the record to check whether the
127
+ attachment hasn't changed, and if not persists the promoted file.
128
+
129
+ Internally it calls `Attacher#abstract_atomic_persist` to do the persistence,
130
+ forwarding `:reload` and `:persist` options as well as a given block to it (see
131
+ the next section for more details).
132
+
133
+ ```rb
134
+ # in the controller
135
+ attacher.attach_cached(io)
136
+ attacher.cached? #=> true
137
+ ```
138
+ ```rb
139
+ # in a background job
140
+ attacher.abstract_atomic_promote(reload: -> (&block) { ... }, persist: -> { ... })
141
+ attacher.stored? #=> true
142
+ ```
143
+
144
+ If the attachment has changed during promotion, the promoted file is deleted and
145
+ a `Shrine::AttachmentChanged` exception is raised.
146
+
147
+ If you want to execute some code after the attachment change check but before
148
+ persistence, you can pass a block:
149
+
150
+ ```rb
151
+ attacher.abstract_atomic_promote(**options) do |reloaded_attacher|
152
+ # this will be executed before persistence
153
+ end
154
+ ```
155
+
156
+ Any additional options to `Attacher#abstract_atomic_promote` are forwarded to
157
+ `Attacher#promote`.
158
+
159
+ ## Persisting
160
+
161
+ The `Attacher#abstract_atomic_persist` method reloads the record to check
162
+ whether the attachment hasn't changed, and if not persists the attachment.
163
+
164
+ It requires reloader and persister to be passed in, as they will be specific to
165
+ the database library you're using. The reloader needs to call the given block
166
+ with the reloaded record, while the persister needs to persist the promoted
167
+ file.
168
+
169
+ ```rb
170
+ attacher.abstract_atomic_persist(
171
+ reload: -> (&block) { ... }, # call the block with reloaded record
172
+ persist: -> { ... }, # persist promoted file
173
+ )
174
+ ```
175
+
176
+ To illustrate, this is how the `Attacher#atomic_promote` method provided by the
177
+ `sequel` plugin is implemented:
178
+
179
+ ```rb
180
+ attacher.abstract_atomic_persist(
181
+ reload: -> (&block) {
182
+ attacher.record.db.transaction do
183
+ block.call attacher.record.dup.lock! # the DB lock ensures no changes
184
+ end
185
+ },
186
+ persist: -> {
187
+ attacher.record.save_changes(validate: false)
188
+ }
189
+ )
190
+ ```
191
+
192
+ By default, the file currently set on the attacher will be considered the
193
+ original file, and will be compared to the reloaded record. You can specify a
194
+ different original file:
195
+
196
+ ```rb
197
+ original_file = attacher.file
198
+
199
+ attacher.set(new_file)
200
+
201
+ attacher.abstract_atomic_persist(original_file, **options)
202
+ ```
203
+
204
+ If you want to execute some code after the attachment change check but before
205
+ persistence, you can pass a block:
206
+
207
+ ```rb
208
+ attacher.abstract_atomic_persist(**options) do |reloaded_attacher|
209
+ # this will be executed before persistence
210
+ end
211
+ ```
212
+
213
+ [atomic_helpers]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/atomic_helpers.rb
214
+
215
+ [persistence]: https://shrinerb.com/docs/plugins/persistence
216
+
217
+ [backgrounding]: https://shrinerb.com/docs/plugins/backgrounding
@@ -1,150 +1,208 @@
1
- # Backgrounding
1
+ ---
2
+ title: Backgrounding
3
+ ---
2
4
 
3
5
  The [`backgrounding`][backgrounding] plugin enables you to move promoting and
4
- deleting of files from record's lifecycle into background jobs. This is
5
- especially useful if you're doing processing and/or you're storing files on an
6
- external storage service.
6
+ deleting of files into background jobs. This is especially useful if you're
7
+ processing [derivatives] and storing files to a remote storage service.
7
8
 
8
- The plugin provides `Attacher.promote` and `Attacher.delete` methods, which
9
- allow you to hook up to promoting and deleting and spawn background jobs, by
10
- passing a block.
9
+ ```rb
10
+ Shrine.plugin :backgrounding # load the plugin globally
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ Define background jobs that will promote and destroy attachments:
11
16
 
12
17
  ```rb
13
- Shrine.plugin :backgrounding
18
+ class PromoteJob
19
+ include Sidekiq::Worker
20
+
21
+ def perform(attacher_class, record_class, record_id, name, file_data)
22
+ attacher_class = Object.const_get(attacher_class)
23
+ record = Object.const_get(record_class).find(record_id) # if using Active Record
14
24
 
15
- # makes all uploaders use background jobs
16
- Shrine::Attacher.promote { |data| PromoteJob.perform_async(data) }
17
- Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
25
+ attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
26
+ attacher.atomic_promote
27
+ rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
28
+ # attachment has changed or record has been deleted, nothing to do
29
+ end
30
+ end
18
31
  ```
32
+ ```rb
33
+ class DestroyJob
34
+ include Sidekiq::Worker
19
35
 
20
- If you don't want to apply backgrounding for all uploaders, you can declare the
21
- hooks only for specific uploaders (in this case it's still recommended to keep
22
- the plugin loaded globally).
36
+ def perform(attacher_class, data)
37
+ attacher_class = Object.const_get(attacher_class)
23
38
 
24
- ```rb
25
- class MyUploader < Shrine
26
- # makes this uploader use background jobs
27
- Attacher.promote { |data| PromoteJob.perform_async(data) }
28
- Attacher.delete { |data| DeleteJob.perform_async(data) }
39
+ attacher = attacher_class.from_data(data)
40
+ attacher.destroy
41
+ end
29
42
  end
30
43
  ```
31
44
 
32
- The yielded `data` variable is a serializable hash containing all context
33
- needed for promotion/deletion. Now you just need to declare the job classes,
34
- and inside them call `Attacher.promote` or `Attacher.delete`, this time with
35
- the received data.
45
+ Then, in your initializer, you can configure all uploaders to use these jobs:
36
46
 
37
47
  ```rb
38
- class PromoteJob
39
- include Sidekiq::Worker
40
- def perform(data)
41
- Shrine::Attacher.promote(data)
42
- end
48
+ Shrine::Attacher.promote_block do
49
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name.to_s, file_data)
50
+ end
51
+ Shrine::Attacher.destroy_block do
52
+ DestroyJob.perform_async(self.class.name, data)
43
53
  end
54
+ ```
44
55
 
45
- class DeleteJob
46
- include Sidekiq::Worker
47
- def perform(data)
48
- Shrine::Attacher.delete(data)
56
+ Alternatively, you can setup backgrounding only for specific uploaders:
57
+
58
+ ```rb
59
+ class MyUploader < Shrine
60
+ Attacher.promote_block do
61
+ PromoteJob.perform_async(self.class.name, record.class.name, record.id, name.to_s, file_data)
62
+ end
63
+ Attacher.destroy_block do
64
+ DestroyJob.perform_async(self.class.name, data)
49
65
  end
50
66
  end
51
67
  ```
52
68
 
53
- This example used Sidekiq, but obviously you could just as well use any other
54
- backgrounding library. This setup will be applied globally for all uploaders.
69
+ ## How it works
70
+
71
+ If backgrounding blocks are registered, they will be automatically called on
72
+ `Attacher#promote_cached` and `Attacher#destroy_previous` (called by
73
+ `Attacher#finalize`), and `Attacher#destroy_attached`.
74
+
75
+ ```rb
76
+ attacher.assign(file)
77
+ attacher.finalize # spawns promote job
78
+ attacher.destroy_attached # spawns destroy job
79
+ ```
80
+
81
+ These methods are automatically called as part of the attachment flow if you're
82
+ using `Shrine::Attachment` with a persistence plugin such as `activerecord` or
83
+ `sequel`.
55
84
 
56
- If you're generating versions, and you want to process some versions in the
57
- foreground before kicking off a background job, you can use the `recache`
58
- plugin.
85
+ ```rb
86
+ photo = Photo.new
87
+ photo.image = file
88
+ photo.save # spawns promote job
89
+ photo.destroy # spawns destroy job
90
+ ```
59
91
 
60
- In your application you can use `Attacher#cached?` and `Attacher#stored?`
61
- to differentiate between your background job being in progress and
62
- having completed.
92
+ ### Atomic promotion
93
+
94
+ Inside the promote job, we use `Attacher.retrieve` and
95
+ `Attacher#atomic_promote` for concurrency safety. These methods are provided
96
+ by the [`atomic_helpers`][atomic_helpers] plugin, which is loaded automatically
97
+ by your persistence plugin (`activerecord`, `sequel`).
63
98
 
64
99
  ```rb
65
- if user.avatar_attacher.cached? # background job is still in progress
66
- # ...
67
- elsif user.avatar_attacher.stored? # background job has finished
68
- # ...
69
- end
100
+ attacher = Shrine::Attacher.retrieve(model: record, name: name, file: file_data)
101
+ attacher.atomic_promote
70
102
  ```
71
103
 
72
- ## `Attacher.promote` and `Attacher.delete`
104
+ Without concurrency safety, promotion would look like this:
73
105
 
74
- In background jobs, `Attacher.promote` and `Attacher.delete` will resolve all
75
- necessary objects, and do the promotion/deletion. If `Attacher.find_record` is
76
- defined (which comes with ORM plugins), model instances will be treated as
77
- database records, with the `#id` attribute assumed to represent the primary
78
- key. Then promotion will have the following behaviour:
106
+ ```rb
107
+ attacher = record.send(:"#{name}_attacher")
108
+ attacher.promote
109
+ attacher.persist
110
+ ```
79
111
 
80
- 1. retrieves the database record
81
- * if record is not found, it finishes
82
- * if record is found but attachment has changed, it finishes
83
- 2. uploads cached file to permanent storage
84
- 3. reloads the database record
85
- * if record is not found, it deletes the promoted files and finishes
86
- * if record is found but attachment has changed, it deletes the promoted files and finishes
87
- 4. updates the record with the promoted files
112
+ ## Registering backgrounding blocks
88
113
 
89
- Both `Attacher.promote` and `Attacher.delete` return a `Shrine::Attacher`
90
- instance (if the action hasn't aborted), so you can use it to perform
91
- additional tasks:
114
+ The blocks registered by `Attacher.promote_block` and `Attacher#destroy_block`
115
+ are by default evaluated in context of a `Shrine::Attacher` instance. You can
116
+ also use the explicit version by declaring an attacher argument:
92
117
 
93
118
  ```rb
94
- def perform(data)
95
- attacher = Shrine::Attacher.promote(data)
96
- attacher.record.update(published: true) if attacher && attacher.record.is_a?(Post)
119
+ Shrine::Attacher.promote_block do |attacher|
120
+ PromoteJob.perform_async(
121
+ attacher.class.name,
122
+ attacher.record.class.name,
123
+ attacher.record.id,
124
+ attacher.name.to_s,
125
+ attacher.file_data,
126
+ )
127
+ end
128
+
129
+ Shrine::Attacher.destroy_block do |attacher|
130
+ DestroyJob.perform_async(
131
+ attacher.class.name,
132
+ attacher.data,
133
+ )
97
134
  end
98
135
  ```
99
136
 
100
- ### Plain models
137
+ You can also register backgrounding blocks on attacher *instances* for more
138
+ flexibility:
101
139
 
102
- You can also do backgrounding with plain models which don't represent database
103
- records; the plugin will use that mode if `Attacher.find_record` is not
104
- defined. In that case promotion will have the following behaviour:
140
+ ```rb
141
+ photo.image_attacher.promote_block do |attacher|
142
+ PromoteJob.perform_async(
143
+ attacher.class.name,
144
+ attacher.record.class.name,
145
+ attacher.record.id,
146
+ attacher.name.to_s,
147
+ attacher.file_data,
148
+ current_user.id, # pass arguments known at the controller level
149
+ )
150
+ end
105
151
 
106
- 1. instantiates the model
107
- 2. uploads cached file to permanent storage
108
- 3. writes promoted files to the model instance
152
+ photo.image = file
153
+ photo.save # executes the promote block above
154
+ ```
109
155
 
110
- You can then retrieve the promoted files via the attacher object that
111
- `Attacher.promote` returns, and do any additional tasks if you need to.
156
+ ## Calling backgrounding blocks
112
157
 
113
- ## `Attacher#_promote` and `Attacher#_delete`
158
+ If you want to call backgrounding blocks directly, you can do that by calling
159
+ `Attacher#promote_background` and `Attacher#destroy_background`.
114
160
 
115
- The plugin modifies `Attacher#_promote` and `Attacher#_delete` to call the
116
- registered blocks with serializable attacher data, and these methods are
117
- internally called by the attacher. `Attacher#promote` and `Attacher#delete!`
118
- remain synchronous.
161
+ ```rb
162
+ attacher.promote_background # calls promote block directly
163
+ attacher.destroy_background # calls destroy block directly
164
+ ```
165
+
166
+ Any options passed to these methods will be forwarded to the background block:
119
167
 
120
168
  ```rb
121
- # asynchronous (spawn background jobs)
122
- attacher._promote
123
- attacher._delete(attachment)
169
+ attacher.promote_background(foo: "bar")
170
+ ```
171
+ ```rb
172
+ # with instance eval
173
+ Shrine::Attacher.promote_block do |**options|
174
+ options #=> { foo: "bar" }
175
+ end
124
176
 
125
- # synchronous
126
- attacher.promote
127
- attacher.delete!(attachment)
177
+ # without instance eval
178
+ Shrine::Attacher.promote_block do |attacher, **options|
179
+ options #=> { foo: "bar" }
180
+ end
128
181
  ```
129
182
 
130
- ## `Attacher.dump` and `Attacher.load`
183
+ ## Disabling backgrounding
131
184
 
132
- The plugin adds `Attacher.dump` and `Attacher.load` methods for serializing
133
- attacher object and loading it back up. You can use them to spawn background
134
- jobs for custom tasks.
185
+ If you've registered backgrounding blocks, but want to temporarily disable them
186
+ and make the execution synchronous, you can override them on the attacher level
187
+ and call the default behaviour:
135
188
 
136
189
  ```rb
137
- data = Shrine::Attacher.dump(attacher)
138
- SomethingJob.perform_async(data)
190
+ photo.image_attacher.promote_block { promote } # promote synchronously
191
+ photo.image_attacher.destroy_block { destroy } # destroy synchronously
139
192
 
140
- # ...
193
+ # ... now promotion and deletion will be synchronous ...
194
+ ```
141
195
 
142
- class SomethingJob
143
- def perform(data)
144
- attacher = Shrine::Attacher.load(data)
145
- # ...
146
- end
196
+ You can also do this on the class level if you want to disable backgrounding
197
+ that was set up by a superclass:
198
+
199
+ ```rb
200
+ class MyUploader < Shrine
201
+ Attacher.promote_block { promote } # promote synchronously
202
+ Attacher.destroy_block { destroy } # destroy synchronously
147
203
  end
148
204
  ```
149
205
 
150
- [backgrounding]: /lib/shrine/plugins/backgrounding.rb
206
+ [backgrounding]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/backgrounding.rb
207
+ [derivatives]: https://shrinerb.com/docs/plugins/derivatives
208
+ [atomic_helpers]: https://shrinerb.com/docs/plugins/atomic_helpers
@@ -1,4 +1,6 @@
1
- # Cached Attachment Data
1
+ ---
2
+ title: Cached Attachment Data
3
+ ---
2
4
 
3
5
  The [`cached_attachment_data`][cached_attachment_data] plugin adds the ability
4
6
  to retain the cached file across form redisplays, which means the file doesn't
@@ -13,13 +15,13 @@ cached file as JSON, and should be used to set the value of the hidden form
13
15
  field.
14
16
 
15
17
  ```rb
16
- @user.cached_avatar_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
18
+ photo.cached_image_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
17
19
  ```
18
20
 
19
- This method delegates to `Attacher#read_cached`:
21
+ This method delegates to `Attacher#cached_data`:
20
22
 
21
23
  ```rb
22
- attacher.read_cached #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
24
+ attacher.cached_data #=> '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
23
25
  ```
24
26
 
25
- [cached_attachment_data]: /lib/shrine/plugins/cached_attachment_data.rb
27
+ [cached_attachment_data]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/cached_attachment_data.rb
@@ -0,0 +1,121 @@
1
+ ---
2
+ title: Column
3
+ ---
4
+
5
+ The [`column`][column] plugin provides interface for serializing and
6
+ deserializing attachment data in format suitable for persisting in a database
7
+ column (JSON by default).
8
+
9
+ ```rb
10
+ plugin :column
11
+ ```
12
+
13
+ ## Serializing
14
+
15
+ The `Attacher#column_data` method returns attached file data in serialized
16
+ format, ready to be persisted into a database column.
17
+
18
+ ```rb
19
+ attacher.attach(io)
20
+ attacher.column_data #=> '{"id":"...","storage":"...","metadata":{...}}'
21
+ ```
22
+
23
+ If there is no attached file, `nil` is returned.
24
+
25
+ ```rb
26
+ attacher.column_data #=> nil
27
+ ```
28
+
29
+ If you want to retrieve this data as a Hash, use `Attacher#data` instead.
30
+
31
+ ## Deserializing
32
+
33
+ The `Attacher.from_column` method instantiates the attacher from serialized
34
+ attached file data.
35
+
36
+ ```rb
37
+ attacher = Shrine::Attacher.from_column('{"id":"...","storage":"...","metadata":{...}}')
38
+ attacher.file #=> #<Shrine::UploadedFile>
39
+ ```
40
+
41
+ If `nil` is given, it means no attached file.
42
+
43
+ ```rb
44
+ attacher = Shrine::Attacher.from_column(nil)
45
+ attacher.file #=> nil
46
+ ```
47
+
48
+ Any additional options are forwarded to `Attacher#initialize`.
49
+
50
+ ```rb
51
+ attacher = Shrine::Attacher.from_column('{...}', store: :other_store)
52
+ attacher.store_key #=> :other_store
53
+ ```
54
+
55
+ If you want to load attachment data into an existing attacher, use
56
+ `Attacher#load_column`.
57
+
58
+ ```rb
59
+ attacher.file #=> nil
60
+ attacher.load_column('{"id":"...","storage":"...","metadata":{...}}')
61
+ attacher.file #=> #<Shrine::UploadedFile>
62
+ ```
63
+
64
+ If you want to load attachment from a Hash, use `Attacher.from_data` or
65
+ `Attacher#load_data` instead.
66
+
67
+ ## Serializer
68
+
69
+ By default, the `JSON` standard library is used for serializing hash data. With
70
+ the [`model`][model] and [`entity`][entity] plugin, the data is serialized
71
+ before writing to and deserialized after reading from the data attribute.
72
+
73
+ You can also use your own serializer via the `:serializer` option. The
74
+ serializer object needs to implement `#dump` and `#load` methods:
75
+
76
+ ```rb
77
+ class MyDataSerializer
78
+ def self.dump(data)
79
+ data #=> { "id" => "...", "storage" => "...", "metadata" => { ... } }
80
+
81
+ JSON.generate(data) # serialize data, e.g. into JSON
82
+ end
83
+
84
+ def self.load(data)
85
+ data #=> '{"id":"...", "storage":"...", "metadata": {...}}'
86
+
87
+ JSON.parse(data) # deserialize data, e.g. from JSON
88
+ end
89
+ end
90
+
91
+ plugin :column, serializer: MyDataSerializer
92
+ ```
93
+
94
+ Some serialization libraries such as [Oj] and [MessagePack] already implement
95
+ this interface, which simplifies the configuration:
96
+
97
+ ```rb
98
+ require "oj" # https://github.com/ohler55/oj
99
+
100
+ plugin :column, serializer: Oj
101
+ ```
102
+
103
+ If you want to disable serialization and work with hashes directly, you can set
104
+ `:serializer` to `nil`:
105
+
106
+ ```rb
107
+ plugin :column, serializer: nil # disable serialization
108
+ ```
109
+
110
+ The serializer can also be changed for a particular attacher instance:
111
+
112
+ ```rb
113
+ Shrine::Attacher.new(column_serializer: Oj) # use custom serializer
114
+ Shrine::Attacher.new(column_serializer: nil) # disable serialization
115
+ ```
116
+
117
+ [column]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/column.rb
118
+ [model]: https://shrinerb.com/docs/plugins/model
119
+ [entity]: https://shrinerb.com/docs/plugins/entity
120
+ [Oj]: https://github.com/ohler55/oj
121
+ [MessagePack]: https://github.com/msgpack/msgpack-ruby