shrine 3.0.0.beta → 3.0.0.beta2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +4 -3
- data/doc/plugins/activerecord.md +55 -11
- data/doc/plugins/derivatives.md +23 -18
- data/doc/plugins/entity.md +24 -1
- data/doc/plugins/mirroring.md +87 -0
- data/doc/plugins/model.md +38 -12
- data/doc/plugins/sequel.md +56 -17
- data/lib/shrine/plugins/_persistence.rb +28 -4
- data/lib/shrine/plugins/activerecord.rb +15 -18
- data/lib/shrine/plugins/column.rb +15 -15
- data/lib/shrine/plugins/derivatives.rb +2 -2
- data/lib/shrine/plugins/entity.rb +4 -5
- data/lib/shrine/plugins/mirroring.rb +101 -0
- data/lib/shrine/plugins/model.rb +21 -19
- data/lib/shrine/plugins/sequel.rb +21 -29
- data/lib/shrine/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e35bbe08c0a54aca90746357823eea8bd7f65f7ee64a988fe6bc9c3931f70a91
|
4
|
+
data.tar.gz: 1c70a7212c59f13fa435b866d0dad8c81989d7ee41722247d5107edec461047b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 784c4f79052ef3132ffe26544a79de6f27e9209563cd9c08d475a3e52e0b7b201041f516caebc00315f802c1c9d18e7bd74b0e8e5977576727dccfefe47d0c3b
|
7
|
+
data.tar.gz: 6788e1946a1062a3f184b87cf016b7104d6c524621853fb38a8c092d245fa244625a16866dde958bf53caa4bbb113b6d0e01fb09c7340b39633360e40b5d4ee4
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## 3.0.0.beta2 (2019-09-11)
|
2
|
+
|
3
|
+
* `column` – Allow `Attacher#load_column` to receive a hash (@janko)
|
4
|
+
|
5
|
+
* `activerecord` – Fix integration not working with JSON columns (@janko)
|
6
|
+
|
7
|
+
* `mirroring` – Add new plugin for replicating uploads and deletes to other storages (@janko)
|
8
|
+
|
9
|
+
* `model` – Change disabling model attachment behaviour from `type: :entity` to `model: false` (@janko)
|
10
|
+
|
11
|
+
* `sequel` – Rename `:callbacks` option to `:hooks` (@janko)
|
12
|
+
|
13
|
+
* `derivatives` – Auto-download `UploadedFile` objects passed to `Attacher#process_derivatives` (@janko)
|
14
|
+
|
1
15
|
## 3.0.0.beta (2019-08-29)
|
2
16
|
|
3
17
|
* `atomic_helpers` – Rename `:data` argument to `:file` in `Attacher.retrieve` (@janko)
|
data/README.md
CHANGED
@@ -119,9 +119,10 @@ class Photo < Sequel::Model # ActiveRecord::Base
|
|
119
119
|
end
|
120
120
|
```
|
121
121
|
|
122
|
-
Let's now add the form fields which will use this virtual attribute
|
123
|
-
(1) a file field for choosing files, and
|
124
|
-
uploaded file in case of validation errors
|
122
|
+
Let's now add the form fields which will use this virtual attribute (which is
|
123
|
+
`image`, NOT `image_data`). We need (1) a file field for choosing files, and
|
124
|
+
(2) a hidden field for retaining the uploaded file in case of validation errors
|
125
|
+
and for potential [direct uploads].
|
125
126
|
|
126
127
|
```rb
|
127
128
|
# with Rails form builder:
|
data/doc/plugins/activerecord.md
CHANGED
@@ -9,15 +9,35 @@ plugin :activerecord
|
|
9
9
|
|
10
10
|
## Attachment
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
Including a `Shrine::Attachment` module into an `ActiveRecord::Base` subclass
|
13
|
+
will:
|
14
|
+
|
15
|
+
* add [model] attachment methods
|
16
|
+
* add [validations](#validations) and [callbacks](#callbacks) to tie attachment
|
17
|
+
process to the record lifecycle
|
15
18
|
|
16
19
|
```rb
|
17
|
-
class Photo < ActiveRecord::Base
|
18
|
-
include ImageUploader::Attachment(:image) # adds callbacks & validations
|
20
|
+
class Photo < ActiveRecord::Base # has `image_data` column
|
21
|
+
include ImageUploader::Attachment(:image) # adds methods, callbacks & validations
|
19
22
|
end
|
20
23
|
```
|
24
|
+
```rb
|
25
|
+
photo = Photo.new
|
26
|
+
|
27
|
+
photo.image = file # cache attachment
|
28
|
+
|
29
|
+
photo.image #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:cache ...>
|
30
|
+
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
|
31
|
+
|
32
|
+
photo.save # persist, promote attachment, then persist again
|
33
|
+
|
34
|
+
photo.image #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
35
|
+
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
|
36
|
+
|
37
|
+
photo.destroy # delete attachment
|
38
|
+
|
39
|
+
photo.image.exists? #=> false
|
40
|
+
```
|
21
41
|
|
22
42
|
### Callbacks
|
23
43
|
|
@@ -69,8 +89,8 @@ module *after* they have all been defined.
|
|
69
89
|
|
70
90
|
#### Skipping Callbacks
|
71
91
|
|
72
|
-
If you don't want the attachment module to add any callbacks to your
|
73
|
-
|
92
|
+
If you don't want the attachment module to add any callbacks to your model, you
|
93
|
+
can set `:callbacks` to `false`:
|
74
94
|
|
75
95
|
```rb
|
76
96
|
plugin :activerecord, callbacks: false
|
@@ -104,7 +124,8 @@ presence validator:
|
|
104
124
|
|
105
125
|
```rb
|
106
126
|
class Photo < ActiveRecord::Base
|
107
|
-
include ImageUploader::Attachment
|
127
|
+
include ImageUploader::Attachment(:image)
|
128
|
+
|
108
129
|
validates_presence_of :image
|
109
130
|
end
|
110
131
|
```
|
@@ -147,10 +168,33 @@ plugin :activerecord, validations: false
|
|
147
168
|
|
148
169
|
## Attacher
|
149
170
|
|
150
|
-
|
151
|
-
|
171
|
+
You can also use `Shrine::Attacher` directly (with or without the
|
172
|
+
`Shrine::Attachment` module):
|
173
|
+
|
174
|
+
```rb
|
175
|
+
class Photo < ActiveRecord::Base # has `image_data` column
|
176
|
+
end
|
177
|
+
```
|
178
|
+
```rb
|
179
|
+
photo = Photo.new
|
180
|
+
attacher = ImageUploader::Attacher.from_model(photo, :image)
|
181
|
+
|
182
|
+
attacher.assign(file) # cache
|
183
|
+
|
184
|
+
attacher.file #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:cache ...>
|
185
|
+
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
|
186
|
+
|
187
|
+
photo.save # persist
|
188
|
+
attacher.finalize # promote
|
189
|
+
photo.save # persist
|
190
|
+
|
191
|
+
attacher.file #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
192
|
+
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
|
193
|
+
```
|
194
|
+
|
195
|
+
### Persistence
|
152
196
|
|
153
|
-
The following persistence methods are added to
|
197
|
+
The following persistence methods are added to `Shrine::Attacher`:
|
154
198
|
|
155
199
|
| Method | Description |
|
156
200
|
| :----- | :---------- |
|
data/doc/plugins/derivatives.md
CHANGED
@@ -111,8 +111,9 @@ photo.image_data #=>
|
|
111
111
|
# }
|
112
112
|
```
|
113
113
|
|
114
|
-
|
115
|
-
`Attacher#create_derivatives
|
114
|
+
The `#<name>_derivatives!` model method delegates to
|
115
|
+
`Attacher#create_derivatives`, which you can use if you're using
|
116
|
+
`Shrine::Attacher` directly:
|
116
117
|
|
117
118
|
```rb
|
118
119
|
attacher.file #=> #<Shrine::UploadedFile @id="original.jpg" @storage_key=:store ...>
|
@@ -127,6 +128,18 @@ attacher.derivatives #=>
|
|
127
128
|
# }
|
128
129
|
```
|
129
130
|
|
131
|
+
By default, the `Attacher#create_derivatives` method downloads the attached
|
132
|
+
file, calls the processor, uploads results to attacher's permanent storage, and
|
133
|
+
saves uploaded files on the attacher.
|
134
|
+
|
135
|
+
Any additional arguments are forwarded to
|
136
|
+
[`Attacher#process_derivatives`](#processing-derivatives):
|
137
|
+
|
138
|
+
```rb
|
139
|
+
attacher.create_derivatives(:thumbnails, different_source) # pass a different source file
|
140
|
+
attacher.create_derivatives(:thumbnails, foo: "bar") # pass custom options to the processor
|
141
|
+
```
|
142
|
+
|
130
143
|
### Derivatives storage
|
131
144
|
|
132
145
|
By default, derivatives are uploaded to the permanent storage of the attacher.
|
@@ -308,15 +321,8 @@ Attacher.derivatives_processor :my_processor do |original|
|
|
308
321
|
end
|
309
322
|
```
|
310
323
|
|
311
|
-
The
|
312
|
-
|
313
|
-
|
314
|
-
```rb
|
315
|
-
attacher.create_derivatives(:my_processor)
|
316
|
-
```
|
317
|
-
|
318
|
-
Internally this calls `Attacher#process_derivatives`, which calls the
|
319
|
-
processor and returns processed files:
|
324
|
+
The `Attacher#create_derivatives` method internally calls
|
325
|
+
`Attacher#process_derivatives`, which in turn calls the processor:
|
320
326
|
|
321
327
|
```rb
|
322
328
|
files = attacher.process_derivatives(:my_processor)
|
@@ -340,8 +346,8 @@ Attacher.derivatives_processor :my_processor do |original|
|
|
340
346
|
end
|
341
347
|
```
|
342
348
|
|
343
|
-
Moreover, any options passed to `Attacher#process_derivatives`
|
344
|
-
|
349
|
+
Moreover, any options passed to `Attacher#process_derivatives` will be
|
350
|
+
forwarded to the processor:
|
345
351
|
|
346
352
|
```rb
|
347
353
|
attacher.process_derivatives(:my_processor, foo: "bar")
|
@@ -355,7 +361,7 @@ end
|
|
355
361
|
|
356
362
|
### Source file
|
357
363
|
|
358
|
-
|
364
|
+
By default, the `Attacher#process_derivatives` method will download the
|
359
365
|
attached file and pass it to the processor:
|
360
366
|
|
361
367
|
```rb
|
@@ -368,10 +374,9 @@ end
|
|
368
374
|
attacher.process_derivatives(:my_processor) # downloads attached file and passes it to the processor
|
369
375
|
```
|
370
376
|
|
371
|
-
If you
|
372
|
-
processors in a row and want to avoid downloading the same source file each
|
373
|
-
time, you can pass the source file as the second argument
|
374
|
-
`Attacher#process_derivatives` (or `Attacher#create_derivatives`):
|
377
|
+
If you want to use a different source file, or if you're calling multiple
|
378
|
+
processors in a row and want to avoid re-downloading the same source file each
|
379
|
+
time, you can pass the source file as the second argument:
|
375
380
|
|
376
381
|
```rb
|
377
382
|
# this way the source file is downloaded only once
|
data/doc/plugins/entity.md
CHANGED
@@ -20,7 +20,7 @@ These methods read attachment data from the `#<name>_data` attribute on the
|
|
20
20
|
entity instance.
|
21
21
|
|
22
22
|
```rb
|
23
|
-
class Photo < Entity(:image_data)
|
23
|
+
class Photo < Entity(:image_data) # has `image_data` reader
|
24
24
|
include ImageUploader::Attachment(:image)
|
25
25
|
end
|
26
26
|
```
|
@@ -117,6 +117,29 @@ attacher.store_key #=> :other_store
|
|
117
117
|
|
118
118
|
## Attacher
|
119
119
|
|
120
|
+
You can also use `Shrine::Attacher` directly (with or without the
|
121
|
+
`Shrine::Attachment` module):
|
122
|
+
|
123
|
+
```rb
|
124
|
+
class Photo < Entity(:image_data) # has `image_data` reader
|
125
|
+
end
|
126
|
+
```
|
127
|
+
```rb
|
128
|
+
photo = Photo.new(image_data: '{"id":"...","storage":"...","metadata":{...}}')
|
129
|
+
attacher = ImageUploader::Attacher.from_entity(photo, :image)
|
130
|
+
|
131
|
+
attacher.file #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:store ...>
|
132
|
+
|
133
|
+
attacher.attach(file)
|
134
|
+
attacher.file #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
135
|
+
attacher.column_values #=> { image_data: '{"id":"397eca.jpg","storage":"store","metadata":{...}}' }
|
136
|
+
|
137
|
+
photo = Photo.new(attacher.column_values)
|
138
|
+
attacher = ImageUploader::Attacher.from_entity(photo, :image)
|
139
|
+
|
140
|
+
attacher.file #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
141
|
+
```
|
142
|
+
|
120
143
|
### Loading entity
|
121
144
|
|
122
145
|
The `Attacher.from_entity` method can be used for creating an `Attacher`
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Mirroring
|
2
|
+
|
3
|
+
The [`mirroring`][mirroring] plugin enables replicating uploads and deletes to
|
4
|
+
other storages. This can be useful for setting up a backup storage, or when
|
5
|
+
migrating files from one storage to another.
|
6
|
+
|
7
|
+
```rb
|
8
|
+
Shrine.plugin :mirroring, mirror: { store: :other_store }
|
9
|
+
```
|
10
|
+
|
11
|
+
With the above setup, any upload and delete to `:store` will be replicated to
|
12
|
+
`:other_store`.
|
13
|
+
|
14
|
+
```rb
|
15
|
+
uploaded_file = Shrine.upload(io, :store) # uploads to :store and :other_store
|
16
|
+
uploaded_file.delete # deletes from :store and :other_store
|
17
|
+
```
|
18
|
+
|
19
|
+
## Multiple storages
|
20
|
+
|
21
|
+
You can mirror to multiple storages by specifying an array:
|
22
|
+
|
23
|
+
```rb
|
24
|
+
Shrine.plugin :mirroring, mirror: {
|
25
|
+
store: [:other_store_1, :other_store_2]
|
26
|
+
}
|
27
|
+
```
|
28
|
+
|
29
|
+
## Backup storage
|
30
|
+
|
31
|
+
If you want the mirror storage to act as a backup, you can disable mirroring
|
32
|
+
deletes:
|
33
|
+
|
34
|
+
```rb
|
35
|
+
Shrine.plugin :mirroring, mirror: { ... }, delete: false
|
36
|
+
```
|
37
|
+
|
38
|
+
## Backgrounding
|
39
|
+
|
40
|
+
You can have mirroring performed in a background job:
|
41
|
+
|
42
|
+
```rb
|
43
|
+
Shrine.mirror_upload do |uploaded_file|
|
44
|
+
MirrorUploadJob.perform_async(uploaded_file.shrine_class, uploaded_file.data)
|
45
|
+
end
|
46
|
+
|
47
|
+
Shrine.mirror_delete do |uploaded_file|
|
48
|
+
MirrorDeleteJob.perform_async(uploaded_file.shrine_class, uploaded_file.data)
|
49
|
+
end
|
50
|
+
```
|
51
|
+
```rb
|
52
|
+
class MirrorUploadJob
|
53
|
+
include Sidekiq::Worker
|
54
|
+
def perform(shrine_class, file_data)
|
55
|
+
uploaded_file = Object.const_get(shrine_class).uploaded_file(file_data)
|
56
|
+
uploaded_file.mirror_upload
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
```rb
|
61
|
+
class MirrorDeleteJob
|
62
|
+
include Sidekiq::Worker
|
63
|
+
def perform(shrine_class, file_data)
|
64
|
+
uploaded_file = Object.const_get(shrine_class).uploaded_file(file_data)
|
65
|
+
uploaded_file.mirror_delete
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## API
|
71
|
+
|
72
|
+
You can mirror manually via `UploadedFile#mirror_upload` and
|
73
|
+
`UploadedFile#mirror_delete`:
|
74
|
+
|
75
|
+
```rb
|
76
|
+
# disable automatic mirroring of uploads and deletes
|
77
|
+
Shrine.plugin :mirroring, mirror: { ... }, upload: false, delete: false
|
78
|
+
```
|
79
|
+
```rb
|
80
|
+
file = Shrine.upload(io, :store) # upload to :store
|
81
|
+
file.mirror_upload # upload to :other_store
|
82
|
+
|
83
|
+
file.delete # delete from :store
|
84
|
+
file.mirror_delete # delete from :other_store
|
85
|
+
```
|
86
|
+
|
87
|
+
[mirroring]: /lib/shrine/plugins/mirroring.rb
|
data/doc/plugins/model.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Model
|
2
2
|
|
3
|
-
The [`model`][model] plugin provides integration for handling
|
3
|
+
The [`model`][model] plugin provides integration for handling attachment on
|
4
4
|
mutable structs. It is built on top of the [`entity`][entity] plugin.
|
5
5
|
|
6
6
|
```rb
|
@@ -9,24 +9,28 @@ plugin :model
|
|
9
9
|
|
10
10
|
## Attachment
|
11
11
|
|
12
|
-
Including a `Shrine::Attachment` module into a model class will
|
13
|
-
methods from the `entity` plugin, add the `#<name>=` method for attaching
|
14
|
-
files.
|
12
|
+
Including a `Shrine::Attachment` module into a model class will:
|
15
13
|
|
16
|
-
|
17
|
-
|
14
|
+
* add [entity] attachment methods
|
15
|
+
* add `#<name>=` and `#<name>_changed?` methods
|
18
16
|
|
19
17
|
```rb
|
20
|
-
class Photo < Model(:image_data)
|
18
|
+
class Photo < Model(:image_data) # has `image_data` accessor
|
21
19
|
include ImageUploader::Attachment(:image)
|
22
20
|
end
|
23
21
|
```
|
24
22
|
```rb
|
25
23
|
photo = Photo.new
|
24
|
+
|
26
25
|
photo.image = file
|
27
|
-
|
28
|
-
photo.
|
29
|
-
photo.
|
26
|
+
|
27
|
+
photo.image #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:cache ...>
|
28
|
+
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
|
29
|
+
|
30
|
+
photo.image_attacher.finalize
|
31
|
+
|
32
|
+
photo.image #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
33
|
+
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
|
30
34
|
```
|
31
35
|
|
32
36
|
#### `#<name>=`
|
@@ -98,16 +102,38 @@ photo.image_attacher.cache_key #=> :other_cache
|
|
98
102
|
### Entity
|
99
103
|
|
100
104
|
If you still want to include `Shrine::Attachment` modules to immutable
|
101
|
-
entities, you can disable "model" behaviour by passing `
|
105
|
+
entities, you can disable "model" behaviour by passing `model: false`:
|
102
106
|
|
103
107
|
```rb
|
104
108
|
class Photo < Entity(:image_data)
|
105
|
-
include ImageUploader::Attachment(:image,
|
109
|
+
include ImageUploader::Attachment(:image, model: false)
|
106
110
|
end
|
107
111
|
```
|
108
112
|
|
109
113
|
## Attacher
|
110
114
|
|
115
|
+
You can also use `Shrine::Attacher` directly (with or without the
|
116
|
+
`Shrine::Attachment` module):
|
117
|
+
|
118
|
+
```rb
|
119
|
+
class Photo < Model(:image_data) # has `image_data` accessor
|
120
|
+
end
|
121
|
+
```
|
122
|
+
```rb
|
123
|
+
photo = Photo.new
|
124
|
+
attacher = ImageUploader::Attacher.from_model(photo, :image)
|
125
|
+
|
126
|
+
attacher.assign(file) # cache
|
127
|
+
|
128
|
+
attacher.file #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:cache ...>
|
129
|
+
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
|
130
|
+
|
131
|
+
attacher.finalize # promote
|
132
|
+
|
133
|
+
attacher.file #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
134
|
+
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
|
135
|
+
```
|
136
|
+
|
111
137
|
### Loading model
|
112
138
|
|
113
139
|
The `Attacher.from_model` method can be used for creating an `Attacher`
|
data/doc/plugins/sequel.md
CHANGED
@@ -9,17 +9,36 @@ plugin :sequel
|
|
9
9
|
|
10
10
|
## Attachment
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
Including a `Shrine::Attachment` module into a `Sequel::Model` subclass will:
|
13
|
+
|
14
|
+
* add [model] attachment methods
|
15
|
+
* add [validations](#validations) and [hooks](#hooks) to tie attachment process
|
16
|
+
to the record lifecycle
|
15
17
|
|
16
18
|
```rb
|
17
|
-
class Photo < Sequel::Model
|
18
|
-
include ImageUploader::Attachment(:image) # adds callbacks & validations
|
19
|
+
class Photo < Sequel::Model # has `image_data` column
|
20
|
+
include ImageUploader::Attachment(:image) # adds methods, callbacks & validations
|
19
21
|
end
|
20
22
|
```
|
23
|
+
```rb
|
24
|
+
photo = Photo.new
|
25
|
+
|
26
|
+
photo.image = file # cache attachment
|
27
|
+
|
28
|
+
photo.image #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:cache ...>
|
29
|
+
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
|
30
|
+
|
31
|
+
photo.save # persist, promote attachment, then persist again
|
21
32
|
|
22
|
-
|
33
|
+
photo.image #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
34
|
+
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
|
35
|
+
|
36
|
+
photo.destroy # delete attachment
|
37
|
+
|
38
|
+
photo.image.exists? #=> false
|
39
|
+
```
|
40
|
+
|
41
|
+
### Hooks
|
23
42
|
|
24
43
|
#### After Save
|
25
44
|
|
@@ -52,13 +71,13 @@ photo.destroy
|
|
52
71
|
photo.image.exists? #=> false
|
53
72
|
```
|
54
73
|
|
55
|
-
#### Skipping
|
74
|
+
#### Skipping Hooks
|
56
75
|
|
57
|
-
If you don't want the attachment module to add any
|
58
|
-
|
76
|
+
If you don't want the attachment module to add any hooks to your model, you can
|
77
|
+
set `:hooks` to `false`:
|
59
78
|
|
60
79
|
```rb
|
61
|
-
plugin :sequel,
|
80
|
+
plugin :sequel, hooks: false
|
62
81
|
```
|
63
82
|
|
64
83
|
### Validations
|
@@ -89,9 +108,7 @@ presence validator:
|
|
89
108
|
|
90
109
|
```rb
|
91
110
|
class Photo < Sequel::Model
|
92
|
-
include ImageUploader::Attachment
|
93
|
-
|
94
|
-
plugin :validation_helpers
|
111
|
+
include ImageUploader::Attachment(:image)
|
95
112
|
|
96
113
|
def validate
|
97
114
|
super
|
@@ -111,10 +128,33 @@ plugin :sequel, validations: false
|
|
111
128
|
|
112
129
|
## Attacher
|
113
130
|
|
114
|
-
|
115
|
-
|
131
|
+
You can also use `Shrine::Attacher` directly (with or without the
|
132
|
+
`Shrine::Attachment` module):
|
133
|
+
|
134
|
+
```rb
|
135
|
+
class Photo < Sequel::Model # has `image_data` column
|
136
|
+
end
|
137
|
+
```
|
138
|
+
```rb
|
139
|
+
photo = Photo.new
|
140
|
+
attacher = ImageUploader::Attacher.from_model(photo, :image)
|
141
|
+
|
142
|
+
attacher.assign(file) # cache
|
143
|
+
|
144
|
+
attacher.file #=> #<Shrine::UploadedFile @id="bc2e13.jpg" @storage_key=:cache ...>
|
145
|
+
photo.image_data #=> '{"id":"bc2e13.jpg","storage":"cache","metadata":{...}}'
|
146
|
+
|
147
|
+
photo.save # persist
|
148
|
+
attacher.finalize # promote
|
149
|
+
photo.save # persist
|
150
|
+
|
151
|
+
attacher.file #=> #<Shrine::UploadedFile @id="397eca.jpg" @storage_key=:store ...>
|
152
|
+
photo.image_data #=> '{"id":"397eca.jpg","storage":"store","metadata":{...}}'
|
153
|
+
```
|
154
|
+
|
155
|
+
### Pesistence
|
116
156
|
|
117
|
-
The following persistence methods are added to
|
157
|
+
The following persistence methods are added to `Shrine::Attacher`:
|
118
158
|
|
119
159
|
| Method | Description |
|
120
160
|
| :----- | :---------- |
|
@@ -127,6 +167,5 @@ See [persistence] docs for more details.
|
|
127
167
|
[sequel]: /lib/shrine/plugins/sequel.rb
|
128
168
|
[Sequel]: https://sequel.jeremyevans.net/
|
129
169
|
[model]: /doc/plugins/model.md#readme
|
130
|
-
[hooks]: http://sequel.jeremyevans.net/rdoc/files/doc/model_hooks_rdoc.html
|
131
170
|
[validation]: /doc/plugins/validation.md#readme
|
132
171
|
[persistence]: /doc/plugins/persistence.md#readme
|
@@ -9,13 +9,15 @@ class Shrine
|
|
9
9
|
module Persistence
|
10
10
|
def self.load_dependencies(uploader, *)
|
11
11
|
uploader.plugin :atomic_helpers
|
12
|
+
uploader.plugin :entity
|
12
13
|
end
|
13
14
|
|
14
|
-
#
|
15
|
+
# Using #<name>_persist, #<name>_reload, and #<name>?, defines the
|
16
|
+
# following methods for a persistence plugin:
|
15
17
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
18
|
+
# * Attacher#persist
|
19
|
+
# * Attacher#atomic_persist
|
20
|
+
# * Attacher#atomic_promote
|
19
21
|
def self.configure(uploader, plugin:)
|
20
22
|
plugin_name = plugin.to_s.split("::").last.downcase
|
21
23
|
|
@@ -46,6 +48,14 @@ class Shrine
|
|
46
48
|
|
47
49
|
send(:"#{plugin_name}_persist")
|
48
50
|
end
|
51
|
+
|
52
|
+
define_method :hash_attribute? do
|
53
|
+
return super() unless send(:"#{plugin_name}?")
|
54
|
+
|
55
|
+
respond_to?(:"#{plugin_name}_hash_attribute?", true) &&
|
56
|
+
send(:"#{plugin_name}_hash_attribute?")
|
57
|
+
end
|
58
|
+
private :hash_attribute?
|
49
59
|
end
|
50
60
|
end
|
51
61
|
|
@@ -61,6 +71,20 @@ class Shrine
|
|
61
71
|
def persist(*)
|
62
72
|
raise NotImplementedError, "unhandled by a persistence plugin"
|
63
73
|
end
|
74
|
+
|
75
|
+
# Disable attachment data serialization for data attributes that
|
76
|
+
# accept and return hashes.
|
77
|
+
def set_entity(*)
|
78
|
+
super
|
79
|
+
@column_serializer = nil if hash_attribute?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Whether the data attribute accepts and returns hashes.
|
85
|
+
def hash_attribute?
|
86
|
+
false
|
87
|
+
end
|
64
88
|
end
|
65
89
|
end
|
66
90
|
|
@@ -67,27 +67,16 @@ class Shrine
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
+
# The _persistence plugin uses #activerecord_persist,
|
71
|
+
# #activerecord_reload and #activerecord? to implement the following
|
72
|
+
# methods:
|
73
|
+
#
|
74
|
+
# * Attacher#persist
|
75
|
+
# * Attacher#atomic_persist
|
76
|
+
# * Attacher#atomic_promote
|
70
77
|
module AttacherMethods
|
71
|
-
# The _persistence plugin defines the following methods:
|
72
|
-
#
|
73
|
-
# * #persist (calls #activerecord_persist and #activerecord?)
|
74
|
-
# * #atomic_persist (calls #activerecord_lock, #activerecord_persist and #activerecord?)
|
75
|
-
# * #atomic_promote (calls #activerecord_lock, #activerecord_persist and #activerecord?)
|
76
78
|
private
|
77
79
|
|
78
|
-
# ActiveRecord JSON column attribute needs to be assigned with a Hash.
|
79
|
-
def serialize_column(data)
|
80
|
-
activerecord_json_column? ? data : super
|
81
|
-
end
|
82
|
-
|
83
|
-
# Returns true if the data attribute represents a JSON or JSONB column.
|
84
|
-
def activerecord_json_column?
|
85
|
-
return false unless activerecord?
|
86
|
-
return false unless column = record.class.columns_hash[attribute.to_s]
|
87
|
-
|
88
|
-
[:json, :jsonb].include?(column.type)
|
89
|
-
end
|
90
|
-
|
91
80
|
# Saves changes to the model instance, skipping validations. Used by
|
92
81
|
# the _persistence plugin.
|
93
82
|
def activerecord_persist
|
@@ -100,6 +89,14 @@ class Shrine
|
|
100
89
|
record.transaction { yield record.clone.reload(lock: true) }
|
101
90
|
end
|
102
91
|
|
92
|
+
# Returns true if the data attribute represents a JSON or JSONB column.
|
93
|
+
# Used by the _persistence plugin to determine whether serialization
|
94
|
+
# should be skipped.
|
95
|
+
def activerecord_hash_attribute?
|
96
|
+
column = record.class.columns_hash[attribute.to_s]
|
97
|
+
column && [:json, :jsonb].include?(column.type)
|
98
|
+
end
|
99
|
+
|
103
100
|
# Returns whether the record is an ActiveRecord model. Used by the
|
104
101
|
# _persistence plugin.
|
105
102
|
def activerecord?
|
@@ -9,7 +9,7 @@ class Shrine
|
|
9
9
|
# [doc/plugins/column.md]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/column.md
|
10
10
|
module Column
|
11
11
|
def self.configure(uploader, **opts)
|
12
|
-
uploader.opts[:column] ||= { serializer: JsonSerializer
|
12
|
+
uploader.opts[:column] ||= { serializer: JsonSerializer }
|
13
13
|
uploader.opts[:column].merge!(opts)
|
14
14
|
end
|
15
15
|
|
@@ -62,9 +62,11 @@ class Shrine
|
|
62
62
|
# Attacher.serialize_column(nil)
|
63
63
|
# #=> nil
|
64
64
|
def serialize_column(data)
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
if column_serializer && data
|
66
|
+
column_serializer.dump(data)
|
67
|
+
else
|
68
|
+
data
|
69
|
+
end
|
68
70
|
end
|
69
71
|
|
70
72
|
# Converts the column data string into a hash (parses JSON by default).
|
@@ -75,9 +77,11 @@ class Shrine
|
|
75
77
|
# Attacher.deserialize_column(nil)
|
76
78
|
# #=> nil
|
77
79
|
def deserialize_column(data)
|
78
|
-
|
79
|
-
|
80
|
-
|
80
|
+
if column_serializer && data.is_a?(String)
|
81
|
+
column_serializer.load(data)
|
82
|
+
else
|
83
|
+
data&.to_hash
|
84
|
+
end
|
81
85
|
end
|
82
86
|
end
|
83
87
|
|
@@ -85,16 +89,12 @@ class Shrine
|
|
85
89
|
# create this wrapper class which calls JSON.generate and JSON.parse
|
86
90
|
# instead.
|
87
91
|
class JsonSerializer
|
88
|
-
def
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
def dump(data)
|
93
|
-
@json.generate(data)
|
92
|
+
def self.dump(data)
|
93
|
+
JSON.generate(data)
|
94
94
|
end
|
95
95
|
|
96
|
-
def load(data)
|
97
|
-
|
96
|
+
def self.load(data)
|
97
|
+
JSON.parse(data)
|
98
98
|
end
|
99
99
|
end
|
100
100
|
end
|
@@ -252,9 +252,9 @@ class Shrine
|
|
252
252
|
#
|
253
253
|
# attacher.process_derivatives(:thumbnails)
|
254
254
|
# #=> { small: #<File:...>, medium: #<File:...>, large: #<File:...> }
|
255
|
-
def process_derivatives(processor_name, source =
|
255
|
+
def process_derivatives(processor_name, source = file!, **options)
|
256
256
|
processor = self.class.derivatives_processor(processor_name)
|
257
|
-
fetch_source = source ? source.method(:
|
257
|
+
fetch_source = source.is_a?(UploadedFile) ? source.method(:download) : source.method(:tap)
|
258
258
|
result = nil
|
259
259
|
|
260
260
|
fetch_source.call do |source_file|
|
@@ -11,15 +11,14 @@ class Shrine
|
|
11
11
|
end
|
12
12
|
|
13
13
|
module AttachmentMethods
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(name, type: :entity, **options)
|
14
|
+
def initialize(name, **options)
|
17
15
|
super(name, **options)
|
18
|
-
@type = type
|
19
16
|
|
20
17
|
define_entity_methods(name)
|
21
18
|
end
|
22
19
|
|
20
|
+
private
|
21
|
+
|
23
22
|
# Defines `#<name>`, `#<name>_url`, and `#<name>_attacher` methods.
|
24
23
|
def define_entity_methods(name)
|
25
24
|
super if defined?(super)
|
@@ -38,7 +37,7 @@ class Shrine
|
|
38
37
|
|
39
38
|
# Returns an attacher instance.
|
40
39
|
define_method :"#{name}_attacher" do |**options|
|
41
|
-
attachment.attacher
|
40
|
+
attachment.send(:attacher, self, options)
|
42
41
|
end
|
43
42
|
end
|
44
43
|
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Shrine
|
4
|
+
module Plugins
|
5
|
+
module Mirroring
|
6
|
+
UPLOAD = -> (uploaded_file) { uploaded_file.mirror_upload }
|
7
|
+
DELETE = -> (uploaded_file) { uploaded_file.mirror_delete }
|
8
|
+
|
9
|
+
def self.configure(uploader, **opts)
|
10
|
+
uploader.opts[:mirroring] ||= { upload: UPLOAD, delete: DELETE }
|
11
|
+
uploader.opts[:mirroring].merge!(opts)
|
12
|
+
|
13
|
+
fail Error, ":mirror option is required for mirroring plugin" unless uploader.opts[:mirroring][:mirror]
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def mirrors(storage_key = nil)
|
18
|
+
if storage_key
|
19
|
+
mirrors = opts[:mirroring][:mirror][storage_key]
|
20
|
+
|
21
|
+
fail Error, "no mirrors registered for storage #{storage_key.inspect}" unless mirrors
|
22
|
+
|
23
|
+
Array(mirrors)
|
24
|
+
else
|
25
|
+
opts[:mirroring][:mirror]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def mirror_upload(&block)
|
30
|
+
if block
|
31
|
+
opts[:mirroring][:upload] = block
|
32
|
+
else
|
33
|
+
opts[:mirroring][:upload]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def mirror_delete(&block)
|
38
|
+
if block
|
39
|
+
opts[:mirroring][:delete] = block
|
40
|
+
else
|
41
|
+
opts[:mirroring][:delete]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module InstanceMethods
|
47
|
+
def upload(io, **options)
|
48
|
+
uploaded_file = super
|
49
|
+
|
50
|
+
if self.class.mirrors[storage_key] && self.class.mirror_upload
|
51
|
+
self.class.mirror_upload.call(uploaded_file)
|
52
|
+
end
|
53
|
+
|
54
|
+
uploaded_file
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module FileMethods
|
59
|
+
def mirror_upload
|
60
|
+
previously_opened = opened?
|
61
|
+
|
62
|
+
each_mirror do |mirror|
|
63
|
+
rewind if opened?
|
64
|
+
|
65
|
+
shrine_class.upload(self, mirror, location: id, close: false)
|
66
|
+
end
|
67
|
+
ensure
|
68
|
+
if opened? && !previously_opened
|
69
|
+
close
|
70
|
+
@io = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def delete
|
75
|
+
result = super
|
76
|
+
|
77
|
+
if shrine_class.mirrors[storage_key] && shrine_class.mirror_delete
|
78
|
+
shrine_class.mirror_delete.call(self)
|
79
|
+
end
|
80
|
+
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
def mirror_delete
|
85
|
+
each_mirror do |mirror|
|
86
|
+
self.class.new(id: id, storage: mirror).delete
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def each_mirror(&block)
|
93
|
+
mirrors = shrine_class.mirrors(storage_key)
|
94
|
+
mirrors.map(&block)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
register_plugin(:mirroring, Mirroring)
|
100
|
+
end
|
101
|
+
end
|
data/lib/shrine/plugins/model.rb
CHANGED
@@ -16,28 +16,25 @@ class Shrine
|
|
16
16
|
end
|
17
17
|
|
18
18
|
module AttachmentMethods
|
19
|
-
# Allows
|
20
|
-
# (default) or an entity.
|
19
|
+
# Allows disabling model behaviour:
|
21
20
|
#
|
22
|
-
# Shrine::Attachment.new(:image)
|
23
|
-
# Shrine::Attachment.new(:image,
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
# Shrine::Attachment.new(:image) # model (default)
|
22
|
+
# Shrine::Attachment.new(:image, model: false) # entity
|
23
|
+
def initialize(name, model: true, **options)
|
24
|
+
super(name, **options)
|
25
|
+
@model = model
|
27
26
|
end
|
28
27
|
|
29
|
-
# We define model methods only on inclusion
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# defining model methods in this case.
|
28
|
+
# We define model methods only on inclusion. This gives other plugins
|
29
|
+
# the ability to disable model behaviour for entity classes. In this
|
30
|
+
# case we want to skip defining model methods as well.
|
33
31
|
def included(klass)
|
34
32
|
super
|
35
|
-
|
36
|
-
return unless type == :model
|
37
|
-
|
38
|
-
define_model_methods(@name)
|
33
|
+
define_model_methods(@name) if model?
|
39
34
|
end
|
40
35
|
|
36
|
+
private
|
37
|
+
|
41
38
|
# Defines attachment setter and enhances the copy constructor.
|
42
39
|
def define_model_methods(name)
|
43
40
|
super if defined?(super)
|
@@ -61,17 +58,21 @@ class Shrine
|
|
61
58
|
|
62
59
|
# Memoizes the attacher instance into an instance variable.
|
63
60
|
def attacher(record, options)
|
64
|
-
return super unless
|
61
|
+
return super unless model?
|
65
62
|
|
66
63
|
if !record.instance_variable_get(:"@#{@name}_attacher") || options.any?
|
67
|
-
attacher =
|
68
|
-
attacher.
|
64
|
+
attacher = record.class.send(:"#{@name}_attacher", options)
|
65
|
+
attacher.load_model(record, @name)
|
69
66
|
|
70
67
|
record.instance_variable_set(:"@#{@name}_attacher", attacher)
|
71
68
|
else
|
72
69
|
record.instance_variable_get(:"@#{@name}_attacher")
|
73
70
|
end
|
74
71
|
end
|
72
|
+
|
73
|
+
def model?
|
74
|
+
@model
|
75
|
+
end
|
75
76
|
end
|
76
77
|
|
77
78
|
module AttacherClassMethods
|
@@ -92,6 +93,7 @@ class Shrine
|
|
92
93
|
def initialize(model_cache: shrine_class.opts[:model][:cache], **options)
|
93
94
|
super(**options)
|
94
95
|
@model_cache = model_cache
|
96
|
+
@model = nil
|
95
97
|
end
|
96
98
|
|
97
99
|
# Saves record and name and initializes attachment from the model
|
@@ -148,7 +150,7 @@ class Shrine
|
|
148
150
|
# This allows users to still use the attacher with an entity instance
|
149
151
|
# or without any record instance.
|
150
152
|
def model?
|
151
|
-
|
153
|
+
@model
|
152
154
|
end
|
153
155
|
end
|
154
156
|
end
|
@@ -14,7 +14,12 @@ class Shrine
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.configure(uploader, **opts)
|
17
|
-
|
17
|
+
if opts.key?(:callbacks)
|
18
|
+
Shrine.deprecation("The :callbacks option in sequel plugin has been renamed to :hooks. The :callbacks alias will be removed in Shrine 4.")
|
19
|
+
opts[:hooks] = opts.delete(:callbacks)
|
20
|
+
end
|
21
|
+
|
22
|
+
uploader.opts[:sequel] ||= { hooks: true, validations: true }
|
18
23
|
uploader.opts[:sequel].merge!(opts)
|
19
24
|
end
|
20
25
|
|
@@ -38,7 +43,7 @@ class Shrine
|
|
38
43
|
end
|
39
44
|
end
|
40
45
|
|
41
|
-
if shrine_class.opts[:sequel][:
|
46
|
+
if shrine_class.opts[:sequel][:hooks]
|
42
47
|
define_method :before_save do
|
43
48
|
super()
|
44
49
|
if send(:"#{name}_attacher").changed?
|
@@ -76,36 +81,15 @@ class Shrine
|
|
76
81
|
end
|
77
82
|
end
|
78
83
|
|
84
|
+
# The _persistence plugin uses #sequel_persist, #sequel_reload and
|
85
|
+
# #sequel? to implement the following methods:
|
86
|
+
#
|
87
|
+
# * Attacher#persist
|
88
|
+
# * Attacher#atomic_persist
|
89
|
+
# * Attacher#atomic_promote
|
79
90
|
module AttacherMethods
|
80
|
-
# The _persistence plugin defines the following methods:
|
81
|
-
#
|
82
|
-
# * #persist (calls #sequel_persist and #sequel?)
|
83
|
-
# * #atomic_persist (calls #sequel_lock, #sequel_persist and #sequel?)
|
84
|
-
# * #atomic_promote (calls #sequel_lock, #sequel_persist and #sequel?)
|
85
91
|
private
|
86
92
|
|
87
|
-
# Sequel JSON column attribute with `pg_json` Sequel extension loaded
|
88
|
-
# returns a `Sequel::Postgres::JSONHashBase` object will be returned,
|
89
|
-
# which we convert into a Hash.
|
90
|
-
def deserialize_column(data)
|
91
|
-
sequel_json_column? ? data&.to_hash : super
|
92
|
-
end
|
93
|
-
|
94
|
-
# Sequel JSON column attribute with `pg_json` Sequel extension loaded
|
95
|
-
# can receive a Hash object, so there is no need to generate a JSON
|
96
|
-
# string.
|
97
|
-
def serialize_column(data)
|
98
|
-
sequel_json_column? ? data : super
|
99
|
-
end
|
100
|
-
|
101
|
-
# Returns true if the data attribute represents a JSON or JSONB column.
|
102
|
-
def sequel_json_column?
|
103
|
-
return false unless sequel?
|
104
|
-
return false unless column = record.class.db_schema[attribute]
|
105
|
-
|
106
|
-
[:json, :jsonb].include?(column[:type])
|
107
|
-
end
|
108
|
-
|
109
93
|
# Saves changes to the model instance, skipping validations. Used by
|
110
94
|
# the _persistence plugin.
|
111
95
|
def sequel_persist
|
@@ -118,6 +102,14 @@ class Shrine
|
|
118
102
|
record.db.transaction { yield record.dup.lock! }
|
119
103
|
end
|
120
104
|
|
105
|
+
# Returns true if the data attribute represents a JSON or JSONB column.
|
106
|
+
# Used by the _persistence plugin to determine whether serialization
|
107
|
+
# should be skipped.
|
108
|
+
def sequel_hash_attribute?
|
109
|
+
column = record.class.db_schema[attribute]
|
110
|
+
column && [:json, :jsonb].include?(column[:type])
|
111
|
+
end
|
112
|
+
|
121
113
|
# Returns whether the record is a Sequel model. Used by the
|
122
114
|
# _persistence plugin.
|
123
115
|
def sequel?
|
data/lib/shrine/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shrine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.0.
|
4
|
+
version: 3.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|
@@ -398,6 +398,7 @@ files:
|
|
398
398
|
- doc/plugins/instrumentation.md
|
399
399
|
- doc/plugins/keep_files.md
|
400
400
|
- doc/plugins/metadata_attributes.md
|
401
|
+
- doc/plugins/mirroring.md
|
401
402
|
- doc/plugins/model.md
|
402
403
|
- doc/plugins/module_include.md
|
403
404
|
- doc/plugins/persistence.md
|
@@ -493,6 +494,7 @@ files:
|
|
493
494
|
- lib/shrine/plugins/instrumentation.rb
|
494
495
|
- lib/shrine/plugins/keep_files.rb
|
495
496
|
- lib/shrine/plugins/metadata_attributes.rb
|
497
|
+
- lib/shrine/plugins/mirroring.rb
|
496
498
|
- lib/shrine/plugins/model.rb
|
497
499
|
- lib/shrine/plugins/module_include.rb
|
498
500
|
- lib/shrine/plugins/presign_endpoint.rb
|
@@ -547,7 +549,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
547
549
|
- !ruby/object:Gem::Version
|
548
550
|
version: 1.3.1
|
549
551
|
requirements: []
|
550
|
-
rubygems_version: 3.0.
|
552
|
+
rubygems_version: 3.0.1
|
551
553
|
signing_key:
|
552
554
|
specification_version: 4
|
553
555
|
summary: Toolkit for file attachments in Ruby applications
|