carrierwave 2.2.6 → 3.1.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +180 -62
  3. data/lib/carrierwave/compatibility/paperclip.rb +4 -2
  4. data/lib/carrierwave/downloader/base.rb +20 -12
  5. data/lib/carrierwave/downloader/remote_file.rb +13 -10
  6. data/lib/carrierwave/locale/en.yml +5 -3
  7. data/lib/carrierwave/mount.rb +36 -50
  8. data/lib/carrierwave/mounter.rb +118 -50
  9. data/lib/carrierwave/orm/activerecord.rb +21 -62
  10. data/lib/carrierwave/processing/mini_magick.rb +45 -14
  11. data/lib/carrierwave/processing/rmagick.rb +47 -20
  12. data/lib/carrierwave/processing/vips.rb +43 -12
  13. data/lib/carrierwave/sanitized_file.rb +58 -77
  14. data/lib/carrierwave/storage/abstract.rb +5 -5
  15. data/lib/carrierwave/storage/file.rb +6 -5
  16. data/lib/carrierwave/storage/fog.rb +86 -65
  17. data/lib/carrierwave/test/matchers.rb +11 -7
  18. data/lib/carrierwave/uploader/cache.rb +19 -11
  19. data/lib/carrierwave/uploader/callbacks.rb +1 -1
  20. data/lib/carrierwave/uploader/configuration.rb +18 -8
  21. data/lib/carrierwave/uploader/{content_type_whitelist.rb → content_type_allowlist.rb} +17 -15
  22. data/lib/carrierwave/uploader/{content_type_blacklist.rb → content_type_denylist.rb} +20 -15
  23. data/lib/carrierwave/uploader/dimension.rb +66 -0
  24. data/lib/carrierwave/uploader/{extension_whitelist.rb → extension_allowlist.rb} +17 -15
  25. data/lib/carrierwave/uploader/{extension_blacklist.rb → extension_denylist.rb} +19 -14
  26. data/lib/carrierwave/uploader/file_size.rb +2 -2
  27. data/lib/carrierwave/uploader/processing.rb +34 -6
  28. data/lib/carrierwave/uploader/proxy.rb +16 -3
  29. data/lib/carrierwave/uploader/store.rb +70 -6
  30. data/lib/carrierwave/uploader/url.rb +1 -1
  31. data/lib/carrierwave/uploader/versions.rb +158 -138
  32. data/lib/carrierwave/uploader.rb +10 -8
  33. data/lib/carrierwave/utilities/file_name.rb +47 -0
  34. data/lib/carrierwave/utilities/uri.rb +14 -11
  35. data/lib/carrierwave/utilities.rb +1 -0
  36. data/lib/carrierwave/validations/active_model.rb +4 -6
  37. data/lib/carrierwave/version.rb +1 -1
  38. data/lib/carrierwave.rb +18 -17
  39. data/lib/generators/templates/{uploader.rb → uploader.rb.erb} +1 -1
  40. data/lib/generators/uploader_generator.rb +3 -3
  41. metadata +33 -59
@@ -22,7 +22,7 @@ module CarrierWave
22
22
  # end
23
23
  #
24
24
  # Or create your own helpers with the powerful manipulate! method. Check
25
- # out the RMagick docs at http://www.imagemagick.org/RMagick/doc/ for more
25
+ # out the RMagick docs at https://rmagick.github.io/ for more
26
26
  # info
27
27
  #
28
28
  # class MyUploader < CarrierWave::Uploader::Base
@@ -62,10 +62,12 @@ module CarrierWave
62
62
  begin
63
63
  require "rmagick"
64
64
  rescue LoadError
65
- require "RMagick"
66
- rescue LoadError => e
67
- e.message << " (You may need to install the rmagick gem)"
68
- raise e
65
+ begin
66
+ require "RMagick"
67
+ rescue LoadError => e
68
+ e.message << " (You may need to install the rmagick gem)"
69
+ raise e
70
+ end
69
71
  end
70
72
 
71
73
  prepend Module.new {
@@ -100,16 +102,20 @@ module CarrierWave
100
102
  def resize_to_geometry_string(geometry_string)
101
103
  process :resize_to_geometry_string => [geometry_string]
102
104
  end
105
+
106
+ def crop(left, top, width, height)
107
+ process :crop => [left, top, width, height]
108
+ end
103
109
  end
104
110
 
105
111
  ##
106
112
  # Changes the image encoding format to the given format
107
113
  #
108
- # See even http://www.imagemagick.org/RMagick/doc/magick.html#formats
114
+ # See even https://rmagick.github.io/magick.html#formats
109
115
  #
110
116
  # === Parameters
111
117
  #
112
- # [format (#to_s)] an abreviation of the format
118
+ # [format (#to_s)] an abbreviation of the format
113
119
  #
114
120
  # === Yields
115
121
  #
@@ -159,7 +165,7 @@ module CarrierWave
159
165
  # image may be shorter or narrower than specified in the smaller dimension
160
166
  # but will not be larger than the specified values."
161
167
  #
162
- # See even http://www.imagemagick.org/RMagick/doc/image3.html#resize_to_fit
168
+ # See even https://rmagick.github.io/image3.html#resize_to_fit
163
169
  #
164
170
  # === Parameters
165
171
  #
@@ -185,7 +191,7 @@ module CarrierWave
185
191
  # specified dimensions while retaining the aspect ratio of the original
186
192
  # image. If necessary, crop the image in the larger dimension."
187
193
  #
188
- # See even http://www.imagemagick.org/RMagick/doc/image3.html#resize_to_fill
194
+ # See even https://rmagick.github.io/image3.html#resize_to_fill
189
195
  #
190
196
  # === Parameters
191
197
  #
@@ -228,13 +234,7 @@ module CarrierWave
228
234
  height = dimension_from height
229
235
  manipulate! do |img|
230
236
  img.resize_to_fit!(width, height)
231
- new_img = ::Magick::Image.new(width, height) { |img| img.background_color = background == :transparent ? 'rgba(255,255,255,0)' : background.to_s }
232
- if background == :transparent
233
- filled = new_img.matte_floodfill(1, 1)
234
- else
235
- filled = new_img.color_floodfill(1, 1, ::Magick::Pixel.from_color(background))
236
- end
237
- destroy_image(new_img)
237
+ filled = ::Magick::Image.new(width, height) { |image| image.background_color = background == :transparent ? 'rgba(255,255,255,0)' : background.to_s }
238
238
  filled.composite!(img, gravity, ::Magick::OverCompositeOp)
239
239
  destroy_image(img)
240
240
  filled = yield(filled) if block_given?
@@ -264,6 +264,33 @@ module CarrierWave
264
264
  end
265
265
  end
266
266
 
267
+ ##
268
+ # Crop the image to the contents of a box positioned at [left] and [top], with the dimensions given
269
+ # by [width] and [height]. The original image bottom/right edge is preserved if the cropping box falls
270
+ # outside the image bounds.
271
+ #
272
+ # === Parameters
273
+ #
274
+ # [left (integer)] left edge of area to extract
275
+ # [top (integer)] top edge of area to extract
276
+ # [width (Integer)] width of area to extract
277
+ # [height (Integer)] height of area to extract
278
+ #
279
+ # === Yields
280
+ #
281
+ # [Magick::Image] additional manipulations to perform
282
+ #
283
+ def crop(left, top, width, height, combine_options: {})
284
+ width = dimension_from width
285
+ height = dimension_from height
286
+
287
+ manipulate! do |img|
288
+ img.crop!(left, top, width, height)
289
+ img = yield(img) if block_given?
290
+ img
291
+ end
292
+ end
293
+
267
294
  ##
268
295
  # Returns the width of the image.
269
296
  #
@@ -363,15 +390,15 @@ module CarrierWave
363
390
  if options[:format] || @format
364
391
  frames.write("#{options[:format] || @format}:#{current_path}", &write_block)
365
392
  move_to = current_path.chomp(File.extname(current_path)) + ".#{options[:format] || @format}"
366
- file.content_type = ::MiniMime.lookup_by_filename(move_to).content_type
393
+ file.content_type = Marcel::Magic.by_path(move_to).try(:type)
367
394
  file.move_to(move_to, permissions, directory_permissions)
368
395
  else
369
396
  frames.write(current_path, &write_block)
370
397
  end
371
398
 
372
399
  destroy_image(frames)
373
- rescue ::Magick::ImageMagickError => e
374
- raise CarrierWave::ProcessingError, I18n.translate(:"errors.messages.rmagick_processing_error", :e => e)
400
+ rescue ::Magick::ImageMagickError
401
+ raise CarrierWave::ProcessingError, I18n.translate(:"errors.messages.processing_error")
375
402
  end
376
403
 
377
404
  private
@@ -381,7 +408,7 @@ module CarrierWave
381
408
  proc do |img|
382
409
  options.each do |k, v|
383
410
  if v.is_a?(String) && (matches = v.match(/^["'](.+)["']/))
384
- ActiveSupport::Deprecation.warn "Passing quoted strings like #{v} to #manipulate! is deprecated, pass them without quoting."
411
+ CarrierWave.deprecator.warn "Passing quoted strings like #{v} to #manipulate! is deprecated, pass them without quoting."
385
412
  v = matches[1]
386
413
  end
387
414
  img.public_send(:"#{k}=", v)
@@ -78,6 +78,10 @@ module CarrierWave
78
78
  def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil)
79
79
  process :resize_and_pad => [width, height, background, gravity, alpha]
80
80
  end
81
+
82
+ def crop(left, top, width, height)
83
+ process :crop => [left, top, width, height]
84
+ end
81
85
  end
82
86
 
83
87
  ##
@@ -208,6 +212,33 @@ module CarrierWave
208
212
  end
209
213
  end
210
214
 
215
+ ##
216
+ # Crop the image to the contents of a box positioned at [left] and [top], with the dimensions given
217
+ # by [width] and [height]. The original image bottom/right edge is preserved if the cropping box falls
218
+ # outside the image bounds.
219
+ #
220
+ # === Parameters
221
+ #
222
+ # [left (integer)] left edge of area to extract
223
+ # [top (integer)] top edge of area to extract
224
+ # [width (Integer)] width of area to extract
225
+ # [height (Integer)] height of area to extract
226
+ #
227
+ # === Yields
228
+ #
229
+ # [Vips::Image] additional manipulations to perform
230
+ #
231
+ def crop(left, top, width, height, combine_options: {})
232
+ width, height = resolve_dimensions(width, height)
233
+ width = vips_image.width - left if width + left > vips_image.width
234
+ height = vips_image.height - top if height + top > vips_image.height
235
+
236
+ vips! do |builder|
237
+ builder.crop(left, top, width, height)
238
+ .apply(combine_options)
239
+ end
240
+ end
241
+
211
242
  ##
212
243
  # Returns the width of the image in pixels.
213
244
  #
@@ -259,26 +290,26 @@ module CarrierWave
259
290
 
260
291
  if File.extname(result.path) != File.extname(current_path)
261
292
  move_to = current_path.chomp(File.extname(current_path)) + File.extname(result.path)
262
- file.content_type = ::MiniMime.lookup_by_filename(move_to).content_type
293
+ file.content_type = Marcel::Magic.by_path(move_to).try(:type)
263
294
  file.move_to(move_to, permissions, directory_permissions)
264
295
  end
265
- rescue ::Vips::Error => e
266
- message = I18n.translate(:"errors.messages.vips_processing_error", :e => e)
296
+ rescue ::Vips::Error
297
+ message = I18n.translate(:"errors.messages.processing_error")
267
298
  raise CarrierWave::ProcessingError, message
268
299
  end
269
300
 
270
- private
301
+ private
271
302
 
272
- def resolve_dimensions(*dimensions)
273
- dimensions.map do |value|
274
- next value unless value.instance_of?(Proc)
275
- value.arity >= 1 ? value.call(self) : value.call
276
- end
303
+ def resolve_dimensions(*dimensions)
304
+ dimensions.map do |value|
305
+ next value unless value.instance_of?(Proc)
306
+ value.arity >= 1 ? value.call(self) : value.call
277
307
  end
308
+ end
278
309
 
279
- def vips_image
280
- ::Vips::Image.new_from_buffer(read, "")
281
- end
310
+ def vips_image
311
+ ::Vips::Image.new_from_buffer(read, "")
312
+ end
282
313
 
283
314
  end # Vips
284
315
  end # CarrierWave
@@ -1,6 +1,5 @@
1
1
  require 'pathname'
2
2
  require 'active_support/core_ext/string/multibyte'
3
- require 'mini_mime'
4
3
  require 'marcel'
5
4
 
6
5
  module CarrierWave
@@ -14,6 +13,7 @@ module CarrierWave
14
13
  # It's probably needlessly comprehensive and complex. Help is appreciated.
15
14
  #
16
15
  class SanitizedFile
16
+ include CarrierWave::Utilities::FileName
17
17
 
18
18
  attr_reader :file
19
19
 
@@ -27,7 +27,7 @@ module CarrierWave
27
27
 
28
28
  def initialize(file)
29
29
  self.file = file
30
- @content = nil
30
+ @content = @content_type = nil
31
31
  end
32
32
 
33
33
  ##
@@ -39,7 +39,7 @@ module CarrierWave
39
39
  #
40
40
  def original_filename
41
41
  return @original_filename if @original_filename
42
- if @file and @file.respond_to?(:original_filename)
42
+ if @file && @file.respond_to?(:original_filename)
43
43
  @file.original_filename
44
44
  elsif path
45
45
  File.basename(path)
@@ -59,29 +59,6 @@ module CarrierWave
59
59
 
60
60
  alias_method :identifier, :filename
61
61
 
62
- ##
63
- # Returns the part of the filename before the extension. So if a file is called 'test.jpeg'
64
- # this would return 'test'
65
- #
66
- # === Returns
67
- #
68
- # [String] the first part of the filename
69
- #
70
- def basename
71
- split_extension(filename)[0] if filename
72
- end
73
-
74
- ##
75
- # Returns the file extension
76
- #
77
- # === Returns
78
- #
79
- # [String] the extension
80
- #
81
- def extension
82
- split_extension(filename)[1] if filename
83
- end
84
-
85
62
  ##
86
63
  # Returns the file's size.
87
64
  #
@@ -132,7 +109,7 @@ module CarrierWave
132
109
  # [Boolean] whether the file is valid and has a non-zero size
133
110
  #
134
111
  def empty?
135
- @file.nil? || self.size.nil? || (self.size.zero? && ! self.exists?)
112
+ @file.nil? || self.size.nil? || (self.size.zero? && !self.exists?)
136
113
  end
137
114
 
138
115
  ##
@@ -151,15 +128,21 @@ module CarrierWave
151
128
  #
152
129
  # [String] contents of the file
153
130
  #
154
- def read
131
+ def read(*args)
155
132
  if @content
156
- @content
133
+ if args.empty?
134
+ @content
135
+ else
136
+ length, outbuf = args
137
+ raise ArgumentError, "outbuf argument not supported since the content is already loaded" if outbuf
138
+ @content[0, length]
139
+ end
157
140
  elsif is_path?
158
- File.open(@file, "rb") {|file| file.read}
141
+ File.open(@file, "rb") {|file| file.read(*args)}
159
142
  else
160
143
  @file.try(:rewind)
161
- @content = @file.read
162
- @file.try(:close) unless @file.try(:closed?)
144
+ @content = @file.read(*args)
145
+ @file.try(:close) unless @file.class.ancestors.include?(::StringIO) || @file.try(:closed?)
163
146
  @content
164
147
  end
165
148
  end
@@ -180,13 +163,10 @@ module CarrierWave
180
163
  mkdir!(new_path, directory_permissions)
181
164
  move!(new_path)
182
165
  chmod!(new_path, permissions)
183
- if keep_filename
184
- self.file = {:tempfile => new_path, :filename => original_filename, :content_type => @content_type}
185
- else
186
- self.file = {:tempfile => new_path, :content_type => @content_type}
187
- end
166
+ self.file = {tempfile: new_path, filename: keep_filename ? original_filename : nil, content_type: declared_content_type}
188
167
  self
189
168
  end
169
+
190
170
  ##
191
171
  # Helper to move file to new path.
192
172
  #
@@ -218,7 +198,7 @@ module CarrierWave
218
198
  mkdir!(new_path, directory_permissions)
219
199
  copy!(new_path)
220
200
  chmod!(new_path, permissions)
221
- self.class.new({:tempfile => new_path, :content_type => content_type})
201
+ self.class.new({tempfile: new_path, content_type: declared_content_type})
222
202
  end
223
203
 
224
204
  ##
@@ -260,9 +240,10 @@ module CarrierWave
260
240
  #
261
241
  def content_type
262
242
  @content_type ||=
263
- existing_content_type ||
264
- marcel_magic_content_type ||
265
- mini_mime_content_type
243
+ identified_content_type ||
244
+ declared_content_type ||
245
+ guessed_safe_content_type ||
246
+ Marcel::MimeType::BINARY
266
247
  end
267
248
 
268
249
  ##
@@ -293,11 +274,11 @@ module CarrierWave
293
274
  if file.is_a?(Hash)
294
275
  @file = file["tempfile"] || file[:tempfile]
295
276
  @original_filename = file["filename"] || file[:filename]
296
- @content_type = file["content_type"] || file[:content_type] || file["type"] || file[:type]
277
+ @declared_content_type = file["content_type"] || file[:content_type] || file["type"] || file[:type]
297
278
  else
298
279
  @file = file
299
280
  @original_filename = nil
300
- @content_type = nil
281
+ @declared_content_type = nil
301
282
  end
302
283
  end
303
284
 
@@ -314,57 +295,57 @@ module CarrierWave
314
295
 
315
296
  # Sanitize the filename, to prevent hacking
316
297
  def sanitize(name)
298
+ name = name.scrub
317
299
  name = name.tr("\\", "/") # work-around for IE
318
300
  name = File.basename(name)
319
- name = name.gsub(sanitize_regexp,"_")
301
+ name = name.gsub(sanitize_regexp, "_")
320
302
  name = "_#{name}" if name =~ /\A\.+\z/
321
303
  name = "unnamed" if name.size.zero?
322
- return name.mb_chars.to_s
304
+ name.mb_chars.to_s
323
305
  end
324
306
 
325
- def existing_content_type
326
- if @file.respond_to?(:content_type) && @file.content_type
327
- Marcel::MimeType.for(declared_type: @file.content_type.to_s.chomp)
328
- end
307
+ def declared_content_type
308
+ @declared_content_type ||
309
+ if @file.respond_to?(:content_type) && @file.content_type
310
+ Marcel::MimeType.for(declared_type: @file.content_type.to_s.chomp)
311
+ end
329
312
  end
330
313
 
331
- def marcel_magic_content_type
332
- if path
333
- type = File.open(path) do |file|
334
- Marcel::Magic.by_magic(file).try(:type)
335
- end
314
+ # Guess content type from its file extension. Limit what to be returned to prevent spoofing.
315
+ def guessed_safe_content_type
316
+ return unless path
336
317
 
337
- if type.nil?
338
- type = Marcel::Magic.by_path(path).try(:type)
339
- type = 'invalid/invalid' unless type.nil? || type.start_with?('text/')
340
- end
318
+ type = Marcel::Magic.by_path(original_filename).to_s
319
+ type if type.start_with?('text/') || type.start_with?('application/json')
320
+ end
321
+
322
+ def identified_content_type
323
+ with_io do |io|
324
+ mimetype_by_magic = Marcel::Magic.by_magic(io)
325
+ mimetype_by_path = Marcel::Magic.by_path(path)
341
326
 
342
- type
327
+ return nil if mimetype_by_magic.nil?
328
+
329
+ if mimetype_by_path&.child_of?(mimetype_by_magic.type)
330
+ mimetype_by_path.type
331
+ else
332
+ mimetype_by_magic.type
333
+ end
343
334
  end
344
335
  rescue Errno::ENOENT
345
336
  nil
346
337
  end
347
338
 
348
- def mini_mime_content_type
349
- return unless path
350
- mime_type = ::MiniMime.lookup_by_filename(path)
351
- @content_type = (mime_type && mime_type.content_type).to_s
352
- end
353
-
354
- def split_extension(filename)
355
- # regular expressions to try for identifying extensions
356
- extension_matchers = [
357
- /\A(.+)\.(tar\.([glx]?z|bz2))\z/, # matches "something.tar.gz"
358
- /\A(.+)\.([^\.]+)\z/ # matches "something.jpg"
359
- ]
360
-
361
- extension_matchers.each do |regexp|
362
- if filename =~ regexp
363
- return $1, $2
339
+ def with_io(&block)
340
+ if file.is_a?(IO)
341
+ begin
342
+ yield file
343
+ ensure
344
+ file.try(:rewind)
364
345
  end
346
+ elsif path
347
+ File.open(path, &block)
365
348
  end
366
- return filename, "" # In case we weren't able to split the extension
367
349
  end
368
-
369
350
  end # SanitizedFile
370
351
  end # CarrierWave
@@ -14,7 +14,7 @@ module CarrierWave
14
14
  end
15
15
 
16
16
  def identifier
17
- uploader.filename
17
+ uploader.deduplicated_filename
18
18
  end
19
19
 
20
20
  def store!(file)
@@ -24,19 +24,19 @@ module CarrierWave
24
24
  end
25
25
 
26
26
  def cache!(new_file)
27
- raise NotImplementedError.new("Need to implement #cache! if you want to use #{self.class.name} as a cache storage.")
27
+ raise NotImplementedError, "Need to implement #cache! if you want to use #{self.class.name} as a cache storage."
28
28
  end
29
29
 
30
30
  def retrieve_from_cache!(identifier)
31
- raise NotImplementedError.new("Need to implement #retrieve_from_cache! if you want to use #{self.class.name} as a cache storage.")
31
+ raise NotImplementedError, "Need to implement #retrieve_from_cache! if you want to use #{self.class.name} as a cache storage."
32
32
  end
33
33
 
34
34
  def delete_dir!(path)
35
- raise NotImplementedError.new("Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage.")
35
+ raise NotImplementedError, "Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage."
36
36
  end
37
37
 
38
38
  def clean_cache!(seconds)
39
- raise NotImplementedError.new("Need to implement #clean_cache! if you want to use #{self.class.name} as a cache storage.")
39
+ raise NotImplementedError, "Need to implement #clean_cache! if you want to use #{self.class.name} as a cache storage."
40
40
  end
41
41
  end # Abstract
42
42
  end # Storage
@@ -17,7 +17,7 @@ module CarrierWave
17
17
  #
18
18
  # By default, store!() uses copy_to(), which operates by copying the file
19
19
  # from the cache to the store, then deleting the file from the cache.
20
- # If move_to_store() is overriden to return true, then store!() uses move_to(),
20
+ # If move_to_store() is overridden to return true, then store!() uses move_to(),
21
21
  # which simply moves the file from cache to store. Useful for large files.
22
22
  #
23
23
  # === Parameters
@@ -109,10 +109,11 @@ module CarrierWave
109
109
  end
110
110
 
111
111
  def clean_cache!(seconds)
112
- Dir.glob(::File.expand_path(::File.join(uploader.cache_dir, '*'), CarrierWave.root)).each do |dir|
113
- # generate_cache_id returns key formated TIMEINT-PID(-COUNTER)-RND
114
- time = dir.scan(/(\d+)-\d+-\d+(?:-\d+)?/).first.map(&:to_i)
115
- time = Time.at(*time)
112
+ Dir.glob(::File.expand_path(::File.join(uploader.cache_dir, '*'), uploader.root)).each do |dir|
113
+ # generate_cache_id returns key formatted TIMEINT-PID(-COUNTER)-RND
114
+ matched = dir.scan(/(\d+)-\d+-\d+(?:-\d+)?/).first
115
+ next unless matched
116
+ time = Time.at(matched[0].to_i)
116
117
  if time < (Time.now.utc - seconds)
117
118
  FileUtils.rm_rf(dir)
118
119
  end