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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5f2d3f6519d99e728cdfffe80783d0278f171bb6f8a850b4840d2d9fb92eda5
4
- data.tar.gz: 28566d4950a3ad23cb657e699db8a0861283efb8709a7a70d6a20c83d8a5c0c7
3
+ metadata.gz: e35bbe08c0a54aca90746357823eea8bd7f65f7ee64a988fe6bc9c3931f70a91
4
+ data.tar.gz: 1c70a7212c59f13fa435b866d0dad8c81989d7ee41722247d5107edec461047b
5
5
  SHA512:
6
- metadata.gz: 0cc989a0037051dd2cc84785a7a0a6080667e450ae8f1e95c4204d4474f9fac98d97498f435c82aed20036963ca86425ccf31e3d07ea274e9113e3b79439bb16
7
- data.tar.gz: 6f4e12336f30753d55b53337efd695a712c4971744bd012497f79418890b3ed910084e6af7fe57c5c05025f854f44ffbb969e5c0aa3cba4aa0d9b729d1225267
6
+ metadata.gz: 784c4f79052ef3132ffe26544a79de6f27e9209563cd9c08d475a3e52e0b7b201041f516caebc00315f802c1c9d18e7bd74b0e8e5977576727dccfefe47d0c3b
7
+ data.tar.gz: 6788e1946a1062a3f184b87cf016b7104d6c524621853fb38a8c092d245fa244625a16866dde958bf53caa4bbb113b6d0e01fb09c7340b39633360e40b5d4ee4
@@ -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. We need
123
- (1) a file field for choosing files, and (2) a hidden field for retaining the
124
- uploaded file in case of validation errors and for potential [direct uploads].
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:
@@ -9,15 +9,35 @@ plugin :activerecord
9
9
 
10
10
  ## Attachment
11
11
 
12
- When `Shrine::Attachment` module is included into an `ActiveRecord::Base`
13
- subclass, additional [callbacks] are added to tie the attachment process to the
14
- record lifecycle.
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 Active
73
- Record model, you can set `:callbacks` to `false`:
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.new(:image)
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
- This section will cover methods added to the `Shrine::Attacher` instance. If
151
- you're not familar with how to obtain it, see the [`model`][model] plugin docs.
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 the attacher:
197
+ The following persistence methods are added to `Shrine::Attacher`:
154
198
 
155
199
  | Method | Description |
156
200
  | :----- | :---------- |
@@ -111,8 +111,9 @@ photo.image_data #=>
111
111
  # }
112
112
  ```
113
113
 
114
- When using `Shrine::Attacher` directly, derivatives are created using
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 [`Attacher#create_derivatives`](#creating-derivatives) method will call
312
- the processor and upload results.
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` (or
344
- `Attacher#create_derivatives`) will be forwarded to the processor:
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
- The `Attacher#process_derivatives` method will automatically download the
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 already have the source file locally, or if you're calling multiple
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 to
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  # Model
2
2
 
3
- The [`model`][model] plugin provides integration for handling atatchments on
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, in addition to
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
- These methods read and write attachment data to the `#<name>_data` attribute on
17
- the model instance.
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
- photo.image #=> #<ImageUploader::UploadedFile>
28
- photo.image_url #=> "https://example.com/foo.jpg"
29
- photo.image_attacher #=> #<ImageUploader::Attacher>
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 `type: :entity`:
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, type: :entity)
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`
@@ -9,17 +9,36 @@ plugin :sequel
9
9
 
10
10
  ## Attachment
11
11
 
12
- When `Shrine::Attachment` module is included into a `Sequel::Model` subclass,
13
- additional [hooks] are added to tie the attachment process to the record
14
- lifecycle.
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
- ### Callbacks
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 Callbacks
74
+ #### Skipping Hooks
56
75
 
57
- If you don't want the attachment module to add any callbacks to your Sequel
58
- model, you can set `:callbacks` to `false`:
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, callbacks: false
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.new(:image)
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
- This section will cover methods added to the `Shrine::Attacher` instance. If
115
- you're not familar with how to obtain it, see the [`model`][model] plugin docs.
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 the attacher:
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
- # Defines the following attacher methods for a persistence plugin:
15
+ # Using #<name>_persist, #<name>_reload, and #<name>?, defines the
16
+ # following methods for a persistence plugin:
15
17
  #
16
- # * #persist (calls #<name>_persist and #<name>?)
17
- # * #atomic_persist (calls #<name>_reload, #<name>_persist and #<name>?)
18
- # * #atomic_promote (calls #<name>_reload, #<name>_persist and #<name>?)
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.new(JSON) }
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
- return data unless column_serializer && data
66
-
67
- column_serializer.dump(data)
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
- return data unless column_serializer && data
79
-
80
- column_serializer.load(data)
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 initialize(json)
89
- @json = json
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
- @json.parse(data)
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 = nil, **options)
255
+ def process_derivatives(processor_name, source = file!, **options)
256
256
  processor = self.class.derivatives_processor(processor_name)
257
- fetch_source = source ? source.method(:tap) : file!.method(:download)
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
- attr_reader :type
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(self, options)
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
@@ -16,28 +16,25 @@ class Shrine
16
16
  end
17
17
 
18
18
  module AttachmentMethods
19
- # Allows specifying whether the attachment should be for a model
20
- # (default) or an entity.
19
+ # Allows disabling model behaviour:
21
20
  #
22
- # Shrine::Attachment.new(:image) # model (default)
23
- # Shrine::Attachment.new(:image, type: :model) # model
24
- # Shrine::Attachment.new(:image, type: :entity) # entity
25
- def initialize(name, **options)
26
- super(name, type: :model, **options)
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, if the attachment type is
30
- # still "model". This gives other plugins the ability to force
31
- # attachment type to "entity" for certain classes, and we can skip
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 type == :model
61
+ return super unless model?
65
62
 
66
63
  if !record.instance_variable_get(:"@#{@name}_attacher") || options.any?
67
- attacher = super
68
- attacher.set_model(record, @name)
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
- instance_variable_defined?(:@model)
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
- uploader.opts[:sequel] ||= { callbacks: true, validations: true }
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][:callbacks]
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?
@@ -9,7 +9,7 @@ class Shrine
9
9
  MAJOR = 3
10
10
  MINOR = 0
11
11
  TINY = 0
12
- PRE = "beta"
12
+ PRE = "beta2"
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
15
15
  end
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.beta
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-08-29 00:00:00.000000000 Z
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.3
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