carrierwave 2.0.1 → 2.2.1
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.
- checksums.yaml +4 -4
- data/README.md +79 -35
- data/lib/carrierwave/downloader/base.rb +42 -5
- data/lib/carrierwave/downloader/remote_file.rb +31 -8
- data/lib/carrierwave/locale/en.yml +5 -4
- data/lib/carrierwave/mounter.rb +1 -1
- data/lib/carrierwave/processing.rb +1 -0
- data/lib/carrierwave/processing/mini_magick.rb +1 -1
- data/lib/carrierwave/processing/rmagick.rb +10 -4
- data/lib/carrierwave/processing/vips.rb +284 -0
- data/lib/carrierwave/sanitized_file.rb +15 -8
- data/lib/carrierwave/storage/fog.rb +26 -8
- data/lib/carrierwave/uploader/cache.rb +1 -1
- data/lib/carrierwave/uploader/content_type_blacklist.rb +17 -8
- data/lib/carrierwave/uploader/content_type_whitelist.rb +20 -8
- data/lib/carrierwave/uploader/extension_blacklist.rb +18 -10
- data/lib/carrierwave/uploader/extension_whitelist.rb +19 -10
- data/lib/carrierwave/uploader/url.rb +6 -3
- data/lib/carrierwave/uploader/versions.rb +1 -1
- data/lib/carrierwave/version.rb +1 -1
- data/lib/generators/templates/uploader.rb +2 -2
- metadata +62 -13
@@ -0,0 +1,284 @@
|
|
1
|
+
module CarrierWave
|
2
|
+
|
3
|
+
##
|
4
|
+
# This module simplifies manipulation with vips by providing a set
|
5
|
+
# of convenient helper methods. If you want to use them, you'll need to
|
6
|
+
# require this file:
|
7
|
+
#
|
8
|
+
# require 'carrierwave/processing/vips'
|
9
|
+
#
|
10
|
+
# And then include it in your uploader:
|
11
|
+
#
|
12
|
+
# class MyUploader < CarrierWave::Uploader::Base
|
13
|
+
# include CarrierWave::Vips
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# You can now use the provided helpers:
|
17
|
+
#
|
18
|
+
# class MyUploader < CarrierWave::Uploader::Base
|
19
|
+
# include CarrierWave::Vips
|
20
|
+
#
|
21
|
+
# process :resize_to_fit => [200, 200]
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Or create your own helpers with the powerful vips! method, which
|
25
|
+
# yields an ImageProcessing::Builder object. Check out the ImageProcessing
|
26
|
+
# docs at http://github.com/janko-m/image_processing and the list of all
|
27
|
+
# available Vips options at
|
28
|
+
# https://libvips.github.io/libvips/API/current/using-cli.html for more info.
|
29
|
+
#
|
30
|
+
# class MyUploader < CarrierWave::Uploader::Base
|
31
|
+
# include CarrierWave::Vips
|
32
|
+
#
|
33
|
+
# process :radial_blur => 10
|
34
|
+
#
|
35
|
+
# def radial_blur(amount)
|
36
|
+
# vips! do |builder|
|
37
|
+
# builder.radial_blur(amount)
|
38
|
+
# builder = yield(builder) if block_given?
|
39
|
+
# builder
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# === Note
|
45
|
+
#
|
46
|
+
# The ImageProcessing gem uses ruby-vips, a binding for the vips image
|
47
|
+
# library. You can find more information here:
|
48
|
+
#
|
49
|
+
# https://github.com/libvips/ruby-vips
|
50
|
+
#
|
51
|
+
#
|
52
|
+
module Vips
|
53
|
+
extend ActiveSupport::Concern
|
54
|
+
|
55
|
+
included do
|
56
|
+
require "image_processing/vips"
|
57
|
+
# We need to disable caching since we're editing images in place.
|
58
|
+
::Vips.cache_set_max(0)
|
59
|
+
end
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
def convert(format)
|
63
|
+
process :convert => format
|
64
|
+
end
|
65
|
+
|
66
|
+
def resize_to_limit(width, height)
|
67
|
+
process :resize_to_limit => [width, height]
|
68
|
+
end
|
69
|
+
|
70
|
+
def resize_to_fit(width, height)
|
71
|
+
process :resize_to_fit => [width, height]
|
72
|
+
end
|
73
|
+
|
74
|
+
def resize_to_fill(width, height, gravity='centre')
|
75
|
+
process :resize_to_fill => [width, height, gravity]
|
76
|
+
end
|
77
|
+
|
78
|
+
def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil)
|
79
|
+
process :resize_and_pad => [width, height, background, gravity, alpha]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Changes the image encoding format to the given format
|
85
|
+
#
|
86
|
+
# See https://libvips.github.io/libvips/API/current/using-cli.html#using-command-line-conversion
|
87
|
+
#
|
88
|
+
# === Parameters
|
89
|
+
#
|
90
|
+
# [format (#to_s)] an abbreviation of the format
|
91
|
+
#
|
92
|
+
# === Yields
|
93
|
+
#
|
94
|
+
# [Vips::Image] additional manipulations to perform
|
95
|
+
#
|
96
|
+
# === Examples
|
97
|
+
#
|
98
|
+
# image.convert(:png)
|
99
|
+
#
|
100
|
+
def convert(format, page=nil)
|
101
|
+
vips! do |builder|
|
102
|
+
builder = builder.convert(format)
|
103
|
+
builder = builder.loader(page: page) if page
|
104
|
+
builder
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Resize the image to fit within the specified dimensions while retaining
|
110
|
+
# the original aspect ratio. Will only resize the image if it is larger than the
|
111
|
+
# specified dimensions. The resulting image may be shorter or narrower than specified
|
112
|
+
# in the smaller dimension but will not be larger than the specified values.
|
113
|
+
#
|
114
|
+
# === Parameters
|
115
|
+
#
|
116
|
+
# [width (Integer)] the width to scale the image to
|
117
|
+
# [height (Integer)] the height to scale the image to
|
118
|
+
# [combine_options (Hash)] additional Vips options to apply before resizing
|
119
|
+
#
|
120
|
+
# === Yields
|
121
|
+
#
|
122
|
+
# [Vips::Image] additional manipulations to perform
|
123
|
+
#
|
124
|
+
def resize_to_limit(width, height, combine_options: {})
|
125
|
+
width, height = resolve_dimensions(width, height)
|
126
|
+
|
127
|
+
vips! do |builder|
|
128
|
+
builder.resize_to_limit(width, height)
|
129
|
+
.apply(combine_options)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Resize the image to fit within the specified dimensions while retaining
|
135
|
+
# the original aspect ratio. The image may be shorter or narrower than
|
136
|
+
# specified in the smaller dimension but will not be larger than the specified values.
|
137
|
+
#
|
138
|
+
# === Parameters
|
139
|
+
#
|
140
|
+
# [width (Integer)] the width to scale the image to
|
141
|
+
# [height (Integer)] the height to scale the image to
|
142
|
+
# [combine_options (Hash)] additional Vips options to apply before resizing
|
143
|
+
#
|
144
|
+
# === Yields
|
145
|
+
#
|
146
|
+
# [Vips::Image] additional manipulations to perform
|
147
|
+
#
|
148
|
+
def resize_to_fit(width, height, combine_options: {})
|
149
|
+
width, height = resolve_dimensions(width, height)
|
150
|
+
|
151
|
+
vips! do |builder|
|
152
|
+
builder.resize_to_fit(width, height)
|
153
|
+
.apply(combine_options)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Resize the image to fit within the specified dimensions while retaining
|
159
|
+
# the aspect ratio of the original image. If necessary, crop the image in the
|
160
|
+
# larger dimension.
|
161
|
+
#
|
162
|
+
# === Parameters
|
163
|
+
#
|
164
|
+
# [width (Integer)] the width to scale the image to
|
165
|
+
# [height (Integer)] the height to scale the image to
|
166
|
+
# [combine_options (Hash)] additional vips options to apply before resizing
|
167
|
+
#
|
168
|
+
# === Yields
|
169
|
+
#
|
170
|
+
# [Vips::Image] additional manipulations to perform
|
171
|
+
#
|
172
|
+
def resize_to_fill(width, height, _gravity = nil, combine_options: {})
|
173
|
+
width, height = resolve_dimensions(width, height)
|
174
|
+
|
175
|
+
vips! do |builder|
|
176
|
+
builder.resize_to_fill(width, height).apply(combine_options)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Resize the image to fit within the specified dimensions while retaining
|
182
|
+
# the original aspect ratio. If necessary, will pad the remaining area
|
183
|
+
# with the given color, which defaults to transparent (for gif and png,
|
184
|
+
# white for jpeg).
|
185
|
+
#
|
186
|
+
# See https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsCompassDirection
|
187
|
+
# for gravity options.
|
188
|
+
#
|
189
|
+
# === Parameters
|
190
|
+
#
|
191
|
+
# [width (Integer)] the width to scale the image to
|
192
|
+
# [height (Integer)] the height to scale the image to
|
193
|
+
# [background (List, nil)] the color of the background as a RGB, like [0, 255, 255], nil indicates transparent
|
194
|
+
# [gravity (String)] how to position the image
|
195
|
+
# [alpha (Boolean, nil)] pad the image with the alpha channel if supported
|
196
|
+
# [combine_options (Hash)] additional vips options to apply before resizing
|
197
|
+
#
|
198
|
+
# === Yields
|
199
|
+
#
|
200
|
+
# [Vips::Image] additional manipulations to perform
|
201
|
+
#
|
202
|
+
def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil, combine_options: {})
|
203
|
+
width, height = resolve_dimensions(width, height)
|
204
|
+
|
205
|
+
vips! do |builder|
|
206
|
+
builder.resize_and_pad(width, height, background: background, gravity: gravity, alpha: alpha)
|
207
|
+
.apply(combine_options)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Returns the width of the image in pixels.
|
213
|
+
#
|
214
|
+
# === Returns
|
215
|
+
#
|
216
|
+
# [Integer] the image's width in pixels
|
217
|
+
#
|
218
|
+
def width
|
219
|
+
vips_image.width
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# Returns the height of the image in pixels.
|
224
|
+
#
|
225
|
+
# === Returns
|
226
|
+
#
|
227
|
+
# [Integer] the image's height in pixels
|
228
|
+
#
|
229
|
+
def height
|
230
|
+
vips_image.height
|
231
|
+
end
|
232
|
+
|
233
|
+
# Process the image with vip, using the ImageProcessing gem. This
|
234
|
+
# method will build a "convert" vips command and execute it on the
|
235
|
+
# current image.
|
236
|
+
#
|
237
|
+
# === Gotcha
|
238
|
+
#
|
239
|
+
# This method assumes that the object responds to +current_path+.
|
240
|
+
# Any class that this module is mixed into must have a +current_path+ method.
|
241
|
+
# CarrierWave::Uploader does, so you won't need to worry about this in
|
242
|
+
# most cases.
|
243
|
+
#
|
244
|
+
# === Yields
|
245
|
+
#
|
246
|
+
# [ImageProcessing::Builder] use it to define processing to be performed
|
247
|
+
#
|
248
|
+
# === Raises
|
249
|
+
#
|
250
|
+
# [CarrierWave::ProcessingError] if processing failed.
|
251
|
+
def vips!
|
252
|
+
builder = ImageProcessing::Vips.source(current_path)
|
253
|
+
builder = yield(builder)
|
254
|
+
|
255
|
+
result = builder.call
|
256
|
+
result.close
|
257
|
+
|
258
|
+
FileUtils.mv result.path, current_path
|
259
|
+
|
260
|
+
if File.extname(result.path) != File.extname(current_path)
|
261
|
+
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
|
263
|
+
file.move_to(move_to, permissions, directory_permissions)
|
264
|
+
end
|
265
|
+
rescue ::Vips::Error => e
|
266
|
+
message = I18n.translate(:"errors.messages.vips_processing_error", :e => e)
|
267
|
+
raise CarrierWave::ProcessingError, message
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
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
|
277
|
+
end
|
278
|
+
|
279
|
+
def vips_image
|
280
|
+
::Vips::Image.new_from_buffer(read, "")
|
281
|
+
end
|
282
|
+
|
283
|
+
end # Vips
|
284
|
+
end # CarrierWave
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'pathname'
|
2
2
|
require 'active_support/core_ext/string/multibyte'
|
3
3
|
require 'mini_mime'
|
4
|
-
require '
|
4
|
+
require 'marcel'
|
5
5
|
|
6
6
|
module CarrierWave
|
7
7
|
|
@@ -181,9 +181,9 @@ module CarrierWave
|
|
181
181
|
move!(new_path)
|
182
182
|
chmod!(new_path, permissions)
|
183
183
|
if keep_filename
|
184
|
-
self.file = {:tempfile => new_path, :filename => original_filename, :content_type => content_type}
|
184
|
+
self.file = {:tempfile => new_path, :filename => original_filename, :content_type => @content_type}
|
185
185
|
else
|
186
|
-
self.file = {:tempfile => new_path, :content_type => content_type}
|
186
|
+
self.file = {:tempfile => new_path, :content_type => @content_type}
|
187
187
|
end
|
188
188
|
self
|
189
189
|
end
|
@@ -261,7 +261,7 @@ module CarrierWave
|
|
261
261
|
def content_type
|
262
262
|
@content_type ||=
|
263
263
|
existing_content_type ||
|
264
|
-
|
264
|
+
marcel_magic_content_type ||
|
265
265
|
mini_mime_content_type
|
266
266
|
end
|
267
267
|
|
@@ -305,7 +305,7 @@ module CarrierWave
|
|
305
305
|
def mkdir!(path, directory_permissions)
|
306
306
|
options = {}
|
307
307
|
options[:mode] = directory_permissions if directory_permissions
|
308
|
-
FileUtils.mkdir_p(File.dirname(path), options) unless File.exist?(File.dirname(path))
|
308
|
+
FileUtils.mkdir_p(File.dirname(path), **options) unless File.exist?(File.dirname(path))
|
309
309
|
end
|
310
310
|
|
311
311
|
def chmod!(path, permissions)
|
@@ -328,11 +328,18 @@ module CarrierWave
|
|
328
328
|
end
|
329
329
|
end
|
330
330
|
|
331
|
-
def
|
331
|
+
def marcel_magic_content_type
|
332
332
|
if path
|
333
|
-
File.open(path) do |file|
|
334
|
-
|
333
|
+
type = File.open(path) do |file|
|
334
|
+
Marcel::Magic.by_magic(file).try(:type)
|
335
335
|
end
|
336
|
+
|
337
|
+
if type.nil?
|
338
|
+
type = Marcel::Magic.by_path(file).try(:type)
|
339
|
+
type = 'invalid/invalid' unless type.nil? || type.start_with?('text/')
|
340
|
+
end
|
341
|
+
|
342
|
+
type
|
336
343
|
end
|
337
344
|
rescue Errno::ENOENT
|
338
345
|
nil
|
@@ -147,8 +147,9 @@ module CarrierWave
|
|
147
147
|
:public => uploader.fog_public
|
148
148
|
).files.all(:prefix => uploader.cache_dir).each do |file|
|
149
149
|
# generate_cache_id returns key formated TIMEINT-PID(-COUNTER)-RND
|
150
|
-
|
151
|
-
|
150
|
+
matched = file.key.match(/(\d+)-\d+-\d+(?:-\d+)?/)
|
151
|
+
next unless matched
|
152
|
+
time = Time.at(matched[1].to_i)
|
152
153
|
file.destroy if time < (Time.now.utc - seconds)
|
153
154
|
end
|
154
155
|
end
|
@@ -161,6 +162,8 @@ module CarrierWave
|
|
161
162
|
end
|
162
163
|
|
163
164
|
class File
|
165
|
+
DEFAULT_S3_REGION = 'us-east-1'
|
166
|
+
|
164
167
|
include CarrierWave::Utilities::Uri
|
165
168
|
|
166
169
|
##
|
@@ -194,11 +197,11 @@ module CarrierWave
|
|
194
197
|
# [NilClass] no authenticated url available
|
195
198
|
#
|
196
199
|
def authenticated_url(options = {})
|
197
|
-
if ['AWS', 'Google', 'Rackspace', 'OpenStack', 'AzureRM', 'Aliyun'].include?(@uploader.fog_credentials[:provider])
|
200
|
+
if ['AWS', 'Google', 'Rackspace', 'OpenStack', 'AzureRM', 'Aliyun', 'backblaze'].include?(@uploader.fog_credentials[:provider])
|
198
201
|
# avoid a get by using local references
|
199
202
|
local_directory = connection.directories.new(:key => @uploader.fog_directory)
|
200
203
|
local_file = local_directory.files.new(:key => path)
|
201
|
-
expire_at = options[:expire_at] || ::Fog::Time.now
|
204
|
+
expire_at = options[:expire_at] || ::Fog::Time.now.since(@uploader.fog_authenticated_url_expiration.to_i)
|
202
205
|
case @uploader.fog_credentials[:provider]
|
203
206
|
when 'AWS', 'Google'
|
204
207
|
# Older versions of fog-google do not support options as a parameter
|
@@ -384,9 +387,15 @@ module CarrierWave
|
|
384
387
|
if valid_subdomain
|
385
388
|
s3_subdomain = @uploader.fog_aws_accelerate ? "s3-accelerate" : "s3"
|
386
389
|
"#{protocol}://#{@uploader.fog_directory}.#{s3_subdomain}.amazonaws.com/#{encoded_path}"
|
387
|
-
else
|
388
|
-
|
389
|
-
|
390
|
+
else # directory is not a valid subdomain, so use path style for access
|
391
|
+
region = @uploader.fog_credentials[:region].to_s
|
392
|
+
host = case region
|
393
|
+
when DEFAULT_S3_REGION, ''
|
394
|
+
's3.amazonaws.com'
|
395
|
+
else
|
396
|
+
"s3.#{region}.amazonaws.com"
|
397
|
+
end
|
398
|
+
"#{protocol}://#{host}/#{@uploader.fog_directory}/#{encoded_path}"
|
390
399
|
end
|
391
400
|
end
|
392
401
|
when 'Google'
|
@@ -442,7 +451,7 @@ module CarrierWave
|
|
442
451
|
# @return [CarrierWave::Storage::Fog::File] the location where the file will be stored.
|
443
452
|
#
|
444
453
|
def copy_to(new_path)
|
445
|
-
connection.copy_object(@uploader.fog_directory, file.key, @uploader.fog_directory, new_path,
|
454
|
+
connection.copy_object(@uploader.fog_directory, file.key, @uploader.fog_directory, new_path, copy_options)
|
446
455
|
CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path)
|
447
456
|
end
|
448
457
|
|
@@ -486,9 +495,18 @@ module CarrierWave
|
|
486
495
|
@file ||= directory.files.head(path)
|
487
496
|
end
|
488
497
|
|
498
|
+
def copy_options
|
499
|
+
options = {}
|
500
|
+
options.merge!(acl_header) if acl_header.present?
|
501
|
+
options['Content-Type'] ||= content_type if content_type
|
502
|
+
options.merge(@uploader.fog_attributes)
|
503
|
+
end
|
504
|
+
|
489
505
|
def acl_header
|
490
506
|
if fog_provider == 'AWS'
|
491
507
|
{ 'x-amz-acl' => @uploader.fog_public ? 'public-read' : 'private' }
|
508
|
+
elsif fog_provider == "Google"
|
509
|
+
@uploader.fog_public ? { destination_predefined_acl: "publicRead" } : {}
|
492
510
|
else
|
493
511
|
{}
|
494
512
|
end
|
@@ -8,39 +8,48 @@ module CarrierWave
|
|
8
8
|
end
|
9
9
|
|
10
10
|
##
|
11
|
-
# Override this method in your uploader to provide a
|
11
|
+
# Override this method in your uploader to provide a denylist of files content types
|
12
12
|
# which are not allowed to be uploaded.
|
13
13
|
# Not only strings but Regexp are allowed as well.
|
14
14
|
#
|
15
15
|
# === Returns
|
16
16
|
#
|
17
|
-
# [NilClass, String, Regexp, Array[String, Regexp]] a
|
17
|
+
# [NilClass, String, Regexp, Array[String, Regexp]] a denylist of content types which are not allowed to be uploaded
|
18
18
|
#
|
19
19
|
# === Examples
|
20
20
|
#
|
21
|
-
# def
|
21
|
+
# def content_type_denylist
|
22
22
|
# %w(text/json application/json)
|
23
23
|
# end
|
24
24
|
#
|
25
25
|
# Basically the same, but using a Regexp:
|
26
26
|
#
|
27
|
-
# def
|
27
|
+
# def content_type_denylist
|
28
28
|
# [/(text|application)\/json/]
|
29
29
|
# end
|
30
30
|
#
|
31
|
-
def
|
31
|
+
def content_type_denylist
|
32
|
+
if respond_to?(:content_type_blacklist)
|
33
|
+
ActiveSupport::Deprecation.warn "#content_type_blacklist is deprecated, use #content_type_denylist instead." unless instance_variable_defined?(:@content_type_blacklist_warned)
|
34
|
+
@content_type_blacklist_warned = true
|
35
|
+
content_type_blacklist
|
36
|
+
end
|
37
|
+
end
|
32
38
|
|
33
39
|
private
|
34
40
|
|
35
41
|
def check_content_type_blacklist!(new_file)
|
42
|
+
return unless content_type_denylist
|
43
|
+
|
36
44
|
content_type = new_file.content_type
|
37
|
-
if
|
38
|
-
raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.content_type_blacklist_error",
|
45
|
+
if blacklisted_content_type?(content_type)
|
46
|
+
raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.content_type_blacklist_error",
|
47
|
+
content_type: content_type, default: :"errors.messages.content_type_denylist_error")
|
39
48
|
end
|
40
49
|
end
|
41
50
|
|
42
51
|
def blacklisted_content_type?(content_type)
|
43
|
-
Array(
|
52
|
+
Array(content_type_denylist).any? { |item| content_type =~ /#{item}/ }
|
44
53
|
end
|
45
54
|
|
46
55
|
end # ContentTypeBlacklist
|