carrierwave 1.3.2 → 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
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 +53 -61
  8. data/lib/carrierwave/mounter.rb +167 -77
  9. data/lib/carrierwave/orm/activerecord.rb +23 -58
  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 +157 -109
  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} +3 -3
  45. data/lib/generators/uploader_generator.rb +3 -3
  46. metadata +103 -36
  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 the value of #cache_name is used
44
+ #
45
+ def temporary_identifier
46
+ cache_name || @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
+ @original_filename = @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