shrine 3.0.0 → 3.2.2

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.

Potentially problematic release.


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

Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -33
  3. data/LICENSE.txt +1 -1
  4. data/README.md +94 -4
  5. data/doc/advantages.md +35 -18
  6. data/doc/attacher.md +16 -17
  7. data/doc/carrierwave.md +75 -34
  8. data/doc/changing_derivatives.md +39 -39
  9. data/doc/design.md +134 -85
  10. data/doc/external/articles.md +56 -41
  11. data/doc/external/extensions.md +38 -34
  12. data/doc/getting_started.md +182 -112
  13. data/doc/metadata.md +79 -43
  14. data/doc/multiple_files.md +5 -3
  15. data/doc/paperclip.md +110 -42
  16. data/doc/plugins/activerecord.md +5 -5
  17. data/doc/plugins/add_metadata.md +92 -35
  18. data/doc/plugins/backgrounding.md +12 -2
  19. data/doc/plugins/column.md +36 -7
  20. data/doc/plugins/data_uri.md +2 -2
  21. data/doc/plugins/default_url.md +6 -3
  22. data/doc/plugins/derivation_endpoint.md +26 -28
  23. data/doc/plugins/derivatives.md +205 -169
  24. data/doc/plugins/determine_mime_type.md +2 -2
  25. data/doc/plugins/entity.md +3 -3
  26. data/doc/plugins/form_assign.md +5 -5
  27. data/doc/plugins/included.md +25 -5
  28. data/doc/plugins/infer_extension.md +2 -2
  29. data/doc/plugins/instrumentation.md +1 -1
  30. data/doc/plugins/metadata_attributes.md +21 -10
  31. data/doc/plugins/model.md +4 -4
  32. data/doc/plugins/persistence.md +1 -0
  33. data/doc/plugins/refresh_metadata.md +5 -4
  34. data/doc/plugins/remote_url.md +8 -3
  35. data/doc/plugins/remove_invalid.md +9 -1
  36. data/doc/plugins/sequel.md +4 -4
  37. data/doc/plugins/signature.md +11 -2
  38. data/doc/plugins/store_dimensions.md +2 -2
  39. data/doc/plugins/type_predicates.md +96 -0
  40. data/doc/plugins/upload_endpoint.md +7 -11
  41. data/doc/plugins/upload_options.md +1 -1
  42. data/doc/plugins/url_options.md +2 -2
  43. data/doc/plugins/validation.md +14 -4
  44. data/doc/plugins/validation_helpers.md +3 -3
  45. data/doc/plugins/versions.md +11 -11
  46. data/doc/processing.md +289 -125
  47. data/doc/refile.md +39 -18
  48. data/doc/release_notes/2.19.0.md +1 -1
  49. data/doc/release_notes/3.0.0.md +275 -258
  50. data/doc/release_notes/3.0.1.md +22 -0
  51. data/doc/release_notes/3.1.0.md +73 -0
  52. data/doc/release_notes/3.2.0.md +96 -0
  53. data/doc/release_notes/3.2.1.md +32 -0
  54. data/doc/release_notes/3.2.2.md +14 -0
  55. data/doc/securing_uploads.md +3 -3
  56. data/doc/storage/file_system.md +1 -1
  57. data/doc/storage/memory.md +19 -0
  58. data/doc/storage/s3.md +105 -86
  59. data/doc/testing.md +2 -2
  60. data/doc/upgrading_to_3.md +115 -33
  61. data/doc/validation.md +3 -2
  62. data/lib/shrine.rb +8 -8
  63. data/lib/shrine/attacher.rb +19 -14
  64. data/lib/shrine/attachment.rb +5 -5
  65. data/lib/shrine/plugins.rb +22 -0
  66. data/lib/shrine/plugins/add_metadata.rb +12 -3
  67. data/lib/shrine/plugins/default_storage.rb +6 -6
  68. data/lib/shrine/plugins/default_url.rb +1 -1
  69. data/lib/shrine/plugins/derivation_endpoint.rb +10 -6
  70. data/lib/shrine/plugins/derivatives.rb +19 -17
  71. data/lib/shrine/plugins/determine_mime_type.rb +3 -3
  72. data/lib/shrine/plugins/entity.rb +6 -6
  73. data/lib/shrine/plugins/metadata_attributes.rb +1 -1
  74. data/lib/shrine/plugins/model.rb +3 -3
  75. data/lib/shrine/plugins/presign_endpoint.rb +2 -2
  76. data/lib/shrine/plugins/pretty_location.rb +1 -1
  77. data/lib/shrine/plugins/processing.rb +1 -1
  78. data/lib/shrine/plugins/refresh_metadata.rb +2 -2
  79. data/lib/shrine/plugins/remote_url.rb +3 -3
  80. data/lib/shrine/plugins/remove_invalid.rb +10 -5
  81. data/lib/shrine/plugins/signature.rb +7 -6
  82. data/lib/shrine/plugins/store_dimensions.rb +18 -9
  83. data/lib/shrine/plugins/type_predicates.rb +113 -0
  84. data/lib/shrine/plugins/upload_endpoint.rb +3 -3
  85. data/lib/shrine/plugins/upload_options.rb +2 -2
  86. data/lib/shrine/plugins/url_options.rb +2 -2
  87. data/lib/shrine/plugins/validation.rb +9 -7
  88. data/lib/shrine/storage/linter.rb +4 -4
  89. data/lib/shrine/storage/s3.rb +62 -38
  90. data/lib/shrine/uploaded_file.rb +5 -1
  91. data/lib/shrine/version.rb +2 -2
  92. data/shrine.gemspec +6 -7
  93. metadata +23 -29
@@ -21,10 +21,13 @@ class Shrine
21
21
  module ClassMethods
22
22
  # Calculates `algorithm` hash of the contents of the IO object, and
23
23
  # encodes it into `format`.
24
- def calculate_signature(io, algorithm, format: :hex)
25
- instrument_signature(io, algorithm, format) do
26
- SignatureCalculator.new(algorithm.downcase, format: format).call(io)
27
- end
24
+ def calculate_signature(io, algorithm, format: :hex, rewind: true)
25
+ calculator = SignatureCalculator.new(algorithm.downcase, format: format)
26
+
27
+ signature = instrument_signature(io, algorithm, format) { calculator.call(io) }
28
+ io.rewind if rewind
29
+
30
+ signature
28
31
  end
29
32
  alias signature calculate_signature
30
33
 
@@ -62,8 +65,6 @@ class Shrine
62
65
 
63
66
  def call(io)
64
67
  hash = send(:"calculate_#{algorithm}", io)
65
- io.rewind
66
-
67
68
  send(:"encode_#{format}", hash)
68
69
  end
69
70
 
@@ -113,23 +113,32 @@ class Shrine
113
113
 
114
114
  def extract_with_fastimage(io)
115
115
  require "fastimage"
116
- FastImage.size(io, raise_on_failure: true)
117
- rescue FastImage::FastImageException => error
118
- on_error(error)
116
+
117
+ begin
118
+ FastImage.size(io, raise_on_failure: true)
119
+ rescue FastImage::FastImageException => error
120
+ on_error(error)
121
+ end
119
122
  end
120
123
 
121
124
  def extract_with_mini_magick(io)
122
125
  require "mini_magick"
123
- Shrine.with_file(io) { |file| MiniMagick::Image.new(file.path).dimensions }
124
- rescue MiniMagick::Error => error
125
- on_error(error)
126
+
127
+ begin
128
+ Shrine.with_file(io) { |file| MiniMagick::Image.new(file.path).dimensions }
129
+ rescue MiniMagick::Error => error
130
+ on_error(error)
131
+ end
126
132
  end
127
133
 
128
134
  def extract_with_ruby_vips(io)
129
135
  require "vips"
130
- Shrine.with_file(io) { |file| Vips::Image.new_from_file(file.path).size }
131
- rescue Vips::Error => error
132
- on_error(error)
136
+
137
+ begin
138
+ Shrine.with_file(io) { |file| Vips::Image.new_from_file(file.path).size }
139
+ rescue Vips::Error => error
140
+ on_error(error)
141
+ end
133
142
  end
134
143
 
135
144
  def on_error(error)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ # Documentation can be found on https://shrinerb.com/docs/plugins/type_predicates
6
+ module TypePredicates
7
+ def self.configure(uploader, methods: [], **opts)
8
+ uploader.opts[:type_predicates] ||= { mime: :mini_mime }
9
+ uploader.opts[:type_predicates].merge!(opts)
10
+
11
+ methods.each do |name|
12
+ uploader::UploadedFile.send(:define_method, "#{name}?") { type?(name) }
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def type_lookup(extension, database = nil)
18
+ database ||= opts[:type_predicates][:mime]
19
+ database = MimeDatabase.new(database) if database.is_a?(Symbol)
20
+ database.call(extension.to_s)
21
+ end
22
+ end
23
+
24
+ module FileMethods
25
+ def image?
26
+ general_type?("image")
27
+ end
28
+
29
+ def video?
30
+ general_type?("video")
31
+ end
32
+
33
+ def audio?
34
+ general_type?("audio")
35
+ end
36
+
37
+ def text?
38
+ general_type?("text")
39
+ end
40
+
41
+ def type?(type)
42
+ matching_mime_type = shrine_class.type_lookup(type)
43
+
44
+ fail Error, "type #{type.inspect} is not recognized by the MIME library" unless matching_mime_type
45
+
46
+ mime_type! == matching_mime_type
47
+ end
48
+
49
+ private
50
+
51
+ def general_type?(type)
52
+ mime_type!.start_with?(type)
53
+ end
54
+
55
+ def mime_type!
56
+ mime_type or fail Error, "mime_type metadata value is missing"
57
+ end
58
+ end
59
+
60
+ class MimeDatabase
61
+ SUPPORTED_TOOLS = %i[mini_mime mime_types mimemagic marcel rack_mime]
62
+
63
+ def initialize(tool)
64
+ raise Error, "unknown type database #{tool.inspect}, supported databases are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
65
+
66
+ @tool = tool
67
+ end
68
+
69
+ def call(extension)
70
+ send(:"lookup_with_#{@tool}", extension)
71
+ end
72
+
73
+ private
74
+
75
+ def lookup_with_mini_mime(extension)
76
+ require "mini_mime"
77
+
78
+ info = MiniMime.lookup_by_extension(extension)
79
+ info&.content_type
80
+ end
81
+
82
+ def lookup_with_mime_types(extension)
83
+ require "mime/types"
84
+
85
+ mime_type = MIME::Types.of(".#{extension}").first
86
+ mime_type&.content_type
87
+ end
88
+
89
+ def lookup_with_mimemagic(extension)
90
+ require "mimemagic"
91
+
92
+ magic = MimeMagic.by_extension(".#{extension}")
93
+ magic&.type
94
+ end
95
+
96
+ def lookup_with_marcel(extension)
97
+ require "marcel"
98
+
99
+ type = Marcel::MimeType.for(extension: ".#{extension}")
100
+ type unless type == "application/octet-stream"
101
+ end
102
+
103
+ def lookup_with_rack_mime(extension)
104
+ require "rack/mime"
105
+
106
+ Rack::Mime.mime_type(".#{extension}", nil)
107
+ end
108
+ end
109
+ end
110
+
111
+ register_plugin(:type_predicates, TypePredicates)
112
+ end
113
+ end
@@ -9,11 +9,11 @@ class Shrine
9
9
  module Plugins
10
10
  # Documentation can be found on https://shrinerb.com/docs/plugins/upload_endpoint
11
11
  module UploadEndpoint
12
- def self.load_dependencies(uploader, opts = {})
12
+ def self.load_dependencies(uploader, **)
13
13
  uploader.plugin :rack_file
14
14
  end
15
15
 
16
- def self.configure(uploader, opts = {})
16
+ def self.configure(uploader, **opts)
17
17
  uploader.opts[:upload_endpoint] ||= {}
18
18
  uploader.opts[:upload_endpoint].merge!(opts)
19
19
  end
@@ -156,7 +156,7 @@ class Shrine
156
156
  if @upload
157
157
  @upload.call(io, context, request)
158
158
  else
159
- uploader.upload(io, context)
159
+ uploader.upload(io, **context)
160
160
  end
161
161
  end
162
162
 
@@ -4,9 +4,9 @@ class Shrine
4
4
  module Plugins
5
5
  # Documentation can be found on https://shrinerb.com/docs/plugins/upload_options
6
6
  module UploadOptions
7
- def self.configure(uploader, options = {})
7
+ def self.configure(uploader, **opts)
8
8
  uploader.opts[:upload_options] ||= {}
9
- uploader.opts[:upload_options].merge!(options)
9
+ uploader.opts[:upload_options].merge!(opts)
10
10
  end
11
11
 
12
12
  module InstanceMethods
@@ -4,9 +4,9 @@ class Shrine
4
4
  module Plugins
5
5
  # Documentation can be found on https://shrinerb.com/docs/plugins/url_options
6
6
  module UrlOptions
7
- def self.configure(uploader, **options)
7
+ def self.configure(uploader, **opts)
8
8
  uploader.opts[:url_options] ||= {}
9
- uploader.opts[:url_options].merge!(options)
9
+ uploader.opts[:url_options].merge!(opts)
10
10
  end
11
11
 
12
12
  module FileMethods
@@ -29,14 +29,16 @@ class Shrine
29
29
  @errors = []
30
30
  end
31
31
 
32
- # Leaves out :validate option when calling `Shrine.upload`.
33
- def upload(*args, validate: nil, **options)
34
- super(*args, **options)
32
+ # Performs validations after attaching cached file.
33
+ def attach_cached(value, validate: nil, **options)
34
+ result = super(value, validate: false, **options)
35
+ validation(validate)
36
+ result
35
37
  end
36
38
 
37
- # Performs validations after changing the file.
38
- def change(file, validate: nil, **)
39
- result = super
39
+ # Performs validations after attaching file.
40
+ def attach(io, validate: nil, **options)
41
+ result = super(io, **options)
40
42
  validation(validate)
41
43
  result
42
44
  end
@@ -52,7 +54,7 @@ class Shrine
52
54
  # Calls validation appropriately based on the :validate value.
53
55
  def validation(argument)
54
56
  case argument
55
- when Hash then validate(argument)
57
+ when Hash then validate(**argument)
56
58
  when false then errors.clear # skip validation
57
59
  else validate
58
60
  end
@@ -35,7 +35,7 @@ class Shrine
35
35
  end
36
36
 
37
37
  def call(io_factory = default_io_factory)
38
- storage.upload(io_factory.call, id = "foo", {})
38
+ storage.upload(io_factory.call, id = "foo", shrine_metadata: { "foo" => "bar" })
39
39
 
40
40
  lint_open(id)
41
41
  lint_exists(id)
@@ -67,13 +67,13 @@ class Shrine
67
67
  end
68
68
 
69
69
  def lint_open(id)
70
- opened = storage.open(id, {})
70
+ opened = storage.open(id)
71
71
  error :open, "doesn't return a valid IO object" if !io?(opened)
72
72
  error :open, "returns an empty IO object" if opened.read.empty?
73
73
  opened.close
74
74
 
75
75
  begin
76
- storage.open(@nonexisting, {})
76
+ storage.open(@nonexisting)
77
77
  error :open, "should raise an exception on nonexisting file"
78
78
  rescue Shrine::FileNotFound
79
79
  rescue => exception
@@ -107,7 +107,7 @@ class Shrine
107
107
  end
108
108
 
109
109
  def lint_presign(id)
110
- data = storage.presign(id, {})
110
+ data = storage.presign(id)
111
111
  error :presign, "result should be a Hash" unless data.respond_to?(:to_h)
112
112
  error :presign, "result should include :method key" unless data.to_h.key?(:method)
113
113
  error :presign, "result should include :url key" unless data.to_h.key?(:url)
@@ -103,7 +103,7 @@ class Shrine
103
103
  #
104
104
  # [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
105
105
  def open(id, rewindable: true, **options)
106
- chunks, length = get_object(object(id), options)
106
+ chunks, length = get(object(id), options)
107
107
 
108
108
  Down::ChunkedIO.new(chunks: chunks, rewindable: rewindable, size: length)
109
109
  rescue Aws::S3::Errors::NoSuchKey
@@ -179,25 +179,7 @@ class Shrine
179
179
  options.merge!(@upload_options)
180
180
  options.merge!(presign_options)
181
181
 
182
- if method == :post
183
- presigned_post = object(id).presigned_post(options)
184
-
185
- { method: method, url: presigned_post.url, fields: presigned_post.fields }
186
- else
187
- url = object(id).presigned_url(method, options)
188
-
189
- # When any of these options are specified, the corresponding request
190
- # headers must be included in the upload request.
191
- headers = {}
192
- headers["Content-Length"] = options[:content_length] if options[:content_length]
193
- headers["Content-Type"] = options[:content_type] if options[:content_type]
194
- headers["Content-Disposition"] = options[:content_disposition] if options[:content_disposition]
195
- headers["Content-Encoding"] = options[:content_encoding] if options[:content_encoding]
196
- headers["Content-Language"] = options[:content_language] if options[:content_language]
197
- headers["Content-MD5"] = options[:content_md5] if options[:content_md5]
198
-
199
- { method: method, url: url, headers: headers }
200
- end
182
+ send(:"presign_#{method}", id, options)
201
183
  end
202
184
 
203
185
  # Deletes the file from the storage.
@@ -235,6 +217,17 @@ class Shrine
235
217
 
236
218
  private
237
219
 
220
+ # Uploads the file to S3. Uses multipart upload for large files.
221
+ def put(io, id, **options)
222
+ if io.respond_to?(:size) && io.size && io.size <= @multipart_threshold[:upload]
223
+ object(id).put(body: io, **options)
224
+ else # multipart upload
225
+ object(id).upload_stream(part_size: part_size(io), **options) do |write_stream|
226
+ IO.copy_stream(io, write_stream)
227
+ end
228
+ end
229
+ end
230
+
238
231
  # Copies an existing S3 object to a new location. Uses multipart copy for
239
232
  # large files.
240
233
  def copy(io, id, **copy_options)
@@ -250,15 +243,28 @@ class Shrine
250
243
  object(id).copy_from(io.storage.object(io.id), **options)
251
244
  end
252
245
 
253
- # Uploads the file to S3. Uses multipart upload for large files.
254
- def put(io, id, **options)
255
- if io.respond_to?(:size) && io.size && io.size <= @multipart_threshold[:upload]
256
- object(id).put(body: io, **options)
257
- else # multipart upload
258
- object(id).upload_stream(part_size: part_size(io), **options) do |write_stream|
259
- IO.copy_stream(io, write_stream)
260
- end
261
- end
246
+ # Generates parameters for a POST upload request.
247
+ def presign_post(id, options)
248
+ presigned_post = object(id).presigned_post(options)
249
+
250
+ { method: :post, url: presigned_post.url, fields: presigned_post.fields }
251
+ end
252
+
253
+ # Generates parameters for a PUT upload request.
254
+ def presign_put(id, options)
255
+ url = object(id).presigned_url(:put, options)
256
+
257
+ # When any of these options are specified, the corresponding request
258
+ # headers must be included in the upload request.
259
+ headers = {}
260
+ headers["Content-Length"] = options[:content_length] if options[:content_length]
261
+ headers["Content-Type"] = options[:content_type] if options[:content_type]
262
+ headers["Content-Disposition"] = options[:content_disposition] if options[:content_disposition]
263
+ headers["Content-Encoding"] = options[:content_encoding] if options[:content_encoding]
264
+ headers["Content-Language"] = options[:content_language] if options[:content_language]
265
+ headers["Content-MD5"] = options[:content_md5] if options[:content_md5]
266
+
267
+ { method: :put, url: url, headers: headers }
262
268
  end
263
269
 
264
270
  # Determins the part size that should be used when uploading the given IO
@@ -273,18 +279,36 @@ class Shrine
273
279
  end
274
280
  end
275
281
 
276
- # Aws::S3::Object#get doesn't allow us to get the content length of the
277
- # object before the content is downloaded, so we hack our way around it.
278
- def get_object(object, params)
279
- req = client.build_request(:get_object, **params, bucket: bucket.name, key: object.key)
282
+ if Gem::Version.new(Aws::CORE_GEM_VERSION) >= Gem::Version.new("3.104.0")
283
+ def get(object, params)
284
+ enum = object.enum_for(:get, **params)
285
+
286
+ begin
287
+ content_length = Integer(enum.peek.last["content-length"])
288
+ rescue StopIteration
289
+ content_length = 0
290
+ end
291
+
292
+ chunks = Enumerator.new { |y| loop { y << enum.next.first } }
280
293
 
281
- body = req.enum_for(:send_request)
282
- body.peek # start the request
294
+ [chunks, content_length]
295
+ end
296
+ else
297
+ def get(object, params)
298
+ req = client.build_request(:get_object, bucket: bucket.name, key: object.key, **params)
299
+
300
+ body = req.enum_for(:send_request)
301
+ begin
302
+ body.peek # start the request
303
+ rescue StopIteration
304
+ # the S3 object is empty
305
+ end
283
306
 
284
- content_length = Integer(req.context.http_response.headers["Content-Length"])
285
- chunks = Enumerator.new { |y| loop { y << body.next } }
307
+ content_length = Integer(req.context.http_response.headers["Content-Length"])
308
+ chunks = Enumerator.new { |y| loop { y << body.next } }
286
309
 
287
- [chunks, content_length]
310
+ [chunks, content_length]
311
+ end
288
312
  end
289
313
 
290
314
  # The file is copyable if it's on S3 and on the same Amazon account.
@@ -6,7 +6,6 @@ require "uri"
6
6
 
7
7
  class Shrine
8
8
  # Core class that represents a file uploaded to a storage.
9
- # Base implementation is defined in InstanceMethods and ClassMethods.
10
9
  class UploadedFile
11
10
  @shrine_class = ::Shrine
12
11
 
@@ -249,6 +248,11 @@ class Shrine
249
248
  self.class.shrine_class
250
249
  end
251
250
 
251
+ # Returns simplified inspect output.
252
+ def inspect
253
+ "#<#{self.class.inspect} storage=#{storage_key.inspect} id=#{id.inspect} metadata=#{metadata.inspect}>"
254
+ end
255
+
252
256
  private
253
257
 
254
258
  # Returns an opened IO object for the uploaded file by calling `#open`