shrine 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,10 +2,11 @@
2
2
  title: Keep Files
3
3
  ---
4
4
 
5
- The [`keep_files`][keep_files] plugin prevents file deletion when the attacher
6
- is about to destroy currently attached or previously attached file. This
7
- functionality is useful when implementing soft deletes, versioning, or in
8
- general any scenario where you need to track history.
5
+ The [`keep_files`][keep_files] plugin prevents the attached file (and any of
6
+ its [derivatives]) from being deleted when the attachment would normally be
7
+ destroyed, which happens when the attachment is removed/replaced, or when the
8
+ record is deleted. This functionality is useful when implementing soft deletes,
9
+ versioning, or in general any scenario where you need to keep history.
9
10
 
10
11
  ```rb
11
12
  plugin :keep_files
@@ -17,3 +18,4 @@ photo.image.exists? #=> true
17
18
  ```
18
19
 
19
20
  [keep_files]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/keep_files.rb
21
+ [derivatives]: https://shrinerb.com/docs/plugins/derivatives
@@ -10,7 +10,7 @@ plugin :validation_helpers
10
10
 
11
11
  Attacher.validate do
12
12
  validate_mime_type %w[image/jpeg image/png image/webp]
13
- validate_max_size 5*1024*1024
13
+ validate_max_size 5*1024*1024 # bytes
14
14
  # ...
15
15
  end
16
16
  ```
data/doc/refile.md CHANGED
@@ -458,7 +458,7 @@ Shrine.plugin :cached_attachment_data
458
458
  ```
459
459
  ```rb
460
460
  form_for @user do |form|
461
- form.hidden_field :profile_image, value: @user.cached_profile_image_data
461
+ form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
462
462
  form.file_field :profile_image
463
463
  end
464
464
  ```
@@ -475,7 +475,7 @@ Shrine.plugin :remove_attachment
475
475
  ```
476
476
  ```rb
477
477
  form_for @user do |form|
478
- form.hidden_field :profile_image, value: @user.cached_profile_image_data
478
+ form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
479
479
  form.file_field :profile_image
480
480
  form.check_box :remove_profile_image
481
481
  end
@@ -491,7 +491,7 @@ Shrine.plugin :remote_url
491
491
  ```
492
492
  ```rb
493
493
  form_for @user do |form|
494
- form.hidden_field :profile_image, value: @user.cached_profile_image_data
494
+ form.hidden_field :profile_image, value: @user.cached_profile_image_data, id: nil
495
495
  form.file_field :profile_image
496
496
  form.text_field :profile_image_remote_url
497
497
  end
@@ -51,7 +51,7 @@ end
51
51
 
52
52
  ```rb
53
53
  form_for @photo do |f|
54
- f.hidden_field :image, value: @photo.cached_image_data
54
+ f.hidden_field :image, value: @photo.cached_image_data, id: nil
55
55
  f.file_filed :image
56
56
  end
57
57
  ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: Shrine 3.5.0
3
+ ---
4
+
5
+ ## New features
6
+
7
+ * The website has been migrated to Docusaurus v2. :sparkles:
8
+
9
+ * The `:signer` option has been added to the `derivation_endpoint` plugin, for when you want to use custom URL signing. This is useful when using `:expires_in`, and wanting to have expiring URLs work with CDN caching.
10
+
11
+ ```rb
12
+ require "aws-sdk-cloudfront"
13
+ signer = Aws::CloudFront::UrlSigner.new(key_pair_id: "...", private_key: "...")
14
+
15
+ plugin :derivation_endpoint,
16
+ expires_in: 90,
17
+ signer: -> (url, expires_in:) do
18
+ signer.signed_url(url, expires: Time.now.to_i + expires_in)
19
+ end
20
+ ```
21
+
22
+ * The S3 storage now supports `:max_multipart_parts` option for specifying the maximum number of concurrent parts in which a large file will get uploaded. This number defaults to `10_000`.
23
+
24
+ ```rb
25
+ Shrine::Storage::S3.new(max_multipart_parts: 1000, ...)
26
+ ```
27
+
28
+ * The `:encoding` option can now be passed to `S3#open`, which is applied to downloaded chunks.
29
+
30
+ ```rb
31
+ io = uploaded_file.open(encoding: Encoding::UTF_8)
32
+ csv = CSV.new(io)
33
+ # ...
34
+ ```
35
+
36
+ ## Other improvements
37
+
38
+ * Passing a boolean value to the `#remove_attachment=` setter now works on Ruby 3.2. Previously this would raise an error, because Shrine would try to call `=~` on it, but `Object#=~` method has been removed in Ruby 3.2.
39
+
40
+ * When duplicating a model instance, the duplicated attacher now references the duplicated model instance instead of the original one.
41
+
42
+ * The download endpoint now returns a `400 Bad Request` response when the serialized file component is invalid.
43
+
44
+ * The `derivatives` plugin now supports passing `mutex: false` option to disable usage of a mutex. This makes the `Shrine::Attacher` object marshallable, which should enable using `Marshal.dump` and `Marshal.load` on model instances with attachments. This should be safe unless you're adding derivatives on the same attacher object concurrently.
45
+
46
+ * When loading the `derivatives` plugin with `versions_compatibility: true`, this setting doesn't leak to other uploaders anymore. Previously if other uploaders would load `derivatives` plugin without this option, versions compatibility would still get enabled for them. This change also fixes behavior on JRuby.
47
+
48
+ * When S3 storage copies files, the AWS tag are not inherited anymore. This allows passing the `:tagging` upload option when promoting from temporary to permanent storage, and have it take effect.
49
+
50
+ * The `UploadedFile#url` method doesn't call the obsolete `URI.regexp` method anymore, which should avoid warnings.
51
+
52
+ * The `infer_extension` plugin now defines `infer_extension` instance method (in addition to class method) on the uploader for convenience, so that it can be easily called at the uploader instance level.
53
+
54
+ ```rb
55
+ class MyUploader < Shrine
56
+ plugin :infer_extension
57
+
58
+ def generate_location(io, metadata:, **)
59
+ extension = infer_extension(metadata["mime_type"])
60
+ # ...
61
+ end
62
+ end
63
+ ```
@@ -124,7 +124,7 @@ end # underlying IO object is closed
124
124
  ```
125
125
 
126
126
  `Shrine::UploadedFile#open` will return the result of a given block.
127
- block. We can use that to safely retrieve the whole content of a file, without
127
+ We can use that to safely retrieve the whole content of a file, without
128
128
  leaving any temporary files lying around.
129
129
 
130
130
  ```rb
data/doc/testing.md CHANGED
@@ -2,6 +2,9 @@
2
2
  title: Testing with Shrine
3
3
  ---
4
4
 
5
+ import Tabs from '@theme/Tabs';
6
+ import TabItem from '@theme/TabItem';
7
+
5
8
  The goal of this guide is to provide some useful tips for testing file
6
9
  attachments implemented with Shrine in your application.
7
10
 
@@ -137,19 +140,26 @@ module TestData
137
140
  end
138
141
  end
139
142
  ```
140
- <!--DOCUSAURUS_CODE_TABS-->
141
- <!--FactoryBot-->
143
+
144
+ <Tabs>
145
+ <TabItem value="factory_bot" label="FactoryBot">
146
+
142
147
  ```rb
143
148
  factory :photo do
144
149
  image_data { TestData.image_data }
145
150
  end
146
151
  ```
147
- <!--Rails YAML fixtures-->
152
+
153
+ </TabItem>
154
+ <TabItem value="fixtures" label="Rails YAML fixtures">
155
+
148
156
  ```erb
149
157
  photo:
150
158
  image_data: <%= TestData.image_data %>
151
159
  ```
152
- <!--END_DOCUSAURUS_CODE_TABS-->
160
+
161
+ </TabItem>
162
+ </Tabs>
153
163
 
154
164
  ## Unit tests
155
165
 
@@ -182,24 +192,33 @@ end
182
192
  In acceptance tests you're testing your app end-to-end, and you likely want to
183
193
  also test file attachments here. Here are examples for some common use cases:
184
194
 
185
- <!--DOCUSAURUS_CODE_TABS-->
186
- <!--Capybara-->
195
+ <Tabs>
196
+ <TabItem value="capybara" label="Capybara">
197
+
187
198
  ```rb
188
199
  attach_file("#image-field", "test/files/image.jpg")
189
200
  ```
190
- <!--Rack::Test-->
201
+
202
+ </TabItem>
203
+ <TabItem value="rack-test" label="rack-test">
204
+
191
205
  ```rb
192
206
  post "/photos", photo: {
193
207
  image: Rack::Test::UploadedFile.new("test/files/image.jpg", "image/jpeg")
194
208
  }
195
209
  ```
196
- <!--Rack::TestApp-->
210
+
211
+ </TabItem>
212
+ </Tabs>
213
+
214
+ If you want to test requests with cached attachment data, you can do so as
215
+ follows:
216
+
197
217
  ```rb
198
- app.post "/photos", multipart: {
199
- "photo[image]" => File.open("test/files/image.jpg")
200
- }
218
+ cached_file = Shrine.upload(file, :cache)
219
+
220
+ post "/photos", photo: { image: cached_file.to_json }
201
221
  ```
202
- <!--END_DOCUSAURUS_CODE_TABS-->
203
222
 
204
223
  ## Background jobs
205
224
 
@@ -207,21 +226,30 @@ If you're using background jobs with Shrine, you probably want to make them
207
226
  synchronous in tests. See your backgrounding library docs for how to make jobs
208
227
  synchronous.
209
228
 
210
- <!--DOCUSAURUS_CODE_TABS-->
211
- <!--ActiveJob-->
229
+ <Tabs>
230
+ <TabItem value="activejob" label="Active Job">
231
+
212
232
  ```rb
213
233
  ActiveJob::Base.queue_adapter = :inline
214
234
  ```
215
- <!--Sidekiq-->
235
+
236
+ </TabItem>
237
+ <TabItem value="sidekiq" label="Sidekiq">
238
+
216
239
  ```rb
217
240
  require "sidekiq/testing"
218
241
  Sidekiq::Testing.inline!
219
242
  ```
220
- <!--SuckerPunch-->
243
+
244
+ </TabItem>
245
+ <TabItem value="sucker_punch" label="SuckerPunch">
246
+
221
247
  ```rb
222
248
  require "sucker_punch/testing/inline"
223
249
  ```
224
- <!--END_DOCUSAURUS_CODE_TABS-->
250
+
251
+ </TabItem>
252
+ </Tabs>
225
253
 
226
254
  ## Processing
227
255
 
@@ -7,9 +7,6 @@ This guide provides instructions for upgrading Shrine in your apps to version
7
7
  3.x. If you're looking for a full list of changes, see the **[3.0 release
8
8
  notes]**.
9
9
 
10
- If you would like assistance with the upgrade, I'm available for consultation,
11
- you can email me at <janko.marohnic@gmail.com>.
12
-
13
10
  ## Attacher
14
11
 
15
12
  The `Shrine::Attacher` class has been rewritten in Shrine 3.0, though much of
@@ -447,7 +444,7 @@ class PromoteJob
447
444
  attacher.create_derivatives # call derivatives processor
448
445
  attacher.atomic_promote
449
446
  rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
450
- # attachment has changed or record has beeen deleted, nothing to do
447
+ # attachment has changed or record has been deleted, nothing to do
451
448
  end
452
449
  end
453
450
  ```
@@ -649,7 +646,8 @@ attacher.copy(other_attacher)
649
646
  with
650
647
 
651
648
  ```rb
652
- attacher.set attacher.upload(other_attacher.file)
649
+ attacher.set nil # clear original attachment
650
+ attacher.attach other_attacher.file, storage: other_attacher.file.storage_key
653
651
  attacher.add_derivatives other_attacher.derivatives # if using derivatives
654
652
  ```
655
653
 
@@ -28,8 +28,8 @@ class Shrine
28
28
  uploader.opts[:derivation_endpoint] ||= { options: {}, derivations: {} }
29
29
  uploader.opts[:derivation_endpoint][:options].merge!(opts)
30
30
 
31
- unless uploader.opts[:derivation_endpoint][:options][:secret_key]
32
- fail Error, "must provide :secret_key option to derivation_endpoint plugin"
31
+ if !uploader.opts[:derivation_endpoint][:options][:secret_key] && !uploader.opts[:derivation_endpoint][:options][:signer]
32
+ fail Error, "must provide :secret_key option to derivation_endpoint plugin when no custom signer is set"
33
33
  end
34
34
 
35
35
  # instrumentation plugin integration
@@ -197,6 +197,7 @@ class Shrine
197
197
  option :metadata, default: -> { [] }
198
198
  option :prefix
199
199
  option :secret_key
200
+ option :signer
200
201
  option :type
201
202
  option :upload, default: -> { false }
202
203
  option :upload_location, default: -> { default_upload_location }, result: -> (o) { upload_location(o) }
@@ -216,7 +217,7 @@ class Shrine
216
217
  option_definition = self.class.options.fetch(name)
217
218
 
218
219
  value = options.fetch(name) { shrine_class.derivation_options[name] }
219
- value = instance_exec(&value) if value.is_a?(Proc)
220
+ value = instance_exec(&value) if value.is_a?(Proc) && value.arity == 0
220
221
 
221
222
  if value.nil?
222
223
  default = option_definition[:default]
@@ -300,20 +301,36 @@ class Shrine
300
301
  end
301
302
 
302
303
  class Derivation::Url < Derivation::Command
303
- delegate :name, :args, :source, :secret_key
304
+ delegate :name, :args, :source, :secret_key, :signer
304
305
 
305
306
  def call(host: nil, prefix: nil, **options)
306
- [host, *prefix, identifier(**options)].join("/")
307
+ base_url = [host, *prefix].join("/")
308
+ path = path_identifier(metadata: options.delete(:metadata))
309
+
310
+ if signer
311
+ url = [base_url, path].join("/")
312
+ signer.call(url, **options)
313
+ else
314
+ signed_part = signed_url("#{path}?#{query(**options)}")
315
+ [base_url, signed_part].join("/")
316
+ end
307
317
  end
308
318
 
309
319
  private
310
320
 
311
- def identifier(expires_in: nil,
312
- version: nil,
313
- type: nil,
314
- filename: nil,
315
- disposition: nil,
316
- metadata: [])
321
+ def path_identifier(metadata: [])
322
+ [
323
+ name,
324
+ *args,
325
+ source.urlsafe_dump(metadata: metadata)
326
+ ].map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
327
+ end
328
+
329
+ def query(expires_in: nil,
330
+ type: nil,
331
+ filename: nil,
332
+ disposition: nil,
333
+ version: nil)
317
334
 
318
335
  params = {}
319
336
  params[:expires_at] = (Time.now + expires_in).to_i if expires_in
@@ -322,23 +339,7 @@ class Shrine
322
339
  params[:filename] = filename if filename
323
340
  params[:disposition] = disposition if disposition
324
341
 
325
- # serializes the source uploaded file into an URL-safe format
326
- source_component = source.urlsafe_dump(metadata: metadata)
327
-
328
- # generate plain URL
329
- url = plain_url(name, *args, source_component, params)
330
-
331
- # generate signed URL
332
- signed_url(url)
333
- end
334
-
335
- def plain_url(*components, params)
336
- # When using Rack < 2, Rack::Utils#escape_path will escape '/'.
337
- # Escape each component and then join them together.
338
- path = components.map{|component| Rack::Utils.escape_path(component.to_s)}.join('/')
339
- query = Rack::Utils.build_query(params)
340
-
341
- "#{path}?#{query}"
342
+ Rack::Utils.build_query(params)
342
343
  end
343
344
 
344
345
  def signed_url(url)
@@ -379,7 +380,7 @@ class Shrine
379
380
  # Returns "404 Not Found" if derivation block is not defined, or if source
380
381
  # file was not found on the storage.
381
382
  def handle_request(request)
382
- verify_signature!(request)
383
+ verify_signature!(request) if secret_key
383
384
  check_expiry!(request)
384
385
 
385
386
  name, *args, serialized_file = request.path_info.split("/")[1..-1]
@@ -12,18 +12,18 @@ class Shrine
12
12
  }.inspect}"
13
13
  end
14
14
 
15
- def self.load_dependencies(uploader, versions_compatibility: false, **)
15
+ def self.load_dependencies(uploader, **)
16
16
  uploader.plugin :default_url
17
-
18
- AttacherMethods.prepend(VersionsCompatibility) if versions_compatibility
19
17
  end
20
18
 
21
- def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
22
- uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key } }
19
+ def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, versions_compatibility: false, **opts)
20
+ uploader.opts[:derivatives] ||= { processors: {}, processor_settings: {}, storage: proc { store_key }, mutex: true }
23
21
  uploader.opts[:derivatives].merge!(opts)
24
22
 
25
23
  # instrumentation plugin integration
26
24
  uploader.subscribe(:derivatives, &log_subscriber) if uploader.respond_to?(:subscribe)
25
+
26
+ uploader::Attacher.include(VersionsCompatibility) if versions_compatibility
27
27
  end
28
28
 
29
29
  module AttachmentMethods
@@ -109,7 +109,7 @@ class Shrine
109
109
  super(**options)
110
110
 
111
111
  @derivatives = derivatives
112
- @derivatives_mutex = Mutex.new
112
+ @derivatives_mutex = Mutex.new if shrine_class.derivatives_options[:mutex]
113
113
  end
114
114
 
115
115
  # Convenience method for accessing derivatives.
@@ -139,7 +139,7 @@ class Shrine
139
139
  # Allows generating a URL to the derivative by passing the derivative
140
140
  # name.
141
141
  #
142
- # attacher.add_derivatives(thumb: thumb)
142
+ # attacher.add_derivatives({ thumb: thumb })
143
143
  # attacher.url(:thumb) #=> "https://example.org/thumb.jpg"
144
144
  def url(*path, **options)
145
145
  return super if path.empty?
@@ -180,7 +180,7 @@ class Shrine
180
180
 
181
181
  # In addition to deleting the main file it also deletes any derivatives.
182
182
  #
183
- # attacher.add_derivatives(thumb: thumb)
183
+ # attacher.add_derivatives({ thumb: thumb })
184
184
  # attacher.derivatives[:thumb].exists? #=> true
185
185
  # attacher.destroy
186
186
  # attacher.derivatives[:thumb].exists? #=> false
@@ -208,7 +208,7 @@ class Shrine
208
208
  # # {
209
209
  # # thumb: #<Shrine::UploadedFile>,
210
210
  # # }
211
- # attacher.add_derivatives(cropped: cropped)
211
+ # attacher.add_derivatives({ cropped: cropped })
212
212
  # attacher.derivatives #=>
213
213
  # # {
214
214
  # # thumb: #<Shrine::UploadedFile>,
@@ -239,7 +239,7 @@ class Shrine
239
239
 
240
240
  # Uploads given hash of files.
241
241
  #
242
- # hash = attacher.upload_derivatives(thumb: thumb)
242
+ # hash = attacher.upload_derivatives({ thumb: thumb })
243
243
  # hash[:thumb] #=> #<Shrine::UploadedFile>
244
244
  def upload_derivatives(files, **options)
245
245
  map_derivative(files) do |path, file|
@@ -298,10 +298,10 @@ class Shrine
298
298
  # Deep merges given uploaded derivatives with current derivatives.
299
299
  #
300
300
  # attacher.derivatives #=> { one: #<Shrine::UploadedFile> }
301
- # attacher.merge_derivatives(two: uploaded_file)
301
+ # attacher.merge_derivatives({ two: uploaded_file })
302
302
  # attacher.derivatives #=> { one: #<Shrine::UploadedFile>, two: #<Shrine::UploadedFile> }
303
303
  def merge_derivatives(new_derivatives)
304
- @derivatives_mutex.synchronize do
304
+ derivatives_synchronize do
305
305
  merged_derivatives = deep_merge_derivatives(derivatives, new_derivatives)
306
306
  set_derivatives(merged_derivatives)
307
307
  end
@@ -379,7 +379,7 @@ class Shrine
379
379
 
380
380
  # Deletes given hash of uploaded files.
381
381
  #
382
- # attacher.delete_derivatives(thumb: uploaded_file)
382
+ # attacher.delete_derivatives({ thumb: uploaded_file })
383
383
  # uploaded_file.exists? #=> false
384
384
  def delete_derivatives(derivatives = self.derivatives)
385
385
  map_derivative(derivatives) { |_, derivative| derivative.delete }
@@ -387,7 +387,7 @@ class Shrine
387
387
 
388
388
  # Sets the given hash of uploaded files as derivatives.
389
389
  #
390
- # attacher.set_derivatives(thumb: uploaded_file)
390
+ # attacher.set_derivatives({ thumb: uploaded_file })
391
391
  # attacher.derivatives #=> { thumb: #<Shrine::UploadedFile> }
392
392
  def set_derivatives(derivatives)
393
393
  self.derivatives = derivatives
@@ -398,7 +398,7 @@ class Shrine
398
398
  # Adds derivative data into the hash.
399
399
  #
400
400
  # attacher.attach(io)
401
- # attacher.add_derivatives(thumb: thumb)
401
+ # attacher.add_derivatives({ thumb: thumb })
402
402
  # attacher.data
403
403
  # #=>
404
404
  # # {
@@ -544,6 +544,14 @@ class Shrine
544
544
  def create_derivatives_on_promote?
545
545
  shrine_class.derivatives_options[:create_on_promote]
546
546
  end
547
+
548
+ def derivatives_synchronize
549
+ if @derivatives_mutex
550
+ @derivatives_mutex.synchronize { yield }
551
+ else
552
+ yield
553
+ end
554
+ end
547
555
  end
548
556
 
549
557
  module ClassMethods
@@ -182,12 +182,19 @@ class Shrine
182
182
  @shrine_class::UploadedFile.urlsafe_load(serialized)
183
183
  rescue Shrine::Error # storage not found
184
184
  not_found!
185
+ rescue JSON::ParserError, ArgumentError => error # invalid serialized component
186
+ raise if error.is_a?(ArgumentError) && error.message != "invalid base64"
187
+ bad_request!("Invalid serialized file")
185
188
  end
186
189
 
187
190
  def not_found!
188
191
  error!(404, "File Not Found")
189
192
  end
190
193
 
194
+ def bad_request!(message)
195
+ error!(400, message)
196
+ end
197
+
191
198
  # Halts the request with the error message.
192
199
  def error!(status, message)
193
200
  throw :halt, [status, { "Content-Type" => "text/plain" }, [message]]
@@ -36,8 +36,14 @@ class Shrine
36
36
  attachment = self
37
37
 
38
38
  # Returns the attached file.
39
- define_method :"#{name}" do |*args|
40
- send(:"#{name}_attacher").get(*args)
39
+ if shrine_class::Attacher.instance_method(:get).arity == 0
40
+ define_method :"#{name}" do
41
+ send(:"#{name}_attacher").get
42
+ end
43
+ else # derivatives
44
+ define_method :"#{name}" do |*args|
45
+ send(:"#{name}_attacher").get(*args)
46
+ end
41
47
  end
42
48
 
43
49
  # Returns the URL to the attached file.
@@ -51,6 +51,10 @@ class Shrine
51
51
  end
52
52
 
53
53
  module InstanceMethods
54
+ def infer_extension(mime_type)
55
+ self.class.infer_extension(mime_type)
56
+ end
57
+
54
58
  private
55
59
 
56
60
  def basic_location(io, metadata:)
@@ -12,7 +12,11 @@ class Shrine
12
12
  def self.configure(uploader, log_subscriber: LOG_SUBSCRIBER, **opts)
13
13
  uploader.opts[:instrumentation] ||= { log_events: EVENTS, subscribers: {} }
14
14
  uploader.opts[:instrumentation].merge!(opts)
15
- uploader.opts[:instrumentation][:notifications] ||= ::ActiveSupport::Notifications
15
+ begin
16
+ uploader.opts[:instrumentation][:notifications] ||= ::ActiveSupport::Notifications
17
+ rescue NameError
18
+ fail Error, "default notifications library is ActiveSupport::Notifications, but activesupport gem is not installed"
19
+ end
16
20
 
17
21
  uploader.opts[:instrumentation][:log_events].each do |event_name|
18
22
  uploader.subscribe(event_name, &log_subscriber)
@@ -48,7 +48,9 @@ class Shrine
48
48
  # The copy constructor that's called on #dup and #clone.
49
49
  define_method :initialize_copy do |other|
50
50
  super(other)
51
- instance_variable_set(:"@#{name}_attacher", instance_variable_get(:"@#{name}_attacher")&.dup)
51
+ attacher_copy = instance_variable_get(:"@#{name}_attacher")&.dup
52
+ attacher_copy.set_entity(self, name) if attacher_copy
53
+ instance_variable_set(:"@#{name}_attacher", attacher_copy)
52
54
  self
53
55
  end
54
56
  private :initialize_copy
@@ -39,6 +39,8 @@ class Shrine
39
39
 
40
40
  # Rails sends "0" or "false" if the checkbox hasn't been ticked.
41
41
  def remove?
42
+ return remove if [true, false].include?(remove)
43
+
42
44
  remove && remove != "" && remove !~ /\A(0|false)\z/
43
45
  end
44
46
  end
@@ -145,7 +145,7 @@ class Shrine
145
145
 
146
146
  # Validates that the dimensions are not smaller than specified.
147
147
  #
148
- # validate_max_dimensions [100, 100]
148
+ # validate_min_dimensions [100, 100]
149
149
  def validate_min_dimensions((min_width, min_height), message: nil)
150
150
  fail Error, "width and/or height metadata is missing" unless file["width"] && file["height"]
151
151