shrine 3.4.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/README.md +11 -11
  4. data/doc/advantages.md +1 -1
  5. data/doc/attacher.md +1 -1
  6. data/doc/carrierwave.md +2 -2
  7. data/doc/changing_derivatives.md +2 -1
  8. data/doc/changing_location.md +19 -6
  9. data/doc/design.md +4 -4
  10. data/doc/external/articles.md +16 -16
  11. data/doc/external/extensions.md +1 -1
  12. data/doc/getting_started.md +73 -30
  13. data/doc/multiple_files.md +57 -22
  14. data/doc/plugins/activerecord.md +91 -0
  15. data/doc/plugins/backgrounding.md +30 -4
  16. data/doc/plugins/data_uri.md +15 -0
  17. data/doc/plugins/derivation_endpoint.md +39 -1
  18. data/doc/plugins/derivatives.md +13 -2
  19. data/doc/plugins/download_endpoint.md +16 -4
  20. data/doc/plugins/dynamic_storage.md +15 -0
  21. data/doc/plugins/fallback_storage.md +90 -0
  22. data/doc/plugins/keep_files.md +6 -4
  23. data/doc/plugins/rack_response.md +9 -0
  24. data/doc/plugins/refresh_metadata.md +20 -0
  25. data/doc/plugins/sequel.md +57 -0
  26. data/doc/plugins/signature.md +8 -6
  27. data/doc/plugins/tempfile.md +14 -6
  28. data/doc/plugins/upload_options.md +12 -0
  29. data/doc/plugins/url_options.md +12 -0
  30. data/doc/plugins/validation_helpers.md +1 -1
  31. data/doc/processing.md +5 -3
  32. data/doc/refile.md +3 -3
  33. data/doc/release_notes/2.1.0.md +1 -1
  34. data/doc/release_notes/3.10.0.md +30 -0
  35. data/doc/release_notes/3.5.0.md +63 -0
  36. data/doc/release_notes/3.6.0.md +23 -0
  37. data/doc/release_notes/3.7.0.md +75 -0
  38. data/doc/release_notes/3.7.1.md +31 -0
  39. data/doc/release_notes/3.8.0.md +37 -0
  40. data/doc/release_notes/3.9.0.md +42 -0
  41. data/doc/retrieving_uploads.md +1 -1
  42. data/doc/storage/s3.md +10 -0
  43. data/doc/testing.md +45 -17
  44. data/doc/upgrading_to_3.md +3 -5
  45. data/lib/shrine/attacher.rb +41 -29
  46. data/lib/shrine/attachment.rb +2 -2
  47. data/lib/shrine/plugins/_persistence.rb +1 -1
  48. data/lib/shrine/plugins/_urlsafe_serialization.rb +4 -4
  49. data/lib/shrine/plugins/activerecord.rb +25 -4
  50. data/lib/shrine/plugins/add_metadata.rb +2 -4
  51. data/lib/shrine/plugins/atomic_helpers.rb +7 -7
  52. data/lib/shrine/plugins/backgrounding.rb +10 -10
  53. data/lib/shrine/plugins/column.rb +7 -5
  54. data/lib/shrine/plugins/data_uri.rb +28 -3
  55. data/lib/shrine/plugins/default_url.rb +4 -4
  56. data/lib/shrine/plugins/delete_raw.rb +2 -2
  57. data/lib/shrine/plugins/derivation_endpoint.rb +65 -59
  58. data/lib/shrine/plugins/derivatives.rb +35 -21
  59. data/lib/shrine/plugins/download_endpoint.rb +72 -11
  60. data/lib/shrine/plugins/entity.rb +15 -9
  61. data/lib/shrine/plugins/fallback_storage.rb +51 -0
  62. data/lib/shrine/plugins/infer_extension.rb +5 -1
  63. data/lib/shrine/plugins/instrumentation.rb +13 -9
  64. data/lib/shrine/plugins/metadata_attributes.rb +1 -1
  65. data/lib/shrine/plugins/mirroring.rb +10 -10
  66. data/lib/shrine/plugins/model.rb +12 -10
  67. data/lib/shrine/plugins/presign_endpoint.rb +13 -10
  68. data/lib/shrine/plugins/pretty_location.rb +2 -2
  69. data/lib/shrine/plugins/processing.rb +3 -3
  70. data/lib/shrine/plugins/rack_file.rb +2 -2
  71. data/lib/shrine/plugins/rack_response.rb +12 -6
  72. data/lib/shrine/plugins/refresh_metadata.rb +6 -6
  73. data/lib/shrine/plugins/remote_url.rb +4 -4
  74. data/lib/shrine/plugins/remove_attachment.rb +2 -0
  75. data/lib/shrine/plugins/remove_invalid.rb +4 -0
  76. data/lib/shrine/plugins/restore_cached_data.rb +3 -3
  77. data/lib/shrine/plugins/sequel.rb +15 -1
  78. data/lib/shrine/plugins/signature.rb +2 -2
  79. data/lib/shrine/plugins/store_dimensions.rb +2 -2
  80. data/lib/shrine/plugins/tempfile.rb +0 -2
  81. data/lib/shrine/plugins/upload_endpoint.rb +7 -5
  82. data/lib/shrine/plugins/upload_options.rb +18 -4
  83. data/lib/shrine/plugins/url_options.rb +17 -3
  84. data/lib/shrine/plugins/validation.rb +8 -8
  85. data/lib/shrine/plugins/validation_helpers.rb +2 -2
  86. data/lib/shrine/plugins/versions.rb +10 -10
  87. data/lib/shrine/plugins.rb +6 -14
  88. data/lib/shrine/storage/file_system.rb +15 -19
  89. data/lib/shrine/storage/linter.rb +8 -8
  90. data/lib/shrine/storage/memory.rb +1 -3
  91. data/lib/shrine/storage/s3.rb +60 -40
  92. data/lib/shrine/uploaded_file.rb +23 -19
  93. data/lib/shrine/version.rb +1 -1
  94. data/lib/shrine.rb +24 -20
  95. data/shrine.gemspec +10 -8
  96. metadata +52 -26
@@ -10,7 +10,7 @@ class Shrine
10
10
 
11
11
  module AttachmentMethods
12
12
  # Defines instance methods on initialization.
13
- def initialize(name, **options)
13
+ def initialize(name, **)
14
14
  super
15
15
 
16
16
  define_entity_methods(name)
@@ -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.
@@ -53,16 +59,16 @@ class Shrine
53
59
 
54
60
  # Returns the class attacher instance with loaded entity. It's not
55
61
  # memoized because the entity object could be frozen.
56
- def attacher(record, **options)
57
- attacher = class_attacher(**options)
62
+ def attacher(record, **)
63
+ attacher = class_attacher(**)
58
64
  attacher.load_entity(record, @name)
59
65
  attacher
60
66
  end
61
67
 
62
68
  # Creates an instance of the corresponding attacher class with set
63
69
  # name.
64
- def class_attacher(**options)
65
- attacher = shrine_class::Attacher.new(**@options, **options)
70
+ def class_attacher(**)
71
+ attacher = shrine_class::Attacher.new(**@options, **)
66
72
  attacher.instance_variable_set(:@name, @name)
67
73
  attacher
68
74
  end
@@ -75,8 +81,8 @@ class Shrine
75
81
  #
76
82
  # attacher = Attacher.from_entity(photo, :image)
77
83
  # attacher.file #=> #<Shrine::UploadedFile>
78
- def from_entity(record, name, **options)
79
- attacher = new(**options)
84
+ def from_entity(record, name, **)
85
+ attacher = new(**)
80
86
  attacher.load_entity(record, name)
81
87
  attacher
82
88
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/fallback_storage
6
+ module FallbackStorage
7
+ def self.configure(uploader, **opts)
8
+ uploader.opts[:fallback_storage] ||= { store: :fallback }
9
+ uploader.opts[:fallback_storage].merge!(opts)
10
+
11
+ uploader.find_storage(uploader.opts.dig(:fallback_storage, :store))
12
+ end
13
+
14
+ module FileMethods
15
+ def self.included(base)
16
+ base.class_eval do
17
+ alias_method :storage_exists?, :exists?
18
+
19
+ def exists?
20
+ super || fallback_storage.exists?(id)
21
+ end
22
+ end
23
+ end
24
+
25
+ def url(**)
26
+ if storage_exists?
27
+ super
28
+ else
29
+ fallback_storage.url(id, **)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def _open(**)
36
+ if storage_exists?
37
+ super
38
+ else
39
+ fallback_storage.open(id, **)
40
+ end
41
+ end
42
+
43
+ def fallback_storage
44
+ shrine_class.find_storage(shrine_class.opts.dig(:fallback_storage, :store))
45
+ end
46
+ end
47
+ end
48
+
49
+ register_plugin(:fallback_storage, FallbackStorage)
50
+ end
51
+ end
@@ -46,11 +46,15 @@ class Shrine
46
46
  def instrument_extension(mime_type, &block)
47
47
  return yield unless respond_to?(:instrument)
48
48
 
49
- instrument(:extension, mime_type: mime_type, &block)
49
+ instrument(:extension, mime_type:, &block)
50
50
  end
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)
@@ -165,14 +169,14 @@ class Shrine
165
169
  end
166
170
  end
167
171
 
168
- def library_send(method_name, *args, &block)
172
+ def library_send(method_name, ...)
169
173
  case notifications.to_s
170
174
  when /Dry::Monitor::Notifications/
171
- send(:"dry_monitor_#{method_name}", *args, &block)
175
+ send(:"dry_monitor_#{method_name}", ...)
172
176
  when /ActiveSupport::Notifications/
173
- send(:"active_support_#{method_name}", *args, &block)
177
+ send(:"active_support_#{method_name}", ...)
174
178
  else
175
- notifications.send(method_name, *args, &block)
179
+ notifications.send(method_name, ...)
176
180
  end
177
181
  end
178
182
  end
@@ -220,14 +224,14 @@ class Shrine
220
224
  event.duration.to_i
221
225
  end
222
226
 
223
- def library_send(method_name, *args, &block)
227
+ def library_send(method_name, ...)
224
228
  case event.class.name
225
229
  when "ActiveSupport::Notifications::Event"
226
- send(:"active_support_#{method_name}", *args, &block)
230
+ send(:"active_support_#{method_name}", ...)
227
231
  when "Dry::Events::Event"
228
- send(:"dry_events_#{method_name}", *args, &block)
232
+ send(:"dry_events_#{method_name}", ...)
229
233
  else
230
- event.send(method_name, *args, &block)
234
+ event.send(method_name, ...)
231
235
  end
232
236
  end
233
237
  end
@@ -4,7 +4,7 @@ class Shrine
4
4
  module Plugins
5
5
  # Documentation can be found on https://shrinerb.com/docs/plugins/metadata_attributes
6
6
  module MetadataAttributes
7
- def self.load_dependencies(uploader, *)
7
+ def self.load_dependencies(uploader, **)
8
8
  uploader.plugin :entity
9
9
  end
10
10
 
@@ -51,9 +51,9 @@ class Shrine
51
51
 
52
52
  module InstanceMethods
53
53
  # Mirrors upload to other mirror storages.
54
- def upload(io, mirror: true, **options)
55
- file = super(io, **options)
56
- file.trigger_mirror_upload(**options) if mirror
54
+ def upload(io, mirror: true, **)
55
+ file = super(io, **)
56
+ file.trigger_mirror_upload(**) if mirror
57
57
  file
58
58
  end
59
59
  end
@@ -61,31 +61,31 @@ class Shrine
61
61
  module FileMethods
62
62
  # Mirrors upload if mirrors are defined. Calls mirror block if
63
63
  # registered, otherwise mirrors synchronously.
64
- def trigger_mirror_upload(**options)
64
+ def trigger_mirror_upload(**)
65
65
  return unless shrine_class.mirrors[storage_key] && shrine_class.mirror_upload?
66
66
 
67
67
  if shrine_class.mirror_upload_block
68
- mirror_upload_background(**options)
68
+ mirror_upload_background(**)
69
69
  else
70
- mirror_upload(**options)
70
+ mirror_upload(**)
71
71
  end
72
72
  end
73
73
 
74
74
  # Calls mirror upload block.
75
- def mirror_upload_background(**options)
75
+ def mirror_upload_background(**)
76
76
  fail Error, "mirror upload block is not registered" unless shrine_class.mirror_upload_block
77
77
 
78
- shrine_class.mirror_upload_block.call(self, **options)
78
+ shrine_class.mirror_upload_block.call(self, **)
79
79
  end
80
80
 
81
81
  # Uploads the file to each mirror storage.
82
- def mirror_upload(**options)
82
+ def mirror_upload(**)
83
83
  previously_opened = opened?
84
84
 
85
85
  each_mirror do |mirror|
86
86
  rewind if opened?
87
87
 
88
- shrine_class.upload(self, mirror, **options, location: id, close: false, action: :mirror)
88
+ shrine_class.upload(self, mirror, **, location: id, close: false, action: :mirror)
89
89
  end
90
90
  ensure
91
91
  if opened? && !previously_opened
@@ -18,8 +18,8 @@ class Shrine
18
18
  #
19
19
  # Shrine::Attachment(:image) # model (default)
20
20
  # Shrine::Attachment(:image, model: false) # entity
21
- def initialize(name, model: true, **options)
22
- super(name, **options)
21
+ def initialize(name, model: true, **)
22
+ super(name, **)
23
23
  @model = model
24
24
  end
25
25
 
@@ -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
@@ -80,16 +82,16 @@ class Shrine
80
82
  #
81
83
  # attacher = Attacher.from_model(photo, :image)
82
84
  # attacher.file #=> #<Shrine::UploadedFile>
83
- def from_model(record, name, **options)
84
- attacher = new(**options)
85
+ def from_model(record, name, **)
86
+ attacher = new(**)
85
87
  attacher.load_model(record, name)
86
88
  attacher
87
89
  end
88
90
  end
89
91
 
90
92
  module AttacherMethods
91
- def initialize(model_cache: shrine_class.opts[:model][:cache], **options)
92
- super(**options)
93
+ def initialize(model_cache: shrine_class.opts[:model][:cache], **)
94
+ super(**)
93
95
  @model_cache = model_cache
94
96
  @model = nil
95
97
  end
@@ -109,11 +111,11 @@ class Shrine
109
111
  end
110
112
 
111
113
  # Called by the attachment attribute setter on the model.
112
- def model_assign(value, **options)
114
+ def model_assign(value, **)
113
115
  if model_cache?
114
- assign(value, **options)
116
+ assign(value, **)
115
117
  else
116
- attach(value, **options)
118
+ attach(value, **)
117
119
  end
118
120
  end
119
121
 
@@ -21,12 +21,12 @@ class Shrine
21
21
  # The `storage_key` needs to be one of the registered Shrine storages.
22
22
  # Additional options can be given to override the options given on
23
23
  # plugin initialization.
24
- def presign_endpoint(storage_key, **options)
24
+ def presign_endpoint(storage_key, **)
25
25
  Shrine::PresignEndpoint.new(
26
26
  shrine_class: self,
27
27
  storage_key: storage_key,
28
28
  **opts[:presign_endpoint],
29
- **options,
29
+ **,
30
30
  )
31
31
  end
32
32
 
@@ -36,7 +36,7 @@ class Shrine
36
36
  # It performs the same mounting logic that Rack and other web
37
37
  # frameworks use, and is meant for cases where statically mounting the
38
38
  # endpoint in the router isn't enough.
39
- def presign_response(storage_key, env, **options)
39
+ def presign_response(storage_key, env, **)
40
40
  script_name = env["SCRIPT_NAME"]
41
41
  path_info = env["PATH_INFO"]
42
42
 
@@ -44,7 +44,7 @@ class Shrine
44
44
  env["SCRIPT_NAME"] += path_info
45
45
  env["PATH_INFO"] = ""
46
46
 
47
- presign_endpoint(storage_key, **options).call(env)
47
+ presign_endpoint(storage_key, **).call(env)
48
48
  ensure
49
49
  env["SCRIPT_NAME"] = script_name
50
50
  env["PATH_INFO"] = path_info
@@ -91,7 +91,9 @@ class Shrine
91
91
  end
92
92
  end
93
93
 
94
- headers["Content-Length"] ||= body.map(&:bytesize).inject(0, :+).to_s
94
+ headers = Rack::Headers[headers] if Rack.release >= "3"
95
+ headers["Content-Length"] ||= body.respond_to?(:bytesize) ? body.bytesize.to_s :
96
+ body.map(&:bytesize).inject(0, :+).to_s
95
97
 
96
98
  [status, headers, body]
97
99
  end
@@ -158,16 +160,17 @@ class Shrine
158
160
  # headers, and a body enumerable. If `:rack_response` option is given,
159
161
  # calls that instead.
160
162
  def make_response(object, request)
161
- if @rack_response
162
- response = @rack_response.call(object, request)
163
+ status, headers, body = if @rack_response
164
+ @rack_response.call(object, request)
163
165
  else
164
- response = [200, { "Content-Type" => CONTENT_TYPE_JSON }, [object.to_json]]
166
+ [200, { "Content-Type" => CONTENT_TYPE_JSON }, [object.to_json]]
165
167
  end
166
168
 
169
+ headers = Rack::Headers[headers] if Rack.release >= "3"
167
170
  # prevent browsers from caching the response
168
- response[1]["Cache-Control"] = "no-store" unless response[1].key?("Cache-Control")
171
+ headers["Cache-Control"] = "no-store" unless headers.key?("Cache-Control")
169
172
 
170
- response
173
+ [status, headers, body]
171
174
  end
172
175
 
173
176
  # Used for early returning an error response.
@@ -10,8 +10,8 @@ class Shrine
10
10
  end
11
11
 
12
12
  module InstanceMethods
13
- def generate_location(io, **options)
14
- pretty_location(io, **options)
13
+ def generate_location(io, **)
14
+ pretty_location(io, **)
15
15
  end
16
16
 
17
17
  def pretty_location(io, name: nil, record: nil, version: nil, derivative: nil, identifier: nil, metadata: {}, **)
@@ -18,14 +18,14 @@ class Shrine
18
18
  end
19
19
 
20
20
  module InstanceMethods
21
- def upload(io, process: true, **options)
21
+ def upload(io, process: true, **)
22
22
  if process
23
- input = process(io, **options)
23
+ input = process(io, **)
24
24
  else
25
25
  input = io
26
26
  end
27
27
 
28
- super(input, **options)
28
+ super(input, **)
29
29
  end
30
30
 
31
31
  private
@@ -24,9 +24,9 @@ class Shrine
24
24
  module AttacherMethods
25
25
  # Checks whether a file is a Rack file hash, and in that case wraps the
26
26
  # hash in an IO-like object.
27
- def assign(value, **options)
27
+ def assign(value, **)
28
28
  if rack_file?(value)
29
- assign shrine_class.rack_file(value), **options
29
+ assign shrine_class.rack_file(value), **
30
30
  else
31
31
  super
32
32
  end
@@ -10,8 +10,8 @@ class Shrine
10
10
  module RackResponse
11
11
  module FileMethods
12
12
  # Returns a Rack response triple for the uploaded file.
13
- def to_rack_response(**options)
14
- FileResponse.new(self).call(**options)
13
+ def to_rack_response(**)
14
+ FileResponse.new(self).call(**)
15
15
  end
16
16
  end
17
17
 
@@ -32,6 +32,8 @@ class Shrine
32
32
  headers = rack_headers(**options)
33
33
  body = rack_body(**options)
34
34
 
35
+ headers = Rack::Headers[headers] if Rack.release >= "3"
36
+
35
37
  [status, headers, body]
36
38
  end
37
39
 
@@ -47,14 +49,14 @@ class Shrine
47
49
  # "Content-Disposition" headers, whose values are extracted from
48
50
  # metadata. Also returns the correct "Content-Range" header on ranged
49
51
  # requests.
50
- def rack_headers(filename: nil, type: nil, disposition: "inline", range: false)
52
+ def rack_headers(filename: nil, type: nil, disposition: "inline", range: false, etag: nil)
51
53
  {
52
54
  "Content-Length" => content_length(range),
53
55
  "Content-Type" => content_type(type),
54
56
  "Content-Disposition" => content_disposition(disposition, filename),
55
57
  "Content-Range" => content_range(range),
56
58
  "Accept-Ranges" => accept_ranges(range),
57
- "ETag" => etag,
59
+ "ETag" => etag || self.etag,
58
60
  }.compact
59
61
  end
60
62
 
@@ -73,7 +75,7 @@ class Shrine
73
75
  def content_disposition(disposition, filename)
74
76
  filename ||= file.original_filename || file.id.split("/").last
75
77
 
76
- ContentDisposition.format(disposition: disposition, filename: filename)
78
+ ContentDisposition.format(disposition:, filename:)
77
79
  end
78
80
 
79
81
  # Value for the "Content-Range" header.
@@ -96,7 +98,7 @@ class Shrine
96
98
  # Returns an object that responds to #each and #close, which yields
97
99
  # contents of the file.
98
100
  def rack_body(range: nil, **)
99
- FileBody.new(file, range: range)
101
+ FileBody.new(file, range:)
100
102
  end
101
103
 
102
104
  # Retrieves a range value parsed from HTTP "Range" header.
@@ -141,6 +143,10 @@ class Shrine
141
143
  file.close
142
144
  end
143
145
 
146
+ def bytesize
147
+ each.inject(0) { |sum, chunk| sum += chunk.length }
148
+ end
149
+
144
150
  # Rack::Sendfile is activated when response body responds to #to_path.
145
151
  def respond_to_missing?(name, include_private = false)
146
152
  name == :to_path && path
@@ -5,19 +5,19 @@ class Shrine
5
5
  # Documentation can be found on https://shrinerb.com/docs/plugins/refresh_metadata
6
6
  module RefreshMetadata
7
7
  module AttacherMethods
8
- def refresh_metadata!(**options)
9
- file!.refresh_metadata!(**context, **options)
8
+ def refresh_metadata!(**)
9
+ file!.refresh_metadata!(**context, **)
10
10
  set(file) # trigger model write
11
11
  end
12
12
  end
13
13
 
14
14
  module FileMethods
15
- def refresh_metadata!(**options)
16
- return open { refresh_metadata!(**options) } unless opened?
15
+ def refresh_metadata!(replace: false, **)
16
+ return open { refresh_metadata!(replace:, **) } unless opened?
17
17
 
18
- refreshed_metadata = uploader.send(:get_metadata, self, metadata: true, **options)
18
+ refreshed_metadata = uploader.send(:get_metadata, self, metadata: true, **)
19
19
 
20
- @metadata = @metadata.merge(refreshed_metadata)
20
+ @metadata = replace ? refreshed_metadata : @metadata.merge(refreshed_metadata)
21
21
  end
22
22
  end
23
23
  end
@@ -18,7 +18,7 @@ class Shrine
18
18
 
19
19
  DOWNLOADER = -> (url, **options) { Down.download(url, **options) }
20
20
 
21
- def self.load_dependencies(uploader, *)
21
+ def self.load_dependencies(uploader, **)
22
22
  uploader.plugin :validation
23
23
  end
24
24
 
@@ -84,11 +84,11 @@ class Shrine
84
84
  # Downloads the remote file and assigns it. If download failed, sets
85
85
  # the error message and assigns the url to an instance variable so that
86
86
  # it shows up in the form.
87
- def assign_remote_url(url, downloader: {}, **options)
87
+ def assign_remote_url(url, downloader: {}, **)
88
88
  return if url == "" || url.nil?
89
89
 
90
90
  downloaded_file = shrine_class.remote_url(url, **downloader)
91
- attach_cached(downloaded_file, **options)
91
+ attach_cached(downloaded_file, **)
92
92
  rescue DownloadError => error
93
93
  errors.clear << remote_url_error_message(url, error)
94
94
  false
@@ -110,7 +110,7 @@ class Shrine
110
110
  # Generates an error message for failed remote URL download.
111
111
  def remote_url_error_message(url, error)
112
112
  message = shrine_class.opts[:remote_url][:error_message]
113
- message = message.call *[url, error].take(message.arity.abs) if message.respond_to?(:call)
113
+ message = message.call(*[url, error].take(message.arity.abs)) if message.respond_to?(:call)
114
114
  message || "download failed: #{error.message}"
115
115
  end
116
116
  end
@@ -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
@@ -26,6 +26,10 @@ class Shrine
26
26
  else
27
27
  load_data nil
28
28
  end
29
+
30
+ # `load_data` bypasses `Attacher#set`, so when the model plugin is
31
+ # loaded the record attribute needs to be synced manually.
32
+ write if respond_to?(:write)
29
33
  end
30
34
  end
31
35
  end
@@ -11,14 +11,14 @@ class Shrine
11
11
  module AttacherMethods
12
12
  private
13
13
 
14
- def cached(value, **options)
14
+ def cached(value, **)
15
15
  cached_file = super
16
16
 
17
17
  # TODO: Remove this conditional when we remove the versions plugin
18
18
  if cached_file.is_a?(Hash) || cached_file.is_a?(Array)
19
- uploaded_file(cached_file) { |file| file.refresh_metadata!(**context, **options) }
19
+ uploaded_file(cached_file) { |file| file.refresh_metadata!(**context, **) }
20
20
  else
21
- cached_file.refresh_metadata!(**context, **options)
21
+ cached_file.refresh_metadata!(**context, **)
22
22
  end
23
23
 
24
24
  cached_file
@@ -87,8 +87,11 @@ class Shrine
87
87
  end
88
88
  end
89
89
 
90
- # Calls Attacher#save. Called before model save.
90
+ # Calls Attacher#save. Called before model save. Remembers whether
91
+ # the record is new, since by the time `#sequel_after_save` runs,
92
+ # Sequel has already flipped `#new?` to false.
91
93
  def sequel_before_save
94
+ @previously_new_record = record.new?
92
95
  save
93
96
  end
94
97
 
@@ -132,6 +135,17 @@ class Shrine
132
135
  def sequel?
133
136
  record.is_a?(::Sequel::Model)
134
137
  end
138
+
139
+ # Prevents the previous file from being destroyed when this save
140
+ # created the record (as opposed to updating an existing one), since
141
+ # a record being created can't yet have its own confirmed attachment
142
+ # to safely replace (e.g. when it was duplicated from another,
143
+ # still-persisted record).
144
+ def destroy_previous?
145
+ return super unless sequel?
146
+
147
+ super && !@previously_new_record
148
+ end
135
149
  end
136
150
  end
137
151
 
@@ -37,7 +37,7 @@ class Shrine
37
37
  def instrument_signature(io, algorithm, format, &block)
38
38
  return yield unless respond_to?(:instrument)
39
39
 
40
- instrument(:signature, io: io, algorithm: algorithm, format: format, &block)
40
+ instrument(:signature, io:, algorithm:, format:, &block)
41
41
  end
42
42
  end
43
43
 
@@ -45,7 +45,7 @@ class Shrine
45
45
  # Calculates `algorithm` hash of the contents of the IO object, and
46
46
  # encodes it into `format`.
47
47
  def calculate_signature(io, algorithm, format: :hex)
48
- self.class.calculate_signature(io, algorithm, format: format)
48
+ self.class.calculate_signature(io, algorithm, format:)
49
49
  end
50
50
  end
51
51
 
@@ -57,7 +57,7 @@ class Shrine
57
57
  def dimensions_analyzer(name)
58
58
  on_error = opts[:store_dimensions][:on_error]
59
59
 
60
- DimensionsAnalyzer.new(name, on_error: on_error).method(:call)
60
+ DimensionsAnalyzer.new(name, on_error:).method(:call)
61
61
  end
62
62
 
63
63
  private
@@ -66,7 +66,7 @@ class Shrine
66
66
  def instrument_dimensions(io, &block)
67
67
  return yield unless respond_to?(:instrument)
68
68
 
69
- instrument(:image_dimensions, io: io, &block)
69
+ instrument(:image_dimensions, io:, &block)
70
70
  end
71
71
  end
72
72
 
@@ -19,8 +19,6 @@ class Shrine
19
19
 
20
20
  module FileMethods
21
21
  def tempfile
22
- raise Error, "uploaded file must be opened" unless @io
23
-
24
22
  @tempfile ||= download
25
23
  @tempfile.rewind
26
24
  @tempfile