shrine 3.0.0.alpha → 3.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -2
  3. data/README.md +1 -1
  4. data/doc/creating_persistence_plugins.md +20 -63
  5. data/doc/plugins/activerecord.md +13 -106
  6. data/doc/plugins/add_metadata.md +31 -9
  7. data/doc/plugins/atomic_helpers.md +33 -9
  8. data/doc/plugins/cached_attachment_data.md +3 -3
  9. data/doc/plugins/data_uri.md +16 -19
  10. data/doc/plugins/default_storage.md +32 -8
  11. data/doc/plugins/default_url.md +21 -9
  12. data/doc/plugins/derivation_endpoint.md +48 -0
  13. data/doc/plugins/derivatives.md +74 -63
  14. data/doc/plugins/entity.md +27 -2
  15. data/doc/plugins/included.md +8 -7
  16. data/doc/plugins/metadata_attributes.md +20 -5
  17. data/doc/plugins/model.md +22 -2
  18. data/doc/plugins/persistence.md +89 -0
  19. data/doc/plugins/remote_url.md +41 -45
  20. data/doc/plugins/remove_attachment.md +23 -4
  21. data/doc/plugins/remove_invalid.md +3 -4
  22. data/doc/plugins/restore_cached_data.md +3 -1
  23. data/doc/plugins/sequel.md +13 -105
  24. data/doc/plugins/signature.md +3 -3
  25. data/lib/shrine/attachment.rb +11 -1
  26. data/lib/shrine/plugins/_persistence.rb +69 -0
  27. data/lib/shrine/plugins/activerecord.rb +31 -81
  28. data/lib/shrine/plugins/atomic_helpers.rb +13 -3
  29. data/lib/shrine/plugins/backgrounding.rb +8 -8
  30. data/lib/shrine/plugins/cached_attachment_data.rb +2 -6
  31. data/lib/shrine/plugins/data_uri.rb +2 -6
  32. data/lib/shrine/plugins/default_storage.rb +28 -2
  33. data/lib/shrine/plugins/derivation_endpoint.rb +3 -9
  34. data/lib/shrine/plugins/derivatives.rb +26 -17
  35. data/lib/shrine/plugins/entity.rb +24 -16
  36. data/lib/shrine/plugins/included.rb +1 -0
  37. data/lib/shrine/plugins/infer_extension.rb +2 -0
  38. data/lib/shrine/plugins/metadata_attributes.rb +18 -8
  39. data/lib/shrine/plugins/model.rb +35 -14
  40. data/lib/shrine/plugins/remote_url.rb +2 -6
  41. data/lib/shrine/plugins/remove_attachment.rb +2 -6
  42. data/lib/shrine/plugins/remove_invalid.rb +10 -6
  43. data/lib/shrine/plugins/sequel.rb +31 -78
  44. data/lib/shrine/plugins/upload_options.rb +2 -2
  45. data/lib/shrine/plugins/validation.rb +1 -11
  46. data/lib/shrine/version.rb +1 -1
  47. metadata +4 -2
@@ -1,7 +1,7 @@
1
1
  # Default URL
2
2
 
3
3
  The [`default_url`][default_url] plugin allows setting the URL which will be
4
- returned when the attachment is missing.
4
+ returned when there is no attached file.
5
5
 
6
6
  ```rb
7
7
  plugin :default_url
@@ -11,13 +11,24 @@ Attacher.default_url do |options|
11
11
  end
12
12
  ```
13
13
 
14
- `Attacher#url` returns the default URL when attachment is missing. Any passed
15
- in URL options will be present in the `options` hash.
14
+ The `Attacher#url` method will return the default URL when attachment is
15
+ missing:
16
16
 
17
17
  ```rb
18
- attacher.url #=> "/avatar/missing.jpg"
19
- # or
20
18
  user.avatar_url #=> "/avatar/missing.jpg"
19
+ # or
20
+ attacher.url #=> "/avatar/missing.jpg"
21
+ ```
22
+
23
+ Any URL options passed will be available in the default URL block:
24
+
25
+ ```rb
26
+ attacher.url(foo: "bar")
27
+ ```
28
+ ```rb
29
+ Attacher.default_url do |options|
30
+ options #=> { foo: "bar" }
31
+ end
21
32
  ```
22
33
 
23
34
  The default URL block is evaluated in the context of an instance of
@@ -25,10 +36,11 @@ The default URL block is evaluated in the context of an instance of
25
36
 
26
37
  ```rb
27
38
  Attacher.default_url do |options|
28
- self #=> #<Shrine::Attacher>
39
+ self #=> #<Shrine::Attacher>
29
40
 
30
- name #=> :avatar
31
- record #=> #<User>
41
+ name #=> :avatar
42
+ record #=> #<User>
43
+ context #=> { ... }
32
44
  end
33
45
  ```
34
46
 
@@ -41,7 +53,7 @@ option:
41
53
  plugin :default_url, host: "https://example.com"
42
54
  ```
43
55
  ```rb
44
- user.avatar_url #=> "https://example.com/avatar/missing.jpg"
56
+ attacher.url #=> "https://example.com/avatar/missing.jpg"
45
57
  ```
46
58
 
47
59
  [default_url]: /lib/shrine/plugins/default_url.rb
@@ -28,6 +28,7 @@ process them on-the-fly.
28
28
  - [Skipping download](#skipping-download)
29
29
  * [Derivation API](#derivation-api)
30
30
  * [Plugin Options](#plugin-options)
31
+ * [Instrumentation](#instrumentation)
31
32
 
32
33
  ## Quick start
33
34
 
@@ -785,6 +786,53 @@ derivation.option(:upload_location)
785
786
  | `:upload_storage` | Storage to which the derivations will be uploaded | same storage as the source file |
786
787
  | `:version` | Version number to append to the URL for cache busting | `nil` |
787
788
 
789
+ ## Instrumentation
790
+
791
+ If the `instrumentation` plugin has been loaded, the `determine_mime_type` plugin
792
+ adds instrumentation around derivation processing.
793
+
794
+ ```rb
795
+ # instrumentation plugin needs to be loaded *before* derivation_endpoint
796
+ plugin :instrumentation
797
+ plugin :derivation_endpoint
798
+ ```
799
+
800
+ Derivation processing will trigger a `derivation.shrine` event with the
801
+ following payload:
802
+
803
+ | Key | Description |
804
+ | :-- | :---- |
805
+ | `:derivation` | `Shrine::Derivation` object for this processing |
806
+ | `:uploader` | The uploader class that sent the event |
807
+
808
+ A default log subscriber is added as well which logs these events:
809
+
810
+ ```
811
+ Derivation (492ms) – {:name=>:thumbnail, :args=>[600, 600], :uploader=>Shrine}
812
+ ```
813
+
814
+ You can also use your own log subscriber:
815
+
816
+ ```rb
817
+ plugin :derivation_endpoint, log_subscriber: -> (event) {
818
+ Shrine.logger.info JSON.generate(
819
+ name: event.name,
820
+ duration: event.duration,
821
+ name: event[:derivation].name,
822
+ args: event[:derivation].args,
823
+ )
824
+ }
825
+ ```
826
+ ```
827
+ {"name":"derivation","duration":492,"name":"thumbnail","args":[600,600],"uploader":"Shrine"}
828
+ ```
829
+
830
+ Or disable logging altogether:
831
+
832
+ ```rb
833
+ plugin :derivation_endpoint, log_subscriber: nil
834
+ ```
835
+
788
836
  [derivation_endpoint]: /lib/shrine/plugins/derivation_endpoint.rb
789
837
  [ImageProcessing]: https://github.com/janko/image_processing
790
838
  [`Content-Type`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
@@ -12,6 +12,7 @@ plugin :derivatives
12
12
 
13
13
  * [API overview](#api-overview)
14
14
  * [Creating derivatives](#creating-derivatives)
15
+ - [Derivatives storage](#derivatives-storage)
15
16
  - [Nesting derivatives](#nesting-derivatives)
16
17
  * [Retrieving derivatives](#retrieving-derivatives)
17
18
  * [Derivative URL](#derivative-url)
@@ -20,7 +21,6 @@ plugin :derivatives
20
21
  - [Source file](#source-file)
21
22
  * [Adding derivatives](#adding-derivatives)
22
23
  * [Uploading derivatives](#uploading-derivatives)
23
- - [Derivatives storage](#derivatives-storage)
24
24
  - [Uploader options](#uploader-options)
25
25
  - [File deletion](#file-deletion)
26
26
  * [Merging derivatives](#merging-derivatives)
@@ -50,8 +50,8 @@ class, and it's layered in the following way:
50
50
 
51
51
  When you have a file attached, you can generate derivatives from it and save
52
52
  them alongside the attached file. The simplest way to do this is to define a
53
- processor which returns the processed files, and then trigger it with
54
- `Attacher#create_derivatives` when you want to generate the derivatives.
53
+ processor which returns the processed files, and then trigger it when you want
54
+ to create derivatives.
55
55
 
56
56
  Here is an example of generating image thumbnails:
57
57
 
@@ -85,7 +85,7 @@ end
85
85
  photo.image #=> #<Shrine::UploadedFile @id="original.jpg" @storage_key=:store ...>
86
86
  photo.image_derivatives #=> {}
87
87
 
88
- photo.image_attacher.create_derivatives(:thumbnails) # calls processor and uploads results
88
+ photo.image_derivatives!(:thumbnails) # calls registered processor and uploads results
89
89
  photo.image_derivatives #=>
90
90
  # {
91
91
  # small: #<Shrine::UploadedFile @id="small.jpg" @storage_key=:store ...>,
@@ -111,12 +111,68 @@ photo.image_data #=>
111
111
  # }
112
112
  ```
113
113
 
114
- Any additional options passed to `Attacher#create_derivatives` are forwarded to
115
- [`Attacher#upload_derivatives`](#uploading-derivatives).
114
+ When using `Shrine::Attacher` directly, derivatives are created using
115
+ `Attacher#create_derivatives`:
116
+
117
+ ```rb
118
+ attacher.file #=> #<Shrine::UploadedFile @id="original.jpg" @storage_key=:store ...>
119
+ attacher.derivatives #=> {}
120
+
121
+ attacher.create_derivatives(:thumbnails) # calls registered processor and uploads results
122
+ attacher.derivatives #=>
123
+ # {
124
+ # small: #<Shrine::UploadedFile @id="small.jpg" @storage_key=:store ...>,
125
+ # medium: #<Shrine::UploadedFile @id="medium.jpg" @storage_key=:store ...>,
126
+ # large: #<Shrine::UploadedFile @id="large.jpg" @storage_key=:store ...>,
127
+ # }
128
+ ```
129
+
130
+ ### Derivatives storage
131
+
132
+ By default, derivatives are uploaded to the permanent storage of the attacher.
133
+ You can change the default destination storage with the `:storage` plugin
134
+ option:
116
135
 
117
136
  ```rb
118
- attacher.create_derivatives(:thumbnails, storage: :other_store) # specify destination storage
119
- attacher.create_derivatives(:thumbnails, upload_options: { acl: "public-read" }) # pass uploader options
137
+ plugin :derivatives, storage: :other_store
138
+ ```
139
+
140
+ The storage can be dynamic based on the derivative name:
141
+
142
+ ```rb
143
+ plugin :derivatives, storage: -> (derivative) do
144
+ if derivative == :thumb
145
+ :thumbnail_store
146
+ else
147
+ :store
148
+ end
149
+ end
150
+ ```
151
+
152
+ You can also set this option with `Attacher.derivatives_storage`:
153
+
154
+ ```rb
155
+ Attacher.derivatives_storage :other_store
156
+ # or
157
+ Attacher.derivatives_storage do |derivative|
158
+ if derivative == :thumb
159
+ :thumbnail_store
160
+ else
161
+ :store
162
+ end
163
+ end
164
+ ```
165
+
166
+ The storage block is evaluated in the context of a `Shrine::Attacher` instance:
167
+
168
+ ```rb
169
+ Attacher.derivatives_storage do |derivative|
170
+ self #=> #<Shrine::Attacher>
171
+
172
+ record #=> #<Photo>
173
+ name #=> :image
174
+ context #=> { ... }
175
+ end
120
176
  ```
121
177
 
122
178
  ### Nesting derivatives
@@ -284,8 +340,8 @@ Attacher.derivatives_processor :my_processor do |original|
284
340
  end
285
341
  ```
286
342
 
287
- Moreover, any options passed to `Attacher#process_derivatives` will be
288
- forwarded to the processor:
343
+ Moreover, any options passed to `Attacher#process_derivatives` (or
344
+ `Attacher#create_derivatives`) will be forwarded to the processor:
289
345
 
290
346
  ```rb
291
347
  attacher.process_derivatives(:my_processor, foo: "bar")
@@ -315,7 +371,7 @@ attacher.process_derivatives(:my_processor) # downloads attached file and passes
315
371
  If you already have the source file locally, or if you're calling multiple
316
372
  processors in a row and want to avoid downloading the same source file each
317
373
  time, you can pass the source file as the second argument to
318
- `Attacher#process_derivatives`:
374
+ `Attacher#process_derivatives` (or `Attacher#create_derivatives`):
319
375
 
320
376
  ```rb
321
377
  # this way the source file is downloaded only once
@@ -406,63 +462,18 @@ attacher.upload_derivative(:thumb, thumbnail_file)
406
462
  #=> #<Shrine::UploadedFile>
407
463
  ```
408
464
 
409
- ### Derivatives storage
410
-
411
- By default, derivatives are uploaded to the permanent storage of the attacher
412
- (`:store` by default). You can specify a different destination storage for
413
- `Attacher#upload_derivative(s)` with the `:storage` option:
414
-
415
- ```rb
416
- attacher.upload_derivatives(derivatives, storage: :other_store)
417
- ```
418
-
419
- You can also set a default derivatives storage on the plugin level:
420
-
421
- ```rb
422
- plugin :derivatives, storage: :other_store
423
- ```
424
-
425
- The storage can be dynamic based on the derivative name:
426
-
427
- ```rb
428
- plugin :derivatives, storage: -> (derivative) do
429
- if derivative == :thumb
430
- :thumbnail_store
431
- else
432
- :store
433
- end
434
- end
435
- ```
436
-
437
- You can also set this option with `Attacher.derivatives_storage`:
438
-
439
- ```rb
440
- Attacher.derivatives_storage :other_store
441
- # or
442
- Attacher.derivatives_storage do |derivative|
443
- if derivative == :thumb
444
- :thumbnail_store
445
- else
446
- :store
447
- end
448
- end
449
- ```
465
+ ### Uploader options
450
466
 
451
- The storage block is evaluated in the context of a `Shrine::Attacher` instance:
467
+ You can specify the destination storage by passing `:storage` option to
468
+ `Attacher#upload_derivative(s)`. This will override the [default derivatives
469
+ storage](#derivatives-storage) setting.
452
470
 
453
471
  ```rb
454
- Attacher.derivatives_storage do |derivative|
455
- self #=> #<Shrine::Attacher>
456
-
457
- record #=> #<Photo>
458
- name #=> :image
459
- context #=> { ... }
460
- end
472
+ attacher.upload_derivative(:thumb, thumnbail_file, storage: :other_store)
473
+ #=> #<Shrine::UploadedFile @id="thumb.jpg" @storage_key=:other_store ...>
461
474
  ```
462
475
 
463
- ### Uploader options
464
-
465
- Any options other than `:storage` will be forwarded to the uploader:
476
+ Any other options will be forwarded to the uploader:
466
477
 
467
478
  ```rb
468
479
  attacher.upload_derivative :thumb, thumbnail_file,
@@ -102,6 +102,19 @@ attacher = photo.image_attacher
102
102
  attacher.store_key #=> :other_store
103
103
  ```
104
104
 
105
+ You can retrieve an `Attacher` instance from the entity *class* as well. In
106
+ this case it will not be initialized with any entity instance.
107
+
108
+ ```rb
109
+ attacher = Photo.image_attacher
110
+ attacher #=> #<ImageUploader::Attacher>
111
+ attacher.record #=> nil
112
+ attacher.name #=> nil
113
+
114
+ attacher = Photo.image_attacher(store: :other_store)
115
+ attacher.store_key #=> :other_store
116
+ ```
117
+
105
118
  ## Attacher
106
119
 
107
120
  ### Loading entity
@@ -133,9 +146,21 @@ You can also load an entity into an existing attacher with
133
146
  ```rb
134
147
  photo = Photo.new(image_data: '{"id":"...","storage":"...","metadata":{...}}')
135
148
 
136
- attacher.file #=> nil
137
149
  attacher.load_entity(photo, :image)
138
- attacher.file #=> #<ImageUploader::UploadedFile>
150
+ attacher.record #=> #<Photo>
151
+ attacher.name #=> :image
152
+ attacher.file #=> #<ImageUploader::UploadedFile>
153
+ ```
154
+
155
+ Or just `Attacher#set_entity` if you don't want to load attachment data:
156
+
157
+ ```rb
158
+ photo = Photo.new(image_data: '{"id":"...","storage":"...","metadata":{...}}')
159
+
160
+ attacher.set_entity(photo, :image) # doesn't load attachment data
161
+ attacher.record #=> #<Photo>
162
+ attacher.name #=> :image
163
+ attacher.file #=> nil
139
164
  ```
140
165
 
141
166
  ### Reloading
@@ -1,18 +1,19 @@
1
1
  # Included
2
2
 
3
3
  The [`included`][included] plugin allows you to hook up to the `.included` hook
4
- of the attachment module, and call additional methods on the model which
4
+ of the attachment module, and call additional methods on the model that
5
5
  includes it.
6
6
 
7
7
  ```rb
8
8
  plugin :included do |name|
9
- before_save do
10
- # ...
11
- end
9
+ # called when attachment module is included into a model
10
+
11
+ self #=> #<Photo>
12
+ name #=> :image
12
13
  end
13
14
  ```
14
-
15
- If you want to define additional methods on the model, it's recommended to use
16
- the `module_include` plugin instead.
15
+ ```rb
16
+ Photo.include Shrine::Attachment(:image)
17
+ ```
17
18
 
18
19
  [included]: /lib/shrine/plugins/included.rb
@@ -7,7 +7,9 @@ method:
7
7
 
8
8
  ```rb
9
9
  plugin :metadata_attributes, :size => :size, :mime_type => :type
10
+
10
11
  # or
12
+
11
13
  plugin :metadata_attributes
12
14
  Attacher.metadata_attributes :size => :size, :mime_type => :type
13
15
  ```
@@ -28,19 +30,32 @@ user.avatar_size #=> nil
28
30
  user.avatar_type #=> nil
29
31
  ```
30
32
 
33
+ If you're using the [`entity`][entity] plugin, metadata attributes will be
34
+ added to `Attacher#column_values`:
35
+
36
+ ```rb
37
+ attacher.assign(io)
38
+ attacher.column_values #=>
39
+ # {
40
+ # :image_data => '{ ... }',
41
+ # :image_size => 95724,
42
+ # :image_type => "image/jpeg",
43
+ # }
44
+ ```
45
+
31
46
  If you want to specify the full record attribute name, pass the record
32
47
  attribute name as a string instead of a symbol.
33
48
 
34
49
  ```rb
35
50
  Attacher.metadata_attributes :filename => "original_filename"
36
-
37
- # ...
38
-
51
+ ```
52
+ ```rb
39
53
  photo.image = image
40
54
  photo.original_filename #=> "nature.jpg"
41
55
  ```
42
56
 
43
- If any corresponding metadata attribute doesn't exist on the record, that
44
- metadata sync will be silently skipped.
57
+ Any metadata attributes that were declared but are missing on the record will
58
+ be skipped.
45
59
 
46
60
  [metadata_attributes]: /lib/shrine/plugins/metadata_attributes.rb
61
+ [entity]: /doc/plugins/entity.md#readme
data/doc/plugins/model.md CHANGED
@@ -41,6 +41,17 @@ photo.image.storage_key #=> :cache
41
41
  photo.image_data #=> '{"id":"...","storage":"cache","metadata":{...}}'
42
42
  ```
43
43
 
44
+ #### `#<name>_changed?`
45
+
46
+ Calls `Attacher#changed?` which returns whether the attachment has changed.
47
+
48
+ ```rb
49
+ photo = Photo.new
50
+ photo.image_changed? #=> false
51
+ photo.image = file
52
+ photo.image_changed? #=> true
53
+ ```
54
+
44
55
  #### Disabling caching
45
56
 
46
57
  If you don't want to use temporary storage, you can have `#<name>=` upload
@@ -118,14 +129,23 @@ You can also load an entity into an existing attacher with
118
129
  `Attacher#load_model`.
119
130
 
120
131
  ```rb
121
- photo = Photo.new(image_data: '{"id":"...","storage":"...","metadata":{...}}')
122
- attacher = ImageUploader::Attacher.from_model(photo, :image)
132
+ photo = Photo.new(image_data: '{"id":"...","storage":"...","metadata":{...}}')
123
133
 
124
134
  attacher.file #=> nil
125
135
  attacher.load_model(photo, :image)
126
136
  attacher.file #=> #<ImageUploader::UploadedFile>
127
137
  ```
128
138
 
139
+ Or just `Attacher#set_model` if you don't want to load attachment data:
140
+
141
+ ```rb
142
+ photo = Photo.new(image_data: '{"id":"...","storage":"...","metadata":{...}}')
143
+
144
+ attacher.file #=> nil
145
+ attacher.set_model(photo, :image) # doesn't load attachment data
146
+ attacher.file #=> nil
147
+ ```
148
+
129
149
  ### Writing attachment data
130
150
 
131
151
  The `Attacher#write` method writes attachment data to the `#<name>_data`
@@ -0,0 +1,89 @@
1
+ # Persistence
2
+
3
+ This is an internal plugin that provides uniform persistence interface across
4
+ different persistence plugins (e.g. [`activerecord`][activerecord],
5
+ [`sequel`][sequel]).
6
+
7
+ ## Atomic promotion
8
+
9
+ If you're promoting cached file to permanent storage
10
+ [asynchronously][backgrounding], and want to handle the possibility of
11
+ attachment changing during promotion, you can use `Attacher#atomic_promote`:
12
+
13
+ ```rb
14
+ # in your controller
15
+ attacher.attach_cached(io)
16
+ attacher.cached? #=> true
17
+ ```
18
+ ```rb
19
+ # in a background job
20
+ attacher.atomic_promote # promotes cached file and persists
21
+ attacher.stored? #=> true
22
+ ```
23
+
24
+ After the cached file is uploaded to permanent storage, the record is reloaded
25
+ in order to check whether the attachment hasn't changed, and if it hasn't the
26
+ attachment is persisted. If the attachment has changed,
27
+ `Shrine::AttachmentChanged` exception is raised.
28
+
29
+ If you want to execute code after the attachment change check but before
30
+ persistence, you can pass a block:
31
+
32
+ ```rb
33
+ attacher.atomic_promote do |reloaded_attacher|
34
+ # run code after attachment change check but before persistence
35
+ end
36
+ ```
37
+
38
+ You can pass `:reload` and `:persist` options to change how the record is
39
+ reloaded and pesisted. See the [`atomic_helpers`][atomic_helpers] plugin docs
40
+ for more details.
41
+
42
+ Any other options are forwarded to `Attacher#promote`:
43
+
44
+ ```rb
45
+ attacher.atomic_promote(metadata: true) # re-extract metadata
46
+ ```
47
+
48
+ ## Atomic persistence
49
+
50
+ If you're updating something based on the attached file
51
+ [asynchronously][backgrounding], you might want to handle the possibility of
52
+ the attachment changing in the meanwhile. You can do that with
53
+ `Attacher#atomic_persist`:
54
+
55
+ ```rb
56
+ # in a background job
57
+ attacher.refresh_metadata! # refresh_metadata plugin
58
+ attacher.atomic_persist # persists attachment data
59
+ ```
60
+
61
+ The record is first reloaded in order to check whether the attachment hasn't
62
+ changed, and if it hasn't the attachment is persisted. If the attachment has
63
+ changed, `Shrine::AttachmentChanged` exception is raised.
64
+
65
+ If you want to execute code after the attachment change check but before
66
+ persistence, you can pass a block:
67
+
68
+ ```rb
69
+ attacher.atomic_persist do |reloaded_attacher|
70
+ # run code after attachment change check but before persistence
71
+ end
72
+ ```
73
+
74
+ You can pass `:reload` and `:persist` options to change how the record is
75
+ reloaded and pesisted. See the [`atomic_helpers`][atomic_helpers] plugin docs
76
+ for more details.
77
+
78
+ ## Simple Persistence
79
+
80
+ To simply save attachment changes to the underlying record, use
81
+ `Attacher#persist`:
82
+
83
+ ```rb
84
+ attacher.attach(io)
85
+ attacher.persist # saves the underlying record
86
+ ```
87
+
88
+ [activerecord]: /doc/plugins/activerecord.md#readme
89
+ [sequel]: /doc/plugins/sequel.md#readme