carrierwave 1.3.2 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +235 -91
  3. data/lib/carrierwave/compatibility/paperclip.rb +4 -2
  4. data/lib/carrierwave/downloader/base.rb +101 -0
  5. data/lib/carrierwave/downloader/remote_file.rb +68 -0
  6. data/lib/carrierwave/locale/en.yml +9 -6
  7. data/lib/carrierwave/mount.rb +48 -61
  8. data/lib/carrierwave/mounter.rb +167 -77
  9. data/lib/carrierwave/orm/activerecord.rb +15 -55
  10. data/lib/carrierwave/processing/mini_magick.rb +108 -123
  11. data/lib/carrierwave/processing/rmagick.rb +11 -15
  12. data/lib/carrierwave/processing/vips.rb +284 -0
  13. data/lib/carrierwave/processing.rb +1 -0
  14. data/lib/carrierwave/sanitized_file.rb +60 -66
  15. data/lib/carrierwave/storage/abstract.rb +5 -5
  16. data/lib/carrierwave/storage/file.rb +6 -5
  17. data/lib/carrierwave/storage/fog.rb +101 -62
  18. data/lib/carrierwave/storage.rb +1 -0
  19. data/lib/carrierwave/test/matchers.rb +11 -7
  20. data/lib/carrierwave/uploader/cache.rb +40 -24
  21. data/lib/carrierwave/uploader/callbacks.rb +1 -1
  22. data/lib/carrierwave/uploader/configuration.rb +38 -19
  23. data/lib/carrierwave/uploader/content_type_allowlist.rb +62 -0
  24. data/lib/carrierwave/uploader/content_type_denylist.rb +62 -0
  25. data/lib/carrierwave/uploader/dimension.rb +66 -0
  26. data/lib/carrierwave/uploader/download.rb +2 -123
  27. data/lib/carrierwave/uploader/extension_allowlist.rb +63 -0
  28. data/lib/carrierwave/uploader/extension_denylist.rb +64 -0
  29. data/lib/carrierwave/uploader/file_size.rb +2 -2
  30. data/lib/carrierwave/uploader/mountable.rb +6 -0
  31. data/lib/carrierwave/uploader/processing.rb +42 -7
  32. data/lib/carrierwave/uploader/proxy.rb +17 -4
  33. data/lib/carrierwave/uploader/serialization.rb +1 -1
  34. data/lib/carrierwave/uploader/store.rb +47 -7
  35. data/lib/carrierwave/uploader/url.rb +7 -4
  36. data/lib/carrierwave/uploader/versions.rb +153 -105
  37. data/lib/carrierwave/uploader.rb +10 -17
  38. data/lib/carrierwave/utilities/file_name.rb +47 -0
  39. data/lib/carrierwave/utilities/uri.rb +14 -11
  40. data/lib/carrierwave/utilities.rb +1 -0
  41. data/lib/carrierwave/validations/active_model.rb +7 -9
  42. data/lib/carrierwave/version.rb +1 -1
  43. data/lib/carrierwave.rb +13 -17
  44. data/lib/generators/templates/{uploader.rb → uploader.rb.erb} +2 -2
  45. data/lib/generators/uploader_generator.rb +3 -3
  46. metadata +100 -33
  47. data/lib/carrierwave/uploader/content_type_blacklist.rb +0 -48
  48. data/lib/carrierwave/uploader/content_type_whitelist.rb +0 -48
  49. data/lib/carrierwave/uploader/extension_blacklist.rb +0 -51
  50. data/lib/carrierwave/uploader/extension_whitelist.rb +0 -52
@@ -0,0 +1,63 @@
1
+ module CarrierWave
2
+ module Uploader
3
+ module ExtensionAllowlist
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before :cache, :check_extension_allowlist!
8
+ end
9
+
10
+ ##
11
+ # Override this method in your uploader to provide an allowlist of extensions which
12
+ # are allowed to be uploaded. Compares the file's extension case insensitive.
13
+ # Furthermore, not only strings but Regexp are allowed as well.
14
+ #
15
+ # When using a Regexp in the allowlist, `\A` and `\z` are automatically added to
16
+ # the Regexp expression, also case insensitive.
17
+ #
18
+ # === Returns
19
+ #
20
+ # [NilClass, String, Regexp, Array[String, Regexp]] an allowlist of extensions which are allowed to be uploaded
21
+ #
22
+ # === Examples
23
+ #
24
+ # def extension_allowlist
25
+ # %w(jpg jpeg gif png)
26
+ # end
27
+ #
28
+ # Basically the same, but using a Regexp:
29
+ #
30
+ # def extension_allowlist
31
+ # [/jpe?g/, 'gif', 'png']
32
+ # end
33
+ #
34
+ def extension_allowlist
35
+ end
36
+
37
+ private
38
+
39
+ def check_extension_allowlist!(new_file)
40
+ allowlist = extension_allowlist
41
+ if !allowlist && respond_to?(:extension_whitelist) && extension_whitelist
42
+ ActiveSupport::Deprecation.warn "#extension_whitelist is deprecated, use #extension_allowlist instead." unless instance_variable_defined?(:@extension_whitelist_warned)
43
+ @extension_whitelist_warned = true
44
+ allowlist = extension_whitelist
45
+ end
46
+
47
+ return unless allowlist
48
+
49
+ extension = new_file.extension.to_s
50
+ if !allowlisted_extension?(allowlist, extension)
51
+ # Look for whitelist first, then fallback to allowlist
52
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_allowlist_error", extension: new_file.extension.inspect,
53
+ allowed_types: Array(allowlist).join(", "), default: :"errors.messages.extension_whitelist_error")
54
+ end
55
+ end
56
+
57
+ def allowlisted_extension?(allowlist, extension)
58
+ downcase_extension = extension.downcase
59
+ Array(allowlist).any? { |item| downcase_extension =~ /\A#{item}\z/i }
60
+ end
61
+ end # ExtensionAllowlist
62
+ end # Uploader
63
+ end # CarrierWave
@@ -0,0 +1,64 @@
1
+ module CarrierWave
2
+ module Uploader
3
+ module ExtensionDenylist
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before :cache, :check_extension_denylist!
8
+ end
9
+
10
+ ##
11
+ # Override this method in your uploader to provide a denylist of extensions which
12
+ # are prohibited to be uploaded. Compares the file's extension case insensitive.
13
+ # Furthermore, not only strings but Regexp are allowed as well.
14
+ #
15
+ # When using a Regexp in the denylist, `\A` and `\z` are automatically added to
16
+ # the Regexp expression, also case insensitive.
17
+ #
18
+ # === Returns
19
+
20
+ # [NilClass, String, Regexp, Array[String, Regexp]] a deny list of extensions which are prohibited to be uploaded
21
+ #
22
+ # === Examples
23
+ #
24
+ # def extension_denylist
25
+ # %w(swf tiff)
26
+ # end
27
+ #
28
+ # Basically the same, but using a Regexp:
29
+ #
30
+ # def extension_denylist
31
+ # [/swf/, 'tiff']
32
+ # end
33
+ #
34
+ def extension_denylist
35
+ end
36
+
37
+ private
38
+
39
+ def check_extension_denylist!(new_file)
40
+ denylist = extension_denylist
41
+ if !denylist && respond_to?(:extension_blacklist) && extension_blacklist
42
+ ActiveSupport::Deprecation.warn "#extension_blacklist is deprecated, use #extension_denylist instead." unless instance_variable_defined?(:@extension_blacklist_warned)
43
+ @extension_blacklist_warned = true
44
+ denylist = extension_blacklist
45
+ end
46
+
47
+ return unless denylist
48
+
49
+ ActiveSupport::Deprecation.warn "Use of #extension_denylist is deprecated for the security reason, use #extension_allowlist instead to explicitly state what are safe to accept" unless instance_variable_defined?(:@extension_denylist_warned)
50
+ @extension_denylist_warned = true
51
+
52
+ extension = new_file.extension.to_s
53
+ if denylisted_extension?(denylist, extension)
54
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_denylist_error", extension: new_file.extension.inspect,
55
+ prohibited_types: Array(extension_denylist).join(", "), default: :"errors.messages.extension_blacklist_error")
56
+ end
57
+ end
58
+
59
+ def denylisted_extension?(denylist, extension)
60
+ Array(denylist).any? { |item| extension =~ /\A#{item}\z/i }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -14,7 +14,7 @@ module CarrierWave
14
14
  # are allowed to be uploaded.
15
15
  # === Returns
16
16
  #
17
- # [NilClass, Range] a size range which are permitted to be uploaded
17
+ # [NilClass, Range] a size range (in bytes) which are permitted to be uploaded
18
18
  #
19
19
  # === Examples
20
20
  #
@@ -24,7 +24,7 @@ module CarrierWave
24
24
  #
25
25
  def size_range; end
26
26
 
27
- private
27
+ private
28
28
 
29
29
  def check_size!(new_file)
30
30
  size = new_file.size
@@ -33,6 +33,12 @@ module CarrierWave
33
33
  @mounted_as = mounted_as
34
34
  end
35
35
 
36
+ ##
37
+ # Returns array index of given uploader within currently mounted uploaders
38
+ #
39
+ def index
40
+ model.__send__(:_mounter, mounted_as).uploaders.index(self)
41
+ end
36
42
  end # Mountable
37
43
  end # Uploader
38
44
  end # CarrierWave
@@ -18,7 +18,7 @@ module CarrierWave
18
18
  # Adds a processor callback which applies operations as a file is uploaded.
19
19
  # The argument may be the name of any method of the uploader, expressed as a symbol,
20
20
  # or a list of such methods, or a hash where the key is a method and the value is
21
- # an array of arguments to call the method with
21
+ # an array of arguments to call the method with. Also accepts an :if or :unless condition
22
22
  #
23
23
  # === Parameters
24
24
  #
@@ -31,6 +31,7 @@ module CarrierWave
31
31
  # process :sepiatone, :vignette
32
32
  # process :scale => [200, 200]
33
33
  # process :scale => [200, 200], :if => :image?
34
+ # process :scale => [200, 200], :unless => :disallowed_image_type?
34
35
  # process :sepiatone, :if => :image?
35
36
  #
36
37
  # def sepiatone
@@ -49,6 +50,10 @@ module CarrierWave
49
50
  # ...
50
51
  # end
51
52
  #
53
+ # def disallowed_image_type?
54
+ # ...
55
+ # end
56
+ #
52
57
  # end
53
58
  #
54
59
  def process(*args)
@@ -57,12 +62,17 @@ module CarrierWave
57
62
  hash.merge!(arg)
58
63
  end
59
64
 
60
- condition = new_processors.delete(:if)
65
+ condition_type = new_processors.keys.detect { |key| [:if, :unless].include?(key) }
66
+ condition = new_processors.delete(:if) || new_processors.delete(:unless)
61
67
  new_processors.each do |processor, processor_args|
62
- self.processors += [[processor, processor_args, condition]]
68
+ self.processors += [[processor, processor_args, condition, condition_type]]
69
+
70
+ if processor == :convert
71
+ # Treat :convert specially, since it should trigger the file extension change
72
+ force_extension processor_args
73
+ end
63
74
  end
64
75
  end
65
-
66
76
  end # ClassMethods
67
77
 
68
78
  ##
@@ -72,19 +82,44 @@ module CarrierWave
72
82
  return unless enable_processing
73
83
 
74
84
  with_callbacks(:process, new_file) do
75
- self.class.processors.each do |method, args, condition|
76
- if(condition)
85
+ self.class.processors.each do |method, args, condition, condition_type|
86
+ if condition && condition_type == :if
77
87
  if condition.respond_to?(:call)
78
88
  next unless condition.call(self, :args => args, :method => method, :file => new_file)
79
89
  else
80
90
  next unless self.send(condition, new_file)
81
91
  end
92
+ elsif condition && condition_type == :unless
93
+ if condition.respond_to?(:call)
94
+ next if condition.call(self, :args => args, :method => method, :file => new_file)
95
+ elsif self.send(condition, new_file)
96
+ next
97
+ end
98
+ end
99
+
100
+ if args.is_a? Array
101
+ kwargs, args = args.partition { |arg| arg.is_a? Hash }
102
+ end
103
+
104
+ if kwargs.present?
105
+ kwargs = kwargs.reduce(:merge)
106
+ self.send(method, *args, **kwargs)
107
+ else
108
+ self.send(method, *args)
82
109
  end
83
- self.send(method, *args)
84
110
  end
85
111
  end
86
112
  end
87
113
 
114
+ private
115
+
116
+ def forcing_extension(filename)
117
+ if force_extension && filename
118
+ Pathname.new(filename).sub_ext(".#{force_extension.to_s.delete_prefix('.')}").to_s
119
+ else
120
+ filename
121
+ end
122
+ end
88
123
  end # Processing
89
124
  end # Uploader
90
125
  end # CarrierWave
@@ -23,14 +23,27 @@ module CarrierWave
23
23
  alias_method :path, :current_path
24
24
 
25
25
  ##
26
- # Returns a string that uniquely identifies the last stored file
26
+ # Returns a string that uniquely identifies the retrieved or last stored file
27
27
  #
28
28
  # === Returns
29
29
  #
30
30
  # [String] uniquely identifies a file
31
31
  #
32
32
  def identifier
33
- storage.try(:identifier)
33
+ @identifier || (file && storage.try(:identifier))
34
+ end
35
+
36
+ ##
37
+ # Returns a String which is to be used as a temporary value which gets assigned to the column.
38
+ # The purpose is to mark the column that it will be updated. Finally before the save it will be
39
+ # overwritten by the #identifier value, which is usually #filename.
40
+ #
41
+ # === Returns
42
+ #
43
+ # [String] a temporary_identifier, by default @original_filename is used
44
+ #
45
+ def temporary_identifier
46
+ @original_filename || @identifier
34
47
  end
35
48
 
36
49
  ##
@@ -40,8 +53,8 @@ module CarrierWave
40
53
  #
41
54
  # [String] contents of the file
42
55
  #
43
- def read
44
- file.try(:read)
56
+ def read(*args)
57
+ file.try(:read, *args)
45
58
  end
46
59
 
47
60
  ##
@@ -7,7 +7,7 @@ module CarrierWave
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  def serializable_hash(options = nil)
10
- {"url" => url}.merge Hash[versions.map { |name, version| [name, { "url" => version.url }] }]
10
+ {"url" => url}.merge Hash[versions.map { |name, version| [name.to_s, { "url" => version.url }] }]
11
11
  end
12
12
 
13
13
  def as_json(options=nil)
@@ -11,7 +11,7 @@ module CarrierWave
11
11
  prepend Module.new {
12
12
  def initialize(*)
13
13
  super
14
- @file, @filename, @cache_id = nil
14
+ @file, @filename, @cache_id, @identifier, @deduplication_index = nil
15
15
  end
16
16
  }
17
17
  end
@@ -34,9 +34,26 @@ module CarrierWave
34
34
  @filename
35
35
  end
36
36
 
37
+ ##
38
+ # Returns a filename which doesn't conflict with already-stored files.
39
+ #
40
+ # === Returns
41
+ #
42
+ # [String] the filename with suffix added for deduplication
43
+ #
44
+ def deduplicated_filename
45
+ return unless filename
46
+ return filename unless @deduplication_index
47
+
48
+ parts = filename.split('.')
49
+ basename = parts.shift
50
+ basename.sub!(/ ?\(\d+\)\z/, '')
51
+ ([basename.to_s + (@deduplication_index > 1 ? "(#{@deduplication_index})" : '')] + parts).join('.')
52
+ end
53
+
37
54
  ##
38
55
  # Calculates the path where the file should be stored. If +for_file+ is given, it will be
39
- # used as the filename, otherwise +CarrierWave::Uploader#filename+ is assumed.
56
+ # used as the identifier, otherwise +CarrierWave::Uploader#identifier+ is assumed.
40
57
  #
41
58
  # === Parameters
42
59
  #
@@ -46,7 +63,7 @@ module CarrierWave
46
63
  #
47
64
  # [String] the store path
48
65
  #
49
- def store_path(for_file=filename)
66
+ def store_path(for_file=identifier)
50
67
  File.join([store_dir, full_filename(for_file)].compact)
51
68
  end
52
69
 
@@ -60,8 +77,8 @@ module CarrierWave
60
77
  # [new_file (File, IOString, Tempfile)] any kind of file object
61
78
  #
62
79
  def store!(new_file=nil)
63
- cache!(new_file) if new_file && ((@cache_id != parent_cache_id) || @cache_id.nil?)
64
- if !cache_only and @file and @cache_id
80
+ cache!(new_file) if new_file && !cached?
81
+ if !cache_only && @file && @cache_id
65
82
  with_callbacks(:store, new_file) do
66
83
  new_file = storage.store!(@file)
67
84
  if delete_tmp_file_after_storage
@@ -69,7 +86,9 @@ module CarrierWave
69
86
  cache_storage.delete_dir!(cache_path(nil))
70
87
  end
71
88
  @file = new_file
72
- @cache_id = nil
89
+ @identifier = storage.identifier
90
+ @cache_id = @deduplication_index = nil
91
+ @staged = false
73
92
  end
74
93
  end
75
94
  end
@@ -84,13 +103,34 @@ module CarrierWave
84
103
  def retrieve_from_store!(identifier)
85
104
  with_callbacks(:retrieve_from_store, identifier) do
86
105
  @file = storage.retrieve!(identifier)
106
+ @identifier = identifier
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Look for an identifier which doesn't collide with the given already-stored identifiers.
112
+ # It is done by adding a index number as the suffix.
113
+ # For example, if there's 'image.jpg' and the @deduplication_index is set to 2,
114
+ # The stored file will be named as 'image(2).jpg'.
115
+ #
116
+ # === Parameters
117
+ #
118
+ # [current_identifiers (Array[String])] List of identifiers for already-stored files
119
+ #
120
+ def deduplicate(current_identifiers)
121
+ @deduplication_index = nil
122
+ return unless current_identifiers.include?(identifier)
123
+
124
+ (1..current_identifiers.size + 1).each do |i|
125
+ @deduplication_index = i
126
+ break unless current_identifiers.include?(identifier)
87
127
  end
88
128
  end
89
129
 
90
130
  private
91
131
 
92
132
  def full_filename(for_file)
93
- for_file
133
+ forcing_extension(for_file)
94
134
  end
95
135
 
96
136
  def storage
@@ -15,12 +15,15 @@ module CarrierWave
15
15
  # [String] the location where this file is accessible via a url
16
16
  #
17
17
  def url(options = {})
18
- if file.respond_to?(:url) and not (tmp_url = file.url).blank?
19
- file.method(:url).arity == 0 ? tmp_url : file.url(options)
20
- elsif file.respond_to?(:path)
18
+ if file.respond_to?(:url)
19
+ tmp_url = file.method(:url).arity.zero? ? file.url : file.url(options)
20
+ return tmp_url if tmp_url.present?
21
+ end
22
+
23
+ if file.respond_to?(:path)
21
24
  path = encode_path(file.path.sub(File.expand_path(root), ''))
22
25
 
23
- if host = asset_host
26
+ if (host = asset_host)
24
27
  if host.respond_to? :call
25
28
  "#{host.call(file)}#{path}"
26
29
  else